Last active
November 1, 2024 08:17
-
-
Save maxkostinevich/aad64bffee69f179f9b70c2f5f3cc8f1 to your computer and use it in GitHub Desktop.
Cloudflare Worker - Handle Contact Form
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
/* | |
* Serverless contact form handler for Cloudflare Workers. | |
* Emails are sent via Mailgun. | |
* | |
* Learn more at https://maxkostinevich.com/blog/serverless-contact-form | |
* Live demo: https://codesandbox.io/s/serverless-contact-form-example-x0neb | |
* | |
* (c) Max Kostinevich / https://maxkostinevich.com | |
*/ | |
--> | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> | |
<title>Serverless Contact Form Example</title> | |
<link | |
rel="stylesheet" | |
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" | |
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" | |
crossorigin="anonymous" | |
/> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="row justify-content-center"> | |
<div class="col-12 col-md-4"> | |
<h1 class="text-center">Serverless Contact Form Example</h1> | |
<p class="alert alert-primary"> | |
Email sending is disabled on this demo.<br /> | |
Learn more | |
<a | |
target="_blank" | |
href="https://maxkostinevich.com/blog/serverless-contact-form" | |
>here</a | |
>. | |
</p> | |
<form | |
action="https://contact-dev.frontier.workers.dev/" | |
method="post" | |
class="form-horizontal ajax-form" | |
> | |
<!-- Notifications --> | |
<p class="msg-container text-center"></p> | |
<div class="form-group"> | |
<label for="name">Name</label> | |
<input | |
type="text" | |
class="form-control" | |
name="name" | |
id="name" | |
placeholder="Your name" | |
/> | |
</div> | |
<div class="form-group"> | |
<label for="eml">Email</label> | |
<input | |
type="email" | |
class="form-control validate validate_userEmail" | |
name="eml" | |
id="eml" | |
placeholder="Your email" | |
/> | |
</div> | |
<div class="form-group"> | |
<label for="message">Message</label> | |
<textarea | |
rows="5" | |
class="form-control validate validate_msgText" | |
name="message" | |
id="message" | |
placeholder="Your message" | |
></textarea> | |
</div> | |
<div class="form-group"> | |
<button | |
type="submit" | |
class="btn btn-primary" | |
data-btn-label="Submit" | |
data-btn-label-processing="Processing.." | |
> | |
Submit | |
</button> | |
</div> | |
<input | |
type="text" | |
class="input-honeypot" | |
style="visibility:hidden;width:1px;height:1px;padding:0px;border:none;" | |
name="eml2" | |
value="" | |
/> | |
</form> | |
<hr /> | |
<p class="text-secondary text-center"> | |
Created by | |
<a href="https://maxkostinevich.com" target="_blank" | |
>Max Kostinevich</a | |
> | |
</p> | |
</div> | |
<!-- /End Contact Form Col --> | |
</div> | |
</div> | |
<script | |
src="https://code.jquery.com/jquery-3.4.1.min.js" | |
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" | |
crossorigin="anonymous" | |
></script> | |
<script | |
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" | |
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" | |
crossorigin="anonymous" | |
></script> | |
<script | |
src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" | |
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" | |
crossorigin="anonymous" | |
></script> | |
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> | |
<script> | |
$(document).on("submit", "form.ajax-form", function(e) { | |
e.preventDefault(); | |
var currentForm = $(this); // Get current form object | |
// disable submit button | |
$("[type=submit]", currentForm).attr("disabled", "disabled"); | |
// clean up the msg container | |
$(".msg-container", currentForm) | |
.html("") | |
.attr("class", "msg-container text-center") | |
.css("display", "hidden"); | |
// remove fields error classes | |
currentForm.find(".is-invalid").removeClass("is-invalid"); | |
// add preloader | |
$("[type=submit]", currentForm).html( | |
$("[type=submit]", currentForm).data("btn-label-processing") | |
); | |
let formData = $(this) | |
.serializeArray() | |
.map( | |
function(x) { | |
this[x.name] = x.value; | |
return this; | |
}.bind({}) | |
)[0]; | |
axios({ | |
method: $(this).attr("method"), | |
url: $(this).attr("action"), | |
data: formData | |
}) | |
.then(function(response) { | |
var hand = setTimeout(function() { | |
// clear the form if form submitted successfully | |
$(currentForm).trigger("reset"); | |
// show returned message | |
$(".msg-container", currentForm) | |
.addClass("alert alert-success") | |
.html(response.data["message"]) | |
.css("display", "none"); | |
// enable submit button again | |
var btnLabel = $("[type=submit]", currentForm).data("btn-label"); | |
$("[type=submit]", currentForm).removeAttr("disabled"); | |
$("[type=submit]", currentForm).html(btnLabel); | |
clearTimeout(hand); | |
}, 1000); | |
}) | |
.catch(function(error) { | |
// show returned message | |
$(".msg-container", currentForm) | |
.addClass("alert alert-danger") | |
.html(error.response.data["message"]) | |
.css("display", "block"); | |
// enable submit button again | |
var btnLabel = $("[type=submit]", currentForm).data("btn-label"); | |
$("[type=submit]", currentForm).removeAttr("disabled"); | |
$("[type=submit]", currentForm).html(btnLabel); | |
console.log(error); | |
}); | |
return false; | |
}); | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Serverless contact form handler for Cloudflare Workers. | |
* Emails are sent via Mailgun. | |
* | |
* Learn more at https://maxkostinevich.com/blog/serverless-contact-form | |
* Live demo: https://codesandbox.io/s/serverless-contact-form-example-x0neb | |
* | |
* (c) Max Kostinevich / https://maxkostinevich.com | |
*/ | |
// Script configuration | |
const config = { | |
mailgun_key: "YOUR_MAILGUN_API_KEY", | |
mailgun_domain: "YOUR_MAILGUN_DOMAIN", | |
from: "no-reply <no-reply@YOUR_DOMAIN>", | |
admin_email: "xxxxx@YOUR_DOMAIN", | |
email_field: "eml", // email field name | |
form_fields: ["name", "message"], // list of required fields | |
honeypot_field: "eml2" // honeypot field name | |
}; | |
// -------- | |
// utility function to convert object to url string | |
const urlfy = obj => | |
Object.keys(obj) | |
.map(k => encodeURIComponent(k) + "=" + encodeURIComponent(obj[k])) | |
.join("&"); | |
// Helper function to return JSON response | |
const JSONResponse = (message, status = 200) => { | |
let headers = { | |
headers: { | |
"content-type": "application/json;charset=UTF-8", | |
"Access-Control-Allow-Origin": "*", | |
"Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS", | |
"Access-Control-Allow-Headers": "Content-Type" | |
}, | |
status: status | |
}; | |
let response = { | |
message: message | |
}; | |
return new Response(JSON.stringify(response), headers); | |
}; | |
addEventListener("fetch", event => { | |
const request = event.request; | |
if (request.method === "OPTIONS") { | |
event.respondWith(handleOptions(request)); | |
} else { | |
event.respondWith(handle(request)); | |
} | |
}); | |
const corsHeaders = { | |
"Access-Control-Allow-Origin": "*", | |
"Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS", | |
"Access-Control-Allow-Headers": "Content-Type" | |
}; | |
function handleOptions(request) { | |
if ( | |
request.headers.get("Origin") !== null && | |
request.headers.get("Access-Control-Request-Method") !== null && | |
request.headers.get("Access-Control-Request-Headers") !== null | |
) { | |
// Handle CORS pre-flight request. | |
return new Response(null, { | |
headers: corsHeaders | |
}); | |
} else { | |
// Handle standard OPTIONS request. | |
return new Response(null, { | |
headers: { | |
Allow: "GET, HEAD, POST, OPTIONS" | |
} | |
}); | |
} | |
} | |
async function handle(request) { | |
try { | |
const form = await request.json(); | |
// Honeypot / anti-spam check | |
// Honeypot field should be hidden on the frontend (via css), | |
// and always have an empty value. If value is not empty, then (most likely) the form has been filled-in by spam-bot | |
if (form[config.honeypot_field] !== "") { | |
return JSONResponse("Invalid request", 400); | |
} | |
// Validate form inputs | |
for (let i = 0; i < config.form_fields.length; i++) { | |
let field = config.form_fields[i]; | |
if (form[field] === "") { | |
return JSONResponse(`${field} is required`, 400); | |
} | |
} | |
// Validate email field | |
let email_regex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; | |
if ( | |
form[config.email_field] == "" || | |
!email_regex.test(form[config.email_field]) | |
) { | |
return JSONResponse("Please, enter valid email address", 400); | |
} | |
// assign email address to the form | |
form["email"] = form[config.email_field]; | |
const admin_template = ` | |
<html> | |
<head> | |
<title>New message from ${form.name}</title> | |
</head> | |
<body> | |
New message has been sent via website.<br><br> | |
<b>Name:</b> ${form.name} <br> | |
<b>Email:</b> ${form.email} <br> | |
<br> | |
<b>Message:</b><br> | |
${form.message.replace(/(?:\r\n|\r|\n)/g, "<br>")} | |
</body> | |
</html> | |
`; | |
const user_template = ` | |
Hello ${form.name}, | |
Thank you for contacting me! | |
I have received your message and I will get back to you as soon as possible. | |
`; | |
let admin_data = { | |
from: config.from, | |
to: config.admin_email, | |
subject: `New message from ${form.name}`, | |
html: admin_template, | |
"h:Reply-To": form.email // reply to user | |
}; | |
let admin_options = { | |
method: "POST", | |
headers: { | |
Authorization: "Basic " + btoa("api:" + config.mailgun_key), | |
"Content-Type": "application/x-www-form-urlencoded", | |
"Content-Length": admin_data.length | |
}, | |
body: urlfy(admin_data) | |
}; | |
let user_data = { | |
from: config.from, | |
to: form.email, | |
subject: "Thank you for contacting me!", | |
html: user_template, | |
"h:Reply-To": config.admin_email // reply to admin | |
}; | |
let user_options = { | |
method: "POST", | |
headers: { | |
Authorization: "Basic " + btoa("api:" + config.mailgun_key), | |
"Content-Type": "application/x-www-form-urlencoded", | |
"Content-Length": user_data.length | |
}, | |
body: urlfy(user_data) | |
}; | |
try { | |
/* | |
let results = await Promise.all([ | |
fetch(`https://api.mailgun.net/v3/${config.mailgun_domain}/messages`, admin_options), | |
fetch(`https://api.mailgun.net/v3/${config.mailgun_domain}/messages`, user_options) | |
]); | |
console.log('Got results'); | |
console.log(results); | |
*/ | |
return JSONResponse("Message has been sent"); | |
} catch (err) { | |
console.log("Error"); | |
console.log(err); | |
return JSONResponse("Oops! Something went wrong.", 400); | |
} | |
} catch (err) { | |
return new Response(""); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment