31 May 2023

Cloudflare Email Routing

A colleague recently told me about Cloudflare's Email Routing service. This is a free service which can handle scenarios ranging from basic forwarding of emails from A to B right up to more complicated logic flows that utilise workers. This post is a bit of an overview based on my recent experience playing with the service.

I keep most of my domains at Cloudflare. For email handling I typically use a pro Gmail account (Google Workspace? I loose track of the names). This has some limitations, at least with what you can achieve without extra cost. It is pretty much just adding new alias domains for all existing mailboxes.

Cloudflare Email Routing has been around for a little while (open beta in Feb 2022 and general release in October 2022). It is a service that provides programmatic control for handling of emails. I’ve been interested to try this for the project sites I create, e.g. voiptoolbox.net. Sites look that do not need full email accounts, but some forwarders might be useful.

Basic Mail Forwarding

Skipping project domains for now, I have got a family member who has an outlook.com address that is rejected for sign up on ChatGPT (either plan sign up or ‘sign in with Microsoft’), no idea why but that feels solvable with email forwarding. I’ve also got an unused domain, teamscalldata.com, that is just sitting idle on Cloudflare from an old project.

When it comes to enabling Email Routing on the domain the web interface makes the setup extremely easy. There is a literal “turn on email routing” button. After this I was prompted with the required DNS record changes. Another button press later and Cloudflare proceeds to add/modify the domains MX and SPF records.

As an aside, I appreciate the prompt rather than having the record change happen behind the scenes. I’ve always been wary of domain based services where things happen ‘transparently’.

The MX record set Cloudflare add was a little surprising:

MX 52 route1.mx.cloudflare.net
MX 98 route2.mx.cloudflare.net
MX 91 route3.mx.cloudflare.net

Those numbers are the MX priorities, which all seem a little unusual. I tried to Google around this and based on the post here the suggestion is that they are just random with no purpose, although in the future may act as a level of DNS verification.

Once the domain was setup I completed the form to send emails from [email protected] to [email protected]. Cloudflare send a verification email to B’s address, after which email delivery should be ready.

Cloudflare Simple Email Forwarding

Super easy setup for basic email forwarding...

With that complete I tried a test email and sure enough it came through quickly and cleanly. By cleanly I mean passing the usual SDP/DKIM/DMARC checks. I headed over to the ChatGPT sign up and this time had a successful sign up.

Email Filtering

Encouraged by the basic email forwarding success I turned my attention to a slightly more involved email processing problem. I get a bunch of WordPress alert emails from different sites, which are of mixed value. An example email is one that tells me a user has changed a password. I only really care about this if it’s an admin user I own. My intended Cloudflare fix is:

  1. Update WordPress to send these emails to a new processing Cloudflare email address.
  2. If the subject is Password Changed, check the contents of the message for the usernames I care about.
  3. If it matches, forward it on, otherwise drop it.

For this we’ll use an email worker, which is a normal Cloudflare worker but one that’s triggered from email routing rather than a HTTP request or timer etc.

This part of the service is currently in beta. The setup is not as slick as basic forwarding and I definitely burnt more time here than I first expected. Hence this list of tips…

Tip 1 - How do I get logs?

There is an online email worker editor which allows you to test run the code but only set the From, To and Subject fields (no message body). That meant it wasn’t suitable for my scenario. I instead carried out some live testing by deploying the worker with plenty of console.log lines, then using the live log stream feature, which worked really well.

I did get caught out with non-trivial logging. For example, you can’t use console.log like you might be used to with a browser’s developer tools. Running console.log(message) won’t give you anything useful. I did try JSON.stringify(message) first, but it wouldn’t auto calculate the string fields for the message object, so I had to specify those, e.g. console.log(JSON.stringify(message, ['from', 'to', other-field-you-want])).

Cloudflare Logs

Live logs straight from the worker...

Tip 2 - message.headers.get("from") is more than an email address

Cloudflare Examples

Examples that may or may not work for you...

When you go to add a worker the dashboard will provide a few starting examples (see screenshot). I copied the basic “Allow List” example then got confused as to why my emails were getting dropped every time. It turned out that my example message had a From header that included a display name, e.g. "Foo Bar" <[email protected]>, which I expect is pretty common.

There are shortcuts available in message.from and message.to respectively which are the envelope values. These are just addresses and probably what you want, e.g. [email protected]. Full EmailMessage definition is available in the docs.

Tip 3 - message.forward() result can be empty

