Skip to content

Instantly share code, notes, and snippets.

@maxkostinevich
Last active November 1, 2024 08:17
Show Gist options
  • Save maxkostinevich/aad64bffee69f179f9b70c2f5f3cc8f1 to your computer and use it in GitHub Desktop.
Save maxkostinevich/aad64bffee69f179f9b70c2f5f3cc8f1 to your computer and use it in GitHub Desktop.
Cloudflare Worker - Handle Contact Form
<!--
/*
* 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>
/*
* 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