The Origin Story

For years, my personal email at [email protected] worked because .in domain owners got free mail hosting through India’s National Informatics Centre — mailgw.nic.in. It was a quiet, dependable arrangement.

Until one morning, it wasn’t.

$ dig MX debdut.in +short
0 mailgw.nic.in.

The MX record still pointed there. The service did not.

NIC had wound down the free email offering. My inbox was a dead end on a domain I still very much used — sitting on business cards, on this blog, in the footer of every personal project I’d ever shipped.

The Easy Answer I Didn’t Take

The obvious move was Google Workspace. ₹500/month per user, native Gmail UX, Drive thrown in, problem solved in fifteen minutes.

But ₹6,000/year for one mailbox I check casually? For an identity that gets maybe ten emails a week? That felt absurd.

The actual requirements were small:

  1. Receive at [email protected], end up in my existing Gmail
  2. Send as [email protected] from that same Gmail
  3. Pass SPF, DKIM, and DMARC so mail actually lands in inboxes — not in spam, not flagged with “via amazonses.com”
  4. Cost: as close to zero as physics allows

Free Gmail can do (1) and (2) with the “Send mail as” feature — but only if you can prove you actually send mail from that domain. Google killed sending-through-gmail-for-arbitrary-addresses around 2010 to fight spoofing. So the whole problem reduces to: find a free or near-free way to send authenticated mail from your own domain, and route inbound mail back to where you already read it.

The Architecture

The trick is splitting send and receive across two free-tier services:

              ┌─────────────────┐
  Inbound ──▶ │   Cloudflare    │ ──▶ Gmail (mine)
              │  Email Routing  │
              └─────────────────┘

              ┌─────────────────┐
   Outbound ◀ │   AWS SES SMTP  │ ◀── Gmail "Send mail as"
              │   (us-east-1)   │
              └─────────────────┘
                       │
                       └──▶ Recipient

Cloudflare Email Routing handles the inbound side. It owns the MX records on debdut.in, accepts mail for any address I configure, and forwards it to my real inbox. It is free, unlimited, and ships with SPF and DKIM that Just Work.

AWS SES handles the outbound side. SES is the same email infrastructure Amazon uses for its own retail receipts — boring, reliable, ten cents per thousand emails. It signs my outbound mail with DKIM keys tied to my domain, so recipient servers can cryptographically verify the mail really came from debdut.in.

Gmail “Send mail as” is the UX layer. I never have to leave the Gmail inbox I already live in. When I hit “Reply” to a mail forwarded by Cloudflare, Gmail routes the reply through SES, with the From: header showing [email protected].

Two providers, both free or near-free, with my existing inbox as the cockpit.

The Setup, In Broad Strokes

The full step-by-step with every CLI command is in this Gist — going phase-by-phase here would turn this post into a Stack Overflow answer. The high-level arc:

  1. Move DNS for debdut.in to Cloudflare
  2. Enable Cloudflare Email Routing → adds MX records, accepts inbound mail
  3. Verify debdut.in as a sending identity in AWS SES → adds DKIM CNAMEs
  4. Set up a custom MAIL FROM subdomain (mail.debdut.in) → removes the “via amazonses.com” tag Gmail otherwise shows
  5. Update root SPF to authorize both Cloudflare and SES; publish a DMARC record
  6. Generate SES SMTP credentials (locked to a single From address via IAM)
  7. Configure Gmail’s “Send mail as” with those credentials

End-to-end, this took an evening — most of it spent waiting for SES to verify DNS records and approve production access. The DNS work itself is under thirty minutes.

The Gotchas

The actual reason this post exists. Every generic SES tutorial skips these.

A stale DMARC record from another era

My domain already had a _dmarc TXT pointing to a long-dead address from years ago. Per RFC 7489, two DMARC records means DMARC is ignored entirely — receivers can’t tell which policy is canonical, so they apply none of them.

I lost an hour to “but my new policy is right, why is nothing applying it.” The fix was deleting the old record, not fixing the new one.

Lesson: dig TXT _dmarc.yourdomain +short before publishing anything.

Root SPF missing the new sender

When I added SES, I correctly updated the SPF on the new MAIL FROM subdomain (mail.debdut.in) — but the root domain’s SPF still listed only Cloudflare. That meant my outbound mail passed DKIM but failed SPF alignment when checked against the root.

DKIM alone is enough for DMARC to pass, so the symptoms only surfaced in detailed header inspection. Easy fix once spotted — include both senders on the root:

v=spf1 include:amazonses.com include:_spf.mx.cloudflare.net ~all

Worth checking explicitly.

The configuration set that didn’t exist

My very first test send failed with this baffling error:

554 Configuration Set does not exist: Configuration set <my-first-configuration-set> does not exist.

The SES setup wizard had helpfully assigned a default configuration set name to my domain identity — but the configuration set itself had never been created. SES treats this as a hard rejection.

Two-click fix in the SES console: identity → Configuration Set tab → uncheck “Assign default.” Five minutes of reading the error wrong before that.

MAIL FROM verification stalls if you don’t poke it

SES checks DNS for the custom MAIL FROM domain on its own schedule — usually 15-30 minutes, sometimes much longer. There’s no manual “re-check” button.

The trick: edit the MAIL FROM setting, remove it, save, then immediately re-add it. That forces a fresh DNS lookup. Mine went from “Pending” to “Success” inside a minute.

Don’t use root AWS credentials

Obvious in retrospect, easy to skip in practice. I set up two separate IAM users:

  • debdut-cli — for me to run AWS CLI commands. Scoped to read SES identities and manage SMTP credentials.
  • ses-smtp-user.<timestamp> — created automatically by SES when generating SMTP credentials. Scoped to a single action: ses:SendRawEmail.

I then locked the SMTP user’s policy down further with a condition:

{
  "Condition": {
    "StringEquals": {"ses:FromAddress": "[email protected]"}
  }
}

Now those SMTP credentials can only send mail with From: [email protected]. If they leak — say, from a poorly-secured laptop — the blast radius is one email address, not “anyone who has ever verified a domain on my AWS account.”

This is the kind of thing that costs nothing to set up and saves you an incident report later.

The Proof

Once the dust settled, I sent myself a test mail and looked at the headers via Gmail’s “Show original”:

SPF:    PASS with IP 54.240.8.77
DKIM:   PASS with domain debdut.in
DMARC:  PASS
Return-Path: <[email protected]>
From: Debdut Goswami <[email protected]>

No “via amazonses.com” tag. Return-Path on my own subdomain. All three auth checks green. Indistinguishable from a Workspace mail at the protocol level.

The Cost

Item Monthly Yearly
Cloudflare Email Routing ₹0 ₹0
AWS SES outbound (~300 emails/mo) ₹2.50 ₹30
Total ₹2.50 ₹30

Google Workspace Business Starter, for comparison: ₹500/mo, ₹6,000/year.

Two hundred times cheaper. For an identity I check casually.

What This Does Not Give You

To be fair to Workspace, here is what I am specifically giving up:

  • No Google Drive identity under [email protected] — people sharing Drive files to that address either need to send to a Google Account I create separately, or use “Anyone with link” sharing.
  • No calendar invites from that address. Anything I send via Google Calendar still comes from my personal Gmail.
  • No admin console. No 2FA enforcement. No shared mailboxes across users.
  • No native suite (Docs, Sheets, Meet).

For solo personal email, none of that matters. For a real organization with multiple humans collaborating, it does — and Workspace’s ₹500/mo is genuinely fair value there.

When This Setup Is Right For You

  • You own a domain you barely use for email
  • You’re comfortable touching DNS
  • You already check one Gmail inbox and have no interest in a second one
  • You value “real headers, real deliverability” more than the convenience of a managed suite

And when it is not:

  • You need multiple humans on one domain
  • You want admin tooling and audit logs
  • Your time is worth more than ₹6,000/year (a fair tradeoff to make explicitly)

Closing

Email is one of those things that should be expensive or complicated, and is actually neither — if you are willing to compose the right pieces.

Cloudflare Email Routing solved my inbound. AWS SES solved my outbound. Gmail glued it together at the UX layer. And ₹30/year is the kind of number that makes me grin every time the next AWS bill hits.

The full runbook — every DNS record, every AWS CLI command, every IAM policy — is in the companion Gist. This post was about the why and the gotchas. The Gist is the how.


Next up: automating this entire setup as a one-click flow for any custom domain — Cloudflare and AWS APIs all the way down.