zeqk
2/3/2020 - 11:59 PM

Cloudflare Worker - Handle Contact Form

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/popper.js@1.16.0/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("");
  }
}