You’ve got three actions you can take in the worker:

  1. Forward the email using message.forward()
  2. Reject the email using message.setReject(), which will give the sending mail server a rejection.
  3. Drop the email. No method for this, just don’t call either of the other response functions.

forward() and setReject() are both asynchronous, so you’ll want to call await before hand to make sure your worker doesn’t finish and exit before it has carried out the action.

The extra gotcha here is that in the sandbox both of these return an object which has keys for success and optionally a rejectionReason, however the production worker seems to only give a null value back

Tip 4 - Avoid a worker-level try/catch

The log display works well if your worker crashes, so avoid wrapping your code in a big try/catch, instead let any exceptions bubble up. The Cloudflare logs will then clearly show the exception and the email routing will bail out with an error rather than just silently dropping the email.

Note that trying to send to an unverified address will cause message.forward() to throw an exception as well.

Tip 5 - Test with distinct email accounts (avoid Gmail aliases)

Gmail, likely other providers as well, will avoid delivering an email that was sent from the same mailbox. That means if you try send from name1 in Gmail and expect to receive also in name1’s mailbox just via alias1 it’s not going to work. You can’t do this:

[email protected] -> foo@cloudflare-worker -> [email protected]

The email won’t get rejected, it will appear to proceed correctly via Cloudflare (showing up in the logs along the way), it just won’t actually show up in your Gmail inbox.

Tip 6 - Checking the contents of the email

Checking the body of the email is a little involved as the message object passed to your worker doesn’t directly contain the full content, instead you have a access to a Stream object via message.raw (and message.rawSize to know how much you can read).

There is a really helpful example on Github that I think I found through a StackOverflow post. I ended up using a similar approach.

Note that message.raw is the complete message. That means it is not just the body, but all the headers as well. Depending on how you want to process the message that might cause issues, something to bear in mind.

Worker Summary

Workers definitely provide the ultimate email flexibility here and at 100k requests per day for free it’s likely to be as good as unlimited for most.

The experience is still a little buggy. The browser email worker IDE reload is flakey. The processed email graph sometimes gives incorrect timestamps. Some ‘edit’ buttons take you to the normal worker browser IDE instead of the email worker version, which is confusing as you’ll get errors about missing methods. It is however free and in beta, so all this can be forgiven.


Feature Wishlist

I’d be keen to try out Email Routing on domains that I am already using to send/receive email, which suggests two wishlist items for future that I’d love to see implemented:

Subdomains Support

Subdomains wouldn’t interfere with existing MX records at the top level. e.g. I could create mailproc.falkus.co for programmatic email features. Alas, it turns out Cloudflare don’t support Email Routing on subdomains unless you have an Enterprise plan, which I don’t. Enterprise is the plan that doesn’t have advertised pricing, which also means I can’t afford to try it out.

Transparent Filtering

It would be awesome if a dual delivery option was introduced, i.e. mail can go through Cloudflare’s Email Routing then head on to the normal destination. It would be a way to transparently add worker/webhook capability etc to any existing email service.

I wondered if this could work just with a big set of MX records. For example, MX records 1-3 point at Cloudflare, 4+ point at your original mail provider. Unless Cloudflare had a total outage, all your emails go to the Email Routing service then messages can be forwarded on via the other higher priority (and original) MX records.

I’d be curious to know if there’s a reason this is a bad idea. One guess is that perhaps a lot of email servers don’t respect the MX priorities, so you’d end up with an unreliable delivery method where Email Routing is sometimes used.

Google Workspace has dual delivery support which is along similar lines, although has the burden of configuration in Gmail (which is harder/more expensive) and doesn’t rely on the sending mail servers to obey MX priorities.

A Useful Service For The Toolbox

The Email Routing service is great. It almost feels like a Let’s Encrypt Moment™. That is a moment that perhaps opens up new business or services that weren’t previously viable. For LE the introduction of free, easy to generate and widely trusted SSL certificates allowed a bunch of businesses to exist that otherwise would not work (due to the finance/time cost etc). Free Email Routing strikes me as possibly having a similar utility, especially if subdomains and dual delivery are ever introduced.

What’s in it for Cloudflare? I guess it’s all about the up sell. To do the more interesting webhook stuff you need workers and there is a limit on how much you can do within a free tier (although it is pretty generous). I expect the strategy is similar for their at-cost domain registration. No complaints though, for a free service it’s ace and I’m definitely going to consider wider adoption for future projects.

Cloudflare Dev SysAdmin
Back to posts