Rapheal RobinsonSite Reliability Engineer & Game Developer

← Blog & Chat

engineering postmortem

Gmail compliance when deploying a fresh company mail server

Five bounces from my own mail server, and what each one taught me.

Part of Tidan Games Platform

I wanted the studio’s monthly backups emailed to me without paying a mail provider. So the cluster got its own mail system: a send-only relay that signs outgoing mail, and a full mailbox server with spam filtering. The first test email bounced. So did the next four. Each bounce was a different message, and each taught me a rule the big providers enforce.

1. 550-5.7.1: no Message-ID

Gmail rejected the first message because it had no Message-ID header. My backup job built the email by hand in Python and only set From, To and Subject. Gmail treats a missing Message-ID (or Date) as a sign of a broken or abusive sender.

Fix: always set both headers.

msg["Date"] = email.utils.formatdate(localtime=False)
msg["Message-ID"] = email.utils.make_msgid(domain="tidangames.com")

2. 550-5.7.26: neither SPF nor DKIM passed

With valid headers, the next bounce was about authentication. The domain had no SPF record, so Gmail had no proof the server could send for it. The relay already signed mail with DKIM, but the public key wasn’t in DNS yet. I published three records:

Record Purpose
SPF (v=spf1 ip4:... -all) says which IP may send mail for the domain
DKIM (mail._domainkey) public key that verifies the relay’s signature
DMARC (_dmarc) tells receivers what to do when a check fails

3. DNS refused the DKIM key: CharacterStringTooLong

The DKIM public key was 417 characters long, and one DNS TXT string holds at most 255. The record has to be split into quoted strings, which receivers join back together:

"v=DKIM1; k=rsa; p=MIIBIjANBgkqh...first 200 chars..." "...the rest of the key..."

4. 504: need a fully-qualified HELO

Once the relay accepted the job’s mail, my own inbox server rejected some senders because they introduced themselves with a bare container hostname. Mail servers expect the HELO/EHLO name to be a fully-qualified domain name. Python’s smtplib uses the machine’s hostname unless you pass local_hostname, and in a Kubernetes pod that is just the pod name.

5. Delivered, but straight to Junk

Mail now arrived, but in the spam folder. The backup job connected from inside the cluster without logging in, so its mail failed SPF from the server’s own point of view, and Rspamd (which files spam into Junk instead of deleting it) did its job. The fix: stop treating the backup job as an outside sender. It now logs in and submits mail as the mailbox user, which is what authenticated submission is for.

Takeaways

Comments

    Your email is never shown or shared. It's only used to email you when someone replies, after you confirm it, and every email has a link to stop them. To prevent spam, the server keeps your IP address and a random ID saved in this browser.