My Email Worker Catchall: Backup Routing, Plus Addressing, and Subdomain Addressing

I thought I would share my Email Worker code that I use for all my routing. I have my catch-all point to this worker.

  • No lost Cloudflare to Gmail emails - they all get routed seamlessly, by automatically routing through improvMX when CF throws an error

  • Allows for plus (sub) addressing

  • Allows for subdomain addressing

Essentially I am using Try - Catch statements to route as follows:

  • Try routing directly to Gmail, if that fails…

  • Try routing ImprovMX to Gmail, if that fails…

  • Finally try routing to Outlook. So far outlook as not returned an error, but has sent emails to Junk Mail (all were actual spam).

improvMX is free and was super easy to setup on my subdomain (imx.mydomain.net). As my main backup, it still routes to my Gmail account. Nearly all of the Cloudflare → Gmail emails that are rejected are accepted via improvMX.

[email protected][email protected]

Special thanks to @DarkDeviL, @user-1, for their helpfull tips! This code could probably be optimized further. Hopefully this can be helpfull to others.

export default {
  async email(message, env, ctx) {

  //alias lists using RegEx. Examples below will allow for plus-addressing and subdomain addressing.
  // (I have configured wildcard (*) DNS records to catch all sub-domains). 
    const user1_alias = [
      "user1+.*@mydomain.net",
      ".*@user1.mydomain.net",      
      ];

    const user2_alias = [
      "user2+.*@mydomain.net",
      ".*@user2.mydomain.net",      
      ];

  //primary emails
    const user1_primary = '[email protected]';  
    const user2_primary = '[email protected]';  
    const user3_primary = '[email protected]';  

  //backup emails (still going to primary email, but via improvmx instead of directly from Cloudflare).
  //you will need to create a subdomain (ex. 'imx') setup using improvmx, and improvmx aliases pointed to primary email
    const user1_backup = '[email protected]';  
    const user2_backup = '[email protected]';   
    const user3_backup = '[email protected]';  
  
  //final catchall if primary and backup fails
    const user3_outlook = '[email protected]';  

  //This tests alias against message "To:", showing the value of [true] if matching.
    const is_user1_alias = RegExp(user1_alias.join("|"), "i").test(message.to);
    const is_user2_alias = RegExp(user2_alias.join("|"), "i").test(message.to);
  
  //action to take 
    //inside Try is to route to backup (ImprovMX to Gmail) to get by Cloudflare to Gmail issues.
    //outside Try is to route to Outlook (final catchall) if both primary and backup fails.
    var gmail_error;
    try {
      switch (true){
        case (is_user1_alias):
          try {await message.forward(user1_primary);}
          catch(gmail_error) {await message.forward(user1_backup);}
          break;
        case (is_user2_alias):
          try {await message.forward(user2_primary);}
          catch(gmail_error) {await message.forward(user2_backup);}
          break;
        default:
          try {await message.forward(user3_primary);}
          catch(gmail_error) {await message.forward(user3_backup);}
          break;
      }
     }
    catch(improvmx_error){
      try {await message.forward(user3_outlook);}
      catch(outlook_error) {
        message.setReject(outlook_error.message);
        return message.setReject(outlook_error.message + '\n' + improvmx_error.message + '\n' + gmail_error.message);
        }
    }
  }
}
3 Likes

I exactly thought about such a solution but was not sure how to implement that with a worker.

A few questions about that:

  1. how is the handling done? Is the worker executed instead of the email routing?

  2. what about the cloudflare Mail activity log? Is it not used anymore if I use a worker?

  3. the logging you are doing in the work: where can you access it?

Best

Set your catchall to send to worker instead of an email. Any email address defined before catchall would not be processed by this code.

Any email successfully processed by the worker will show"dropped". It will return any error remaining after after worker is done processing. So it would have to be rejected by all backups to return an error.

As the activity log will show"dropped" unless email is rejected by all backups, i monitor the improvmx logs as well.

I will create a setup later which will try to send to gmail and on error put the main to another mailbox and let gmail get them by pop3. So I have the same features Gmailify provided but I won’t have to pay for it.

1 Like

ImprovMX (my backup now, set up as a subdomain imx.mydomain.net) has has been super dependable for me, and easy to manage and setup. I had been using it exclusively before, but there is 10mb email size limit instead is the 25mb. Emails are routed directly to Gmail instead of having to pop/redirect from another email provider.

