Skip to content

Instantly share code, notes, and snippets.

@muiton
Forked from maxkostinevich/index.html
Created February 28, 2020 18:03
Show Gist options
  • Save muiton/2cb210d4c7ba74a39f471cb4b9a88585 to your computer and use it in GitHub Desktop.
Save muiton/2cb210d4c7ba74a39f471cb4b9a88585 to your computer and use it in GitHub Desktop.

Revisions

  1. @maxkostinevich maxkostinevich revised this gist Jan 1, 2020. 1 changed file with 11 additions and 0 deletions.
    11 changes: 11 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,14 @@
    <!--
    /*
    * 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>
  2. @maxkostinevich maxkostinevich revised this gist Jan 1, 2020. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions worker.js
    Original file line number Diff line number Diff line change
    @@ -3,6 +3,7 @@
    * 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
    */
  3. @maxkostinevich maxkostinevich created this gist Jan 1, 2020.
    185 changes: 185 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,185 @@
    <!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>
    195 changes: 195 additions & 0 deletions worker.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,195 @@
    /*
    * Serverless contact form handler for Cloudflare Workers.
    * Emails are sent via Mailgun.
    *
    * Learn more at https://maxkostinevich.com/blog/serverless-contact-form
    *
    * (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("");
    }
    }