Read articles and posts about technology and hardware Follow my startup journey and entrepreneurship insights View photo gallery and visual content Download and view resume and professional experience Visit GitHub profile for code repositories Watch educational videos and tutorials Connect on LinkedIn professional network Get in touch for collaboration or inquiries

Cloudflare Email Workers: Spam Filter and Multicast Forwarder

Table of Contents

Contents

I am responsible for several domain names which are managed through Cloudflare. Unfortunately, they also receive a lot of spam because the email addresses are regularly published openly online and in print.

Since these sites have fairly well known sources of traffic, I’ve written a simple Email Worker script to filter incoming emails. Combined with Cloudflare’s existing spam detection, the filter blocks over 90% of incoming mail, all of which is spam, and let’s through just 3% of emails which should have been blocked.

This script has the additional benefit of enabling multicast addresses, so one email address forwards to multiple destinations (a kind of manual email group or mailing list, if you like).

The script is very simple, so here’s the code for you to re-use (“Unlicense”‘d).

export default {
  async email(message, env, ctx) {
    // Exact-match strings and regex matches
    // - A pretty crude filter but it works well enough for my purposes
    const blockList = [
      // Example: Block all emails coming from example.com, example.org, etc.
      /^.*@example\.[a-zA-Z0-9]+$/,
      // The heavy-weight filter - block all emails coming from any top-level
      // domain that _isn't_ `.com`, `.org`, `.net` or `.uk`.
      /^.*@.*\.(?!(com|org|net|uk))[a-zA-Z]+$/,
      // Block a specific domain
      /^.*@thisisjunk.com$/,
      // Block a specific email address
      "frodo@gmail.com"
    ];
    // Check the block list
    if (blockList.some((pattern) => typeof pattern == "string" ? pattern == message.from : pattern.test(message.from))) {
        // Email blocked
        //
        // Enabling Cloudflare's worker logs and filtering warning vs info messages
        // helps to measure how successful the filter is being.
      console.warn(`Rejecting email from "${message.from}" sent to "${message.to}" -- matches block list.`);
      message.setReject("Sender address is blocked");
      return;
    }

    // Block emails sent to "honeypot@mydomain.com" - useful to catch some
    // persistent spammers who try to hit any email address they can find.
    if (message.to == "honeypot@mydomain.com") {
      console.warn(`Rejecting email from "${message.from}" sent to "${message.to}" -- honeypot.`);
      message.setReject("You've hit the honey pot.");
      return;
    }

    // N.B. Only verified "Destination Addresses" can be used - add them in Cloudflare Email Routing dashboard
    const forwardingMap = {
      // Forward to one destination
      "personA@mydomain.com": "person-A-address@gmail.com",
      "personB@mydomain.com": "person-B-address@hotmail.com",
      // Forward to multiple destinations
      "everyone@mydomain.com": ["person-A-address@gmail.com", "person-B-address@hotmail.com"]
    };
    // Default forwarding address for emails sent to unrecognised addresses
    const catchAllForwardingAddress = "person-C@gmail.com";
    // Forwarding address if sending to a Gmail address fails (due to Gmail
    // blocking Cloudflare's IP addresses, as happens quite often)
    //
    // N.B. You don't need this if `catchAllForwardingAddress` doesn't land at
    // a Gmail inbox.
    const catchAllForwardingAddressNonGmail = "person-C@outlook.com";

    // Determine where to attempt to forward the email to
    const forwardTo = message.to in forwardingMap 
      ? forwardingMap[message.to] 
      : catchAllForwardingAddress;

    try {
        // Normal attempt to forward

      if (typeof forwardTo == "string") {
        // Single destination
        console.info(`Forwarding email from "${message.from}" sent to "${message.to}" on to: ${forwardTo}`);
        await message.forward(forwardTo);
      }
      else {
        // Multiple destinations
        console.info(`Forwarding email from "${message.from}" sent to "${message.to}" on to: ${forwardTo.join("; ")}`);
        // Await all forwards in parallel is more efficient and ensures an error
        // at one destination doesn't cause all destinations to fail.
        await Promise.all(forwardTo.map(async (address, index) => {
          try {
            console.info(`[${index+1}/${forwardTo.length}] Forwarding on to: ${address} ...`);
            await message.forward(address);
            console.info(`[${index+1}/${forwardTo.length}] Forwarding succeeded.`);
          }
          catch (e) {
            console.error(`[${index+1}/${forwardTo.length}] Forwarding failed: ${e}`);
            throw e;
          }
        }));
      }
    }
    catch (e) {
      const errorMessage = e.toString();

      // Gmail IP ban? Try to forward to the non-Gmail catch-all address
      if (errorMessage.includes("Gmail has detected an unusual rate of unsolicited mail")) {
        console.warn(`[Error handler] Detected Gmail IP address blocking. Forwarding email from "${message.from}" sent to "${message.to}" on to non-Gmail catch-all: ${catchAllForwardingAddressNonGmail}`);
        await message.forward(catchAllForwardingAddressNonGmail);
        return;
      }

      // Otherwise, log the error
      console.error(`[Error handler] Failed to forward message: ${e}`);

      // Blocked due to Cloudflare's spam filter? Stop here.
      if (errorMessage.includes("non-authenticated emails cannot be forwarded")) {
        message.setReject("Non-authenticated emails cannot be forwarded");
        return;
      }

      // Try to forward to the catch-all
      console.error(`[Error handler] Failed to forward message: ${e}`);
      console.info(`[Error handler] Forwarding email from "${message.from}" sent to "${message.to}" on to catch-all: ${catchAllForwardingAddress}`);
      await message.forward(catchAllForwardingAddress);
      throw e;
    }
  }
}

Go into CloudflareYour website/domainEmailEmail RoutingEmail Workers and configure a new worker with the above script (modify as appropriate for your needs). Then configure Destination addresses to have verified destinations (email workers can only forward to verified destinations). Lastly, configure Routing rules to direct addresses or the catch-all to your new email worker. Optionally, enable logging for the worker.