[email protected][email protected]

okay I migrated to your setup and it seems working!

important: keep an eye on your worker requests per day. you have 100.000 requests that can be made to workers per day. if you are using other workers within your free account that could lead to a problem. so just keep an eye on it.

my setup now looks like attached. I am pretty sure that we can make that even shorter with an object and remove duplicate code. but that needs a refactoring within the next couple days. and my solution is not forwarding to another forwarder. it tries to send to gmail and if it fails it sends to another mail server and gmail fetches that mail via pop3.

export default {
  async email(message, env, ctx) {

    const user1_alias = [
      "[email protected]",
      "[email protected]",      
      ];

    const user2_alias = [
      "[email protected]",
      ];

    const user3_alias = [
      "[email protected]",
      "[email protected]",         
      ];

    const user4_alias = [
      "[email protected]",
      ];

    // primary emails
    const user0_primary = '[email protected]'
    const user1_primary = '[email protected]';  
    const user2_primary = '[email protected]';  
    const user3_primary = '[email protected]';
    const user4_primary = '[email protected]';

    // backup emails
    const user0_backup = '[email protected]'; 
    const user1_backup = '[email protected]';
    const user2_backup = '[email protected]';
    const user3_backup = '[email protected]';
    const user4_backup = '[email protected]';
  
    // this tests alias against message "To:", showing the value of [true] if matching.
    const is_user1_alias = RegExp(user1_alias.join("|"), "i").test(message.to);
    const is_user2_alias = RegExp(user2_alias.join("|"), "i").test(message.to);
    const is_user3_alias = RegExp(user3_alias.join("|"), "i").test(message.to);
    const is_user4_alias = RegExp(user4_alias.join("|"), "i").test(message.to);
  
    // action to take 
    var gmail_error;
    try {
      switch (true){
        case (is_user1_alias):
          try {
            await message.forward(user1_primary);
          }
          catch(gmail_error) {
            await message.forward(user1_backup);
          }
          break;
        case (is_user2_alias):
          try {
            await message.forward(user2_primary);
          }
          catch(gmail_error) {
            await message.forward(user2_backup);
          }
          break;
        case (is_user3_alias):
          try {
            await message.forward(user3_primary);
          }
          catch(gmail_error) {
            await message.forward(user3_backup);
          }
          break;
        case (is_user4_alias):
          try {
            await message.forward(user4_primary);
          }
          catch(gmail_error) {
            await message.forward(user4_backup);
          }
          break;
        default:
          try {
            await message.forward(user0_primary);
          }
          catch(gmail_error) {
            await message.forward(user0_backup);
          }
          break;
      }
    }
    catch(gmx_error){
      message.setReject(gmx_error.message);
      return message.setReject(gmx_error.message + '\n' + gmail_error.message);
    }
  }
}
2 Likes

Nice! I would be curious to see how you might refactor. I am just using this for my personal/family email so would think I would be safely within the 100K request limit, unless I am missing something. :smile:

this is my refactored variant (please keep in mind: I never wrote javascript before, this can be done way better for sure!):

export default {
    async email(message, env, ctx) {

        // defining all alias to user mappings we have
        const users = [{
            alias: [
                "[email protected]",
                "[email protected]",
            ],
            primary: "[email protected]",
            backup: "[email protected]",
        }, {
            alias: [
                "[email protected]",
            ],
            primary: "[email protected]",
            backup: "[email protected]",
        }, {
            alias: [
                "[email protected]",
                "[email protected]",
            ],
            primary: "[email protected]",
            backup: "[email protected]",
        }, {
            alias: [
                "[email protected]",
            ],
            primary: "[email protected]",
            backup: "[email protected]",
        }, ]

        // setting the target_user as our catch all user
        let target_user = {
            primary: "[email protected]",
            backup: "[email protected]",
        }

        // checking if we have a user matching and overwrite the target_user with it
        users.forEach(user => {
            if (RegExp(user.alias.join("|"), "i").test(message.to)) {
                target_user = user;
            }
        });

        // sending mail out, first to google and on error to gmx
        let gmail_error;
        try {
            try {
                await message.forward(target_user.primary);
            } catch (gmail_error) {
                await message.forward(target_user.backup);
            }
        } catch (gmx_error) {
            message.setReject(gmx_error.message);
            return message.setReject(gmx_error.message + "\n" + gmail_error.message);
        }
    }
}
2 Likes

Would it be possible to code a worker to pick which outgoing IP address from the range used by Cloudflare to use to forward emails?

Can you explain, why you reject the same message twice?

The first one is rejecting the message and the second ones is returning the error message to the Activity Log.

I guess its not needed. It should be enough to just do:
message.setReject(gmx_error.message + "\n" + gmail_error.message);
without the return (as it is the last call in the code) and I also guess that the setReject method is returning void and so the return is not giving anything back to the caller.

would be great if you can confirm that @user15545

edit 1: At least my assumption with void seems to be true: Cloudflare email worker: do not forward for given domain extensions - #3 by Chaika

edit 2: based on void and some more tests I did I refactored it to this:

        let reject_reason;
        try {
            await message.forward(target_user.primary);
        } catch (gmail_error) {
            reject_reason = gmail_error.message;
            try {
                await message.forward(target_user.backup);
            } catch (gmx_error) {
                reject_reason = gmx_error.message + "\n" + reject_reason;
                message.setReject(reject_reason);
            }
        }

so my final version looks like this:

Final Version
export default {
    async email(message, env, ctx) {

        // defining all alias to user mappings we have
        const users = [{
            alias: [
                "[email protected]",
                "[email protected]",
            ],
            primary: "[email protected]",
            backup: "[email protected]",
        }, {
            alias: [
                "[email protected]",
            ],
            primary: "[email protected]",
            backup: "[email protected]",
        }, {
            alias: [
                "[email protected]",
                "[email protected]",
            ],
            primary: "[email protected]",
            backup: "[email protected]",
        }, {
            alias: [
                "[email protected]",
            ],
            primary: "[email protected]",
            backup: "[email protected]",
        }, ]

        // setting the target_user as our catch all user
        let target_user = {
            primary: "[email protected]",
            backup: "[email protected]",
        }

        // checking if we have a user matching and overwrite the target_user with it
        users.forEach(user => {
            if (RegExp(user.alias.join("|"), "i").test(message.to)) {
                target_user = user;
            }
        });

        // sending mail out, first to google and on error to gmx
        let reject_reason;
        try {
            await message.forward(target_user.primary);
        } catch (gmail_error) {
            reject_reason = gmail_error.message;
            try {
                await message.forward(target_user.backup);
            } catch (gmx_error) {
                reject_reason = gmx_error.message + "\n" + reject_reason;
                message.setReject(reject_reason);
            }
        }
    }
}
2 Likes

I know it may be just few days, but how its working for you in regards to % of emails landed as originally assumed to emails redirected to backup?

Would say less than 5% is sent to backup instead of direct delivery.

1 Like

I get this error when trying to add the improvMX MX records (imx.mydomain.com) to the DNS in Cloudflare:

“This zone is managed by Email Routing. Disable Email Routing to add/modify MX records. (Code: 890190)”

Is there a way to add the MX records without disabling Email Routing?

I sounds like you may have added that subdomain in Email Routing → Settings → Subdmains. You want to use a subdomain that is not managed by Email Routing.

2 Likes

One more question,

  1. I assume that this worker we need to specify in Email > Email Routing > Email Workers
    then
    1.1. In email worker we need to specify all email addresses in domain and destinations need to be verified (added in Destination addresses).
    1.2. its one worker that is included in all domains used with Email Redirect on Cloudflare
  2. Are routing rules still need to match with with worker specification, or worker is used only, and routing rules do not apply, so can be removed?

@jhedfors could you elaborate on Any email address defined before catchall would not be processed by this code.

Is tat mean, I can have specified routes in Routing Rules and only when this doesn’t match, than catch-all kicking in. This refers back to my question from point 2.

Thank you in advance. I am trying to write a blog post about it and incorporate all things we all learned including using workers.

In other words, as this Email Worker is used as the Catch-All destination, any Custom Addresses will not use the Catch-All (even if they fail). All custom addresses will be put in this Catch-All Email Worker. All Destination Addresses will need to be verified.

1 Like

I’ve set the Catch-All to Send to worker according to the screenshot above.

Do I also need to set all the Custom addresses as inactive?

Yes. Otherwise they are directly routed through cloudflare and not through your worker. Set the catch all to the worker and all individual to inactive.

1 Like