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.
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:
- Update WordPress to send these emails to a new processing Cloudflare email address.
- If the subject is
Password Changed
, check the contents of the message for the usernames I care about. - 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]))
.
Tip 2 - message.headers.get("from") is more than an email address
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:
- Forward the email using
message.forward()
- Reject the email using
message.setReject()
, which will give the sending mail server a rejection. - 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.