Small
Plates

Short stories and tech experiments.

Contact
Alex Lance
hello@alexlance.blog

About
Tech consultant and maker of Dibs On Stuff and TF State

Subscribe

RSS

All articles written by a person. All images drawn by a human hand. Alas, all JavaScript is from the machines.

Email for Maniacs
by Alex Lance
2025-11-11
1578 words
tech
I've gone through a number of different iterations on my email setup. It is now minimal, reliable and slightly unhinged.
item


For over a decade I ran Postfix on a Linux server for my email. It required a lot of setup initially, but it worked perfectly. Would I recommend that pathway to anyone else?

Well, no.

Technical efforts aside (which were substantial) there was a cognitive load in running my own server. What if it was hacked or attacked? What about spam reputation and message deliverability?

Also every account recovery mechanism on the planet is happy to send a password reset link to your email address. A hacked email server permits an attack on all your other accounts too.

So at the start of 2025 I decided to try something different. I moved the responsibility for sending and receiving email out of my little EC2 frying pan and into the AWS Simple Email Service (SES).

Have I traded one set of problems for another?

The new setup is more portable and feels like less of a technical burden than a single server sitting on the public internet, it certainly has less cognitive burden from a security perspective.

Email for maniacs? I do have a tendency to find solutions that are a little unrelatable. And so would I recommend this way of doing things to others?

Well, maybe.

I like it quite a lot. But unless you know the ins-and-outs of AWS, DNS and mail delivery, it is quite a lot. But feel free to check out the HOWTO below.

I will say, it's the best solution I have so far. Very secure, high level of control, "serverless" and aside from my time-spent, basically free to run.

Overview
When someone emails me:
When I send email to others:
How do I access my email when I'm working remotely or on my phone? The short answer is VPN (wireguard/tailscale) back home to where the mail is stored. I might leave that part of it for another time.

HOWTO
Ok this looks appalling. But most of it is just DNS setup and you can glean most of it from the AWS SES Identity creation section in the AWS web console. I'll walk you through it anyway using a domain name of mine that I never figured out what to do with: alouy.com.

  1. Get your domain name registration and DNS out of AWS Route53 Just my opinion, but this ensures that DNS names can always be redirected elsewhere in times of serious trouble with AWS. Eg, in the examples below I've screenshotted the Cloudflare DNS manager.
  2. Deactivate the SES limits sandbox This normally takes a support ticket conversation with AWS (and I suspect the process is designed to dissuade you) but basically you want to ensure that for the AWS region you care about, your AWS SES service is not still in the email limits sandbox.
  3. Create a new AWS SES Identity for your domain name
    • In your AWS SES region, click the Create Identity Button
    • Identity Type: Domain
    • Enter your domain name (eg for me: alouy.com)
    • Select the checkbox for "Use a custom MAIL FROM domain"
    • Make up a sub-domain for the MAIL FROM, eg in my case: mail.alouy.com
    • Publish DNS records to Route53? As mentioned above I'd suggest no, it does mean you've got to do all your DNS records manually, but thankfully AWS displays the exact records you need on the SES Identity create page.
  4. Verify your Domain with DKIM
    • DKIM is a system which basically makes your email look less like spam to others and helps provide assurance that your email message hasn't been tampered with in-transit.
    • Expand: Advanced DKIM Settings
    • If you choose the option to provide the DKIM authentication key yourself (BYODKIM) you will need to paste a private key that you generate. The command to generate a key is: opendkim-genkey -b 2048 -h sha256 -r -s 2025 -d alouy.com -v (swap your own domain in). This will generate two files: 2025.private is the one you paste into the AWS Private Key box.

      2025.private

      MIIEvgIBADANBS<snip>cl5OnXMurv1FmmqT3X8T+30o5ztb9kwem==
      Note:
      • Remove the header and footer that say:
        -----BEGIN/END PRIVATE KEY-----
      • Remove all the newlines from the key. I.e. it should be one long line in the text box (tread carefully on this one!)
      • Ensure the key ends in two equals signs
    • As a selector name, enter the "-s" argument that you specified in the opendkim-genkey command. Note this could have been anything, but I chose to use the year the key was created.
    • Now we need to publish some DNS records to activate our DKIM configuration

      You can see that AWS wants us to publish a TXT flavoured DNS record, where the domain name is set to: 2025._domainkey.alouy.com and the value of that is contained in the 2025.txt file that was generated from the opendkim-genkey command above. In my case the contents of that file look like:

      2025.txt

      2025._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; s=email; " "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudyMjt0JBPS7A9Gq+/3J8x8Q3udK4CqHjcnbZJDXhurhqO+gWdpf3feHcyXaW7rVKjpkeA+G5CAUPQP8Uz0L/EaPZVNWp6bTMoQtVIe665uZSdIcnYlBcXp0eIjwO1dnLX4w5/zhMYg9Fvv2Hu+FjqSDgwdcSZziMOntU2hm9ICD5PTF2U3ai5/cyyU7l/frBnJS5HZnNmA2Bg" "abFrphkNe6WzX7HlcPcXOfHyaP+rxdKJtXwjV0sPZb6JP1drhgziMvi+sy0VrztOJYKBeZEXYykTb/QyoqAa7KDTfRN1TraOCDIO9gn1zQ0By5jmPkjODr8up1PkFrjK8Qhzl3cwIDAQAB" ) ; ----- DKIM key 2025 for alouy.com
      But we actually only want a bit of that, and in this format:

      2025.txt (transformed)

      "v=DKIM1; h=sha256; k=rsa; s=email; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudyMjt0JBPS7A9Gq+/3J8x8Q3udK4CqHjcnbZJDXhurhqO+gWdpf3feHcyXaW7rVKjpkeA+G5CAUPQP8Uz0L/EaPZVNWp6bTMoQtVIe665uZSdIcnYlBcXp0eIjwO1dnLX4w5/zhMYg9Fvv2Hu+FjqSDgwdcSZziMOntU2hm9ICD5PTF2U3ai5/cyyU7l/frBnJS5HZnNmA2BgabFrphkNe6WzX7HlcPcXOfHyaP+rxdKJtXwjV0sPZb6JP1drhgziMvi+sy0VrztOJYKBeZEXYykTb/QyoqAa7KDTfRN1TraOCDIO9gn1zQ0By5jmPkjODr8up1PkFrjK8Qhzl3cwIDAQAB"
      Note how it is all one line now, and the double-quotes that broke up the lines have been removed.

    • Now if you refresh the AWS SES page for your domain, you should see a little green tick underneath the domain name that says Verified.
  5. Setup MAIL FROM Remember how we told AWS we wanted to use mail.alouy.com as our custom MAIL FROM? Well we need to setup more DNS records to allow that to happen.

    • Expand the "Publish DNS Records" bit in AWS. It'll tell us what new DNS records to add. In this case an MX record and a TXT record for mail.alouy.com. So back to our DNS manager to add in the two new records.
    • Now when we refresh the AWS page, you should see the "Pending" underneath the MAIL FROM Configuration change to "Successful". Note that this one sometimes takes a little while.
  6. Setup DMARC While we're waiting lets add DNS records in for DMARC (Domain-based Message Authentication, Reporting, and Conformance). Same deal as before, you open up the Publish DNS records section and it'll tell you what new DNS records need to exist. For me it's just a new TXT record for _dmarc.alouy.com.
  7. Verify the verifications Now when you refresh the AWS SES page, you should see all green "Successful" messages there. If not, very carefully check the DNS records you added, wait 10 minutes, then check again.
  8. Sending email
    • You'll need one more DNS record to send email out. For me, using AWS SES in the ap-southeast-2 region, I'll need a record like:

      Email sending DNS

      MX alouy.com inbound-smtp.ap-southeast-2.amazonaws.com with priority 10
      The other regions are here.

      This leaves us with 5 DNS records in total, that look a bit like this:
    • You'll also need some SMTP credentials (that look suspiciously like a normal AWS Access Key/ID) that'll give your email client permission to send outbound email through AWS SMTP:
    • Which you then add into your email client's configuration. In my email client mutt, the config for SMTP sending ends up looking like this:

      .muttrc

      # AWS SES ap-southeast-2 set smtp_url="smtp://AKIAUSERID...@email-smtp.ap-southeast-2.amazonaws.com:587/" set smtp_pass="THESECRETPASSWORDGOESHERE..."
  9. Configure mail delivery, and then fetch it
    • Create an S3 bucket
    • Go to AWS SES Email Receiving
    • Setup a Ruleset with an action to deliver to your Amazon S3 bucket
    • Configure a user with a role that can read and delete the files in the bucket so you can fetch email. Create an access key for the user, and then use those credentials in the script that follows.
    • The simplified version of the script that actually fetches the emails from the S3 bucket. It grabs the email files out of the S3 bucket, then pushes them locally through procmail (a mail filtering program) which dumps them into the pertinent local email (maildir) folders.

      fetch-mail.sh

      #!/bin/bash set -euo pipefail shopt -s nullglob

      # Credentials to download emails from s3 bucket

      export AWS_ACCESS_KEY_ID="MY_KEY_ID" export AWS_SECRET_ACCESS_KEY="MY_SECRET_KEY" temp="/path/to/tmp/" aws s3 mv --quiet --only-show-errors --recursive MY_BUCKET/ ${temp}/ >/dev/null for f in "${temp}"/*; do dos2unix -q "${f}" cat ${f} | /usr/bin/procmail -m /path/to/procmailrc rm ${f} done
    • Being triggered by a crontab job that looks like this:

      crontab

      */2 * * * * /path/to/fetch-mail.sh > /dev/null
      Or also manually triggered by a mutt shortcut key for quick adhoc checking:

      .muttrc

      macro index R "!/path/to/fetch-mail.sh\n<sync-mailbox>"
      The procmailrc looks a bit like:

      procmailrc

      MAILDIR=/path/to/Mail/ LOGFILE=/path/to/Mail/procmail.log

      # just for example, match some words in emails, force them to go to particular mailboxes

      :0HB * stripe.com ${MAILDIR}stripe/ :0HB * tfstate ${MAILDIR}tfstate/ :0HB * dibsonstuff ${MAILDIR}dibsonstuff/

      # filter mail through bogofilter, tagging it as Ham, Spam, or Unsure

      :0fw | bogofilter --bogofilter-dir /path/to/Mail/.bogofilter -e -p

      # file the mail to spam-all if it's spam

      :0: * ^X-Bogosity: Spam, tests=bogofilter ${MAILDIR}spam-all/

      # if it is maybe spam, file the mail to spam-maybe

      :0: * ^X-Bogosity: Unsure, tests=bogofilter ${MAILDIR}spam-maybe/

      # everything else goes to the inbox

      :0 : ${MAILDIR}inbox/


← back