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
- Every error named the exact rule that failed. Reading the full bounce text was faster than guessing.
- Deliverability is mostly DNS. SPF, DKIM and DMARC are the price of entry, and they’re cheap.
- Treat internal jobs like real users: authenticate them, give them proper headers and names.
- Test against a strict receiver and a strict server of your own. Gmail caught the first three problems; my own mail server’s checks caught the last two.
Comments
No comments yet. Be the first.