I was fixing a bug in my KDBX Diff Analyzer's CSV import feature. Simple stuff. The /api/csv-import endpoint required a pre-existing session, but the CSV Import tab is actually the entry point to the app. Nobody uploads a KDBX file first just to import a CSV. Fixed it to create a session on the fly, same as the upload endpoint. All 265 tests pass. Done.
Then I tried to git push.
"Your account is suspended."
Cool.
My GitHub account is registered under my michaelcli.com address. It's been suspended since around February 14. I filed a support request through GitHub's form that same day and got back one of those automated "can you tell us more" replies. I responded to it from a different email address. Five days of nothing.
They did eventually get back to me, about a week later. Turns out the suspension was because I was pushing code at a pace that looked automated to their systems. Which, to be fair, it kind of is: I use AI to assist my coding, and that means I ship faster than a normal human commit pattern. By the time they responded I had already zoomed off on half a dozen other projects and could have lost a lot of work if something had crashed before I could push again. They reviewed everything, reinstated the account, and re-enabled all features. Fully exonerated.
So now I needed to send email FROM my michaelcli.com address. Which I couldn't do. And that's how a one-line bug fix turned into a week of learning how email actually works.
MX Records vs SMTP: The Thing That Confused Me
I kept conflating incoming and outgoing mail. They're two separate systems.
MX records control where your incoming mail goes. They tell the world "mail for this domain should be delivered to these servers." My MX for michaelcli.com points to Spaceship's forwarding servers, which catch-all forward everything to my personal email, which then forwards to Gmail.
SMTP servers control where your outgoing mail goes through. When Gmail's "Send as" feature sends email on your behalf, it routes through an SMTP server. Either Gmail's own or a custom one you configure.
They're independent. You can keep your MX pointing to one provider for receiving while sending through someone completely different. This is why I could set up Zoho Mail for sending from my michaelcli.com address without touching my MX records or breaking my Spaceship catch-all forwarding.
Obvious in hindsight.
SPF: Who's Allowed to Send As You
SPF is a DNS TXT record that lists which mail servers are authorized to send email for your domain. Mine looked like this:
v=spf1 include:spf.efwd.spaceship.net ~all
That only authorized Spaceship's forwarding servers. When I added Zoho for sending, I needed to merge it into one record:
v=spf1 include:zohomail.com include:spf.efwd.spaceship.net ~all
The catch: you can only have one SPF TXT record per domain. Multiple SPF records are invalid per RFC 7208, and receiving servers will reject your mail or do something unpredictable. Spaceship apparently auto-merges if you add a second one, but I wouldn't count on that.
DKIM: Proving You Actually Sent It
DKIM uses public-key cryptography to sign outgoing emails. The sending server signs with a private key, and the receiving server checks it against a public key in your DNS (a TXT record under selector._domainkey.yourdomain.com).
This is the one that explained why Gmail's "Send as" breaks with Yahoo.
When you use Gmail's SMTP to send as your custom domain, Google signs the email with Google's own DKIM key. Not yours. There's no DKIM record for your domain. Yahoo started enforcing DKIM + DMARC in early 2024, sees your email as unverified, and bounces it.
The fix: route outgoing email through a server that holds your domain's private DKIM key. Options:
- Google Workspace (paid): Google holds your private key, you add the public key to DNS
- Resend, Mailgun, SendGrid, etc.: the service holds your private key
- Your own server with Postfix + OpenDKIM: you hold your own private key (best for privacy)
For smolkin.org, I already had Resend set up with a DKIM key at resend._domainkey.smolkin.org. But Gmail was still routing through smtp.gmail.com instead of Resend. I had to go into Gmail's "Send as" SMTP settings and point it at smtp.resend.com (port 465, SSL, username resend, password is your API key). That's all it took.
DMARC: The Policy Layer
DMARC tells receiving servers what to do when SPF or DKIM fail. Yahoo and Google both require it now.
A basic record:
v=DMARC1; p=none; rua=mailto:[email protected]
p=nonemeans "don't reject anything, just report" (good for starting out)p=quarantinemeans "send failures to spam"p=rejectmeans "reject failures outright"ruais where aggregate reports get sent
Cloudflare offers a managed DMARC reporting address that feeds into their dashboard if you use them for DNS. Worth setting up.
The Services I Tried
Spaceship is my registrar for michaelcli.com. Their catch-all email forwarding just works. MX records point to their servers, everything lands in my inbox. Good for receiving. Can't send. Their DNS management is clunky (can't easily edit existing records, auto-merges SPF records without telling you).
Zoho Mail has a free tier: 5 users, 5GB each, one custom domain. Real mailbox with a web interface, and it supports DKIM signing. The free plan is web-only (no IMAP/POP), so forget about desktop mail clients. Good enough when you just need to fire off a few emails from a custom domain. Pro tip: the free plan URL is mail.zoho.com/signup?plan=free because their default signup flow pushes you toward the paid trial.
Resend is developer-focused transactional email. Free tier gives you 100 emails/day but only one domain. Want more? That's $20/month. Great API, solid SMTP support, handles DKIM signing. The tradeoff: as a third-party relay, they can see and log all email content passing through their servers. I'd rather use my own server instead.
The Plan: Self-Hosted Postfix + OpenDKIM
I have an idle Ubuntu server on DigitalOcean. Already paying $5/month for it. Might as well put it to work.
- Install Postfix + OpenDKIM on the server
- Generate DKIM key pairs for both
smolkin.organdmichaelcli.com - Add the public keys as TXT records in Cloudflare and Spaceship
- Configure Postfix as a send-only authenticated SMTP relay
- On my Mac, configure
msmtpor Apple Mail to relay through my server - Emails leave this computer, hit my server, get DKIM-signed, and are delivered. No third party sees the content.
One server, all my domains. No per-domain fees, no third party reading my email content. Adding a new domain is just generating another key pair.
The DNS Records
For my own reference (and yours, if you're doing something similar), here's the current state:
michaelcli.com (Spaceship DNS):
| Record | Type | Host | Value |
|---|---|---|---|
| MX | MX | @ | mx1.efwd.spaceship.net / mx2.efwd.spaceship.net |
| SPF | TXT | @ | v=spf1 include:zohomail.com include:spf.efwd.spaceship.net ~all |
| DKIM | TXT | zmail._domainkey | (Zoho's DKIM public key) |
smolkin.org (Cloudflare DNS):
| Record | Type | Host | Value |
|---|---|---|---|
| MX | MX | @ | route1/2/3.mx.cloudflare.net |
| SPF | TXT | @ | v=spf1 include:_spf.cloudflare.com include:_spf.google.com ~all |
| DKIM | TXT | resend._domainkey | (Resend's DKIM public key) |
| DMARC | TXT | _dmarc | v=DMARC1; p=none; rua=mailto:(...) |
Once I get the Postfix setup running, both domains will need new SPF include: entries for my server's IP and new DKIM TXT records under my own selector. I'll write that up too when it happens.
Oh Right, the Bug
Almost forgot. The thing that started all this.
The KDBX Diff Analyzer CSV import fix: /api/csv-import used requireSession middleware, but CSV Import is a valid entry point with no prior upload. Users going there first got a 401. Fixed it to create-or-reuse a session, same as /api/upload. Also fixed 2 DuplicateFinder test expectations that broke during a rebase. All 265 tests pass.
Committed locally. Can't push until GitHub gives me my account back.
Which is the whole reason I learned about email infrastructure in the first place.
Enjoyed this post?
Get notified when I publish something new. No spam, unsubscribe anytime.