This is going to be a short blog post detailing another migration I undertook to simplify my current stack.

This time, I replaced SES with Cloudflare Email Routing for incoming emails across my domains.


Before: AWS SES

Since my original migration to AWS in ~2018, I’ve used SES to handle incoming and outbound emails for my domains. (Those interested can read more at My Blogging Stack and Building a Serverless Mailing List in AWS ).

SES allows you to configure rule sets for receiving emails, each of which can then contain one or more receipt rules, as shown below.

SES Rule Sets
SES Rule Sets
SES Receipt Rules
SES Receipt Rules

In my case, I only needed one rule, composed of two actions:

  1. First, each new incoming email gets delivered (as a text file) in a dedicated S3 bucket.
  2. Second, the event of creating a new file in the bucket triggered, in turn, a Lambda function which takes care of parsing the file and forwarding it as a “proper” email to a catch-all address I specified.
SES Rule Set Actions
SES Rule Set Actions
Processing Workflow
Processing Workflow

As you might have assumed by now, the Lambda function (taken from the (AWS Lambda SES Email Forwarder) repo) contains the core logic of the processing workflow. It is a Node.js script that uses SES’s inbound/outbound capabilities to run a “serverless” email forwarding service.

Although this solution worked fine for nearly five years, it had some management overhead. For example:

  • The need to monitor the Lambda function to ensure it didn’t silently fail/drop messages.
  • Whenever Lambda did fail, it was a matter of manually logging in to the console to check the logs, find the related message in S3, download it, and only then read the content.

I wanted to get rid of this overhead.

After: Cloudflare Email Routing

Cloudflare announced Email Routing in September 2021, as a more straightforward way to manage email delivery. You can also read “Migrating to Cloudflare Email Routing” for instructions on migrating.

Here is my current setup for inbound emails (obviously in Terraform 😅):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
locals {
    domain = "example.com"

    email_gmail = "<redacted>"
    email_custom = {
        info  = "info@${local.domain}"
        marco = "marco@${local.domain}"
    }
}

#
# Global Config
#
resource "cloudflare_email_routing_settings" "example" {
  zone_id = cloudflare_zone.example.id
  enabled = "true"
}

#
# Addresses
#
resource "cloudflare_email_routing_rule" "example" {
  zone_id = cloudflare_zone.example.id

  for_each = local.email_custom

  name    = each.value
  enabled = true

  matcher {
    type  = "literal"
    field = "to"
    value = each.value
  }

  action {
    type  = "forward"
    value = [local.email_gmail]
  }
}

#
# Catch all
#
resource "cloudflare_email_routing_catch_all" "example" {
  zone_id = cloudflare_zone.example.id
  name    = "${local.domain} catch all"
  enabled = true

  matcher {
    type = "all"
  }

  action {
    type  = "forward"
    value = [local.email_gmail]
  }
}
  • Lines 14-17: cloudflare_email_routing_settings is the primary resource for managing Email Routing settings. Here we are linking it to the relevant Cloudflare Zone and setting it as enabled.
  • Lines 22-40: we iterate over the set of alias we want (defined in lines 5-8), to create an Email Routing rule for each of them. As per the action, it is set to forward emails to a catch-all address in Gmail.
  • Lines 45-58: we define a catch-all rule to handle email received to any handle not covered by its own Email Routing rule.

And here is what it looks like in the Cloudflare dashboard:

Email Routing Dashboard
Email Routing Dashboard
Email Routing Rules
Email Routing Rules

As you can see, with this setup, there are significantly fewer moving parts on my side of the shared responsibility model.

What about outgoing emails?

Note that this setup handles inbound only.

SES still handles outbound via an SMTP user.

Conclusions

I hope you found this post valuable and interesting, and I’m keen to get feedback on it! If you find the information shared helpful, if something is missing, or if you have ideas on improving it, please let me know on 🐣 Twitter or at 📢 feedback.marcolancini.it.

Thank you! 🙇‍♂️