Mail Server Deliverability Checklist
A step-by-step advanced checklist covering PTR records, HELO identity, SPF, DKIM, DMARC, TLS, blocklist remediation, and IP warm-up for a deliverable mail server.
Before you start
- ▸A dedicated public IP address with PTR control via your hosting provider
- ▸A registered domain with access to edit DNS records
- ▸Postfix or another MTA installed and able to send test messages
- ▸Root or sudo access on the mail server
Running your own mail server is satisfying until your messages land in spam or get silently dropped. Deliverability failures are almost always caused by a handful of well-understood configuration gaps: missing or mismatched reverse DNS, incorrect HELO identity, absent or broken SPF/DKIM/DMARC records, and IP reputation problems. This checklist covers every layer, in the order receiving servers check them, so you can work through it top-to-bottom on a fresh server or use it to diagnose an existing one.
1. Reverse DNS (PTR Record)
The first thing a receiving MTA checks is whether your sending IP resolves back to a hostname, and whether that hostname resolves forward to the same IP. A missing or mismatched PTR is an instant spam signal and causes hard failures at some providers.
Set the PTR record through your hosting provider's control panel or API — not in your own DNS zone. The value should match exactly what your mail server announces in its HELO/EHLO greeting.
Verify from your server:
dig -x 203.0.113.10 +short
# Should return something like: mail.example.com.
dig mail.example.com A +short
# Should return: 203.0.113.10
Both lookups must be consistent. Many providers (notably Microsoft and Comcast) reject mail outright when they fail.
2. HELO/EHLO Identity
Your MTA's HELO hostname must be a fully qualified domain name (FQDN) that resolves in public DNS, ideally matching the PTR. For Postfix, set it explicitly:
postconf -e 'myhostname = mail.example.com'
postconf -e 'smtp_helo_name = $myhostname'
systemctl reload postfix
For Exim, the equivalent is primary_hostname in /etc/exim4/exim4.conf.template. Whatever MTA you use, confirm what it actually announces:
openssl s_client -connect mail.example.com:587 -starttls smtp 2>/dev/null | grep -i ehlo
3. SPF Record
SPF authorises which IPs may send mail for your domain. Publish it as a DNS TXT record on the bare domain (example.com, not a subdomain).
A minimal, correct record for a single dedicated mail server:
example.com. 3600 IN TXT "v=spf1 ip4:203.0.113.10 -all"
Use -all (hard fail) rather than ~all (soft fail) once you are confident the record is complete. Add ip6: if your server sends over IPv6. Avoid excessive include chains — the SPF spec limits DNS lookups to 10.
Test it:
dig example.com TXT +short | grep spf
# Use the swaks tool to send a test and inspect the Authentication-Results header
swaks --to [email protected] --from [email protected] \
--server mail.example.com --auth LOGIN
4. DKIM Signing
DKIM attaches a cryptographic signature to outbound mail. Recipients verify it against a public key in DNS. A failed or missing DKIM signature heavily influences spam scoring.
Generate a key pair (OpenDKIM)
# Debian/Ubuntu
sudo apt install opendkim opendkim-tools
# Fedora/RHEL family
sudo dnf install opendkim
# Arch
sudo pacman -S opendkim
sudo mkdir -p /etc/opendkim/keys/example.com
sudo opendkim-genkey -D /etc/opendkim/keys/example.com/ \
-d example.com -s mail
sudo chown -R opendkim:opendkim /etc/opendkim/keys/
This creates mail.private (keep secret) and mail.txt (publish in DNS).
Publish the public key
cat /etc/opendkim/keys/example.com/mail.txt
# Copy the TXT record value and publish it at:
# mail._domainkey.example.com
Long keys (2048-bit) may need to be split across multiple strings in the TXT record — the file from opendkim-genkey already formats it correctly.
Configure and enable OpenDKIM
sudo tee /etc/opendkim.conf <<'EOF'
Mode sv
SubDomains no
Domain example.com
KeyFile /etc/opendkim/keys/example.com/mail.private
Selector mail
Socket inet:8891@localhost
UMask 007
EOF
sudo systemctl enable --now opendkim
Then tell Postfix to milter through it:
postconf -e 'milter_default_action = accept'
postconf -e 'smtpd_milters = inet:localhost:8891'
postconf -e 'non_smtpd_milters = inet:localhost:8891'
systemctl reload postfix
Verify the signature appears in a sent message's headers by mailing yourself and checking for DKIM-Signature: and a pass result in Authentication-Results:.
5. DMARC Policy
DMARC tells receivers what to do when SPF or DKIM checks fail, and gives you a feedback loop via aggregate reports. Start permissive and tighten once you have data.
# Initial record — monitor-only, no enforcement
_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=none; rua=mailto:[email protected]; ruf=mailto:[email protected]; adkim=s; aspf=s"
After two to four weeks of clean aggregate reports, move to quarantine and eventually reject:
# Enforcing record
_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=reject; rua=mailto:[email protected]; adkim=s; aspf=s; pct=100"
Parse incoming aggregate XML reports with a tool like parsedmarc or use a hosted service (Postmark, Dmarcian) so failures don't go unnoticed.
6. TLS and MTA-STS
All modern mail servers must offer STARTTLS. Ensure your certificate is valid and not self-signed for the MX hostname. With Certbot:
sudo certbot certonly --standalone -d mail.example.com
# Then reference the cert in Postfix:
postconf -e 'smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem'
postconf -e 'smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem'
postconf -e 'smtp_tls_security_level = may'
systemctl reload postfix
MTA-STS and TLSRPT further enforce TLS and report failures, but are optional. Publish _mta-sts.example.com and a policy file at https://mta-sts.example.com/.well-known/mta-sts.txt if you want them.
7. Blocklist Checks and Remediation
New or recently reassigned IPs are frequently listed. Check before you send bulk mail:
# Install mxtoolbox CLI or use the web tool; for quick CLI checks:
dig 10.113.0.203.zen.spamhaus.org +short
# A 127.x.x.x response means listed; NXDOMAIN means clean
# Multi-DNSBL check with dnsbl-aclcheck or rbldnsd tools:
for rbl in zen.spamhaus.org bl.spamcop.net dnsbl.sorbs.net; do
result=$(dig -x 203.0.113.10 @8.8.8.8 +short | \
awk -F. '{print $4"."$3"."$2"."$1}' | \
xargs -I{} dig {}.${rbl} +short 2>/dev/null)
echo "$rbl: ${result:-clean}"
done
If listed on Spamhaus, use their removal form at https://check.spamhaus.org/. For Microsoft's SNDS/JMRP, register at https://sendersupport.olc.protection.outlook.com/snds/. Google Postmaster Tools (https://postmaster.google.com) gives you domain and IP reputation dashboards — register your domain there before you start sending.
8. IP Warm-Up
Major providers assign new IPs a low reputation by default. Sending large volumes immediately triggers rate limits or blocks. Warm up gradually:
- Week 1: 200–500 messages/day, highest-engagement recipients only.
- Week 2: Double the volume if bounce and complaint rates stay below 0.1%.
- Week 3–4: Double again. Monitor Google Postmaster and Microsoft SNDS daily.
- Week 5+: Ramp to full volume. Keep complaint rate under 0.08% (Google's threshold).
If you have a second (warmed) IP available, you can route transactional mail through the new IP at low volume while bulk mail stays on the old IP until the new one is warm.
Verification Checklist
Run through these before considering the server production-ready:
- Send to
[email protected]and read the full report. - Send to
mail-tester.comfor a scored report (aim for 10/10). - Use
https://mxtoolbox.com/SuperTool.aspxto check PTR, SPF, DKIM, DMARC, and blocklists in one pass. - Send a test to a Gmail address and inspect the full headers for
dkim=pass,spf=pass,dmarc=pass.
Troubleshooting
Mail accepted but not delivered
Check the mail log first. On systemd-based systems:
journalctl -u postfix --since '1 hour ago' | grep -E 'reject|deferred|bounced'
Deferred with "421" usually means the destination is rate-limiting you — a warm-up problem. A bounce with "550 5.7.1" is an explicit policy rejection; read the diagnostic text for the exact reason.
DKIM signature not verified
The most common causes are: the DNS TXT record not propagated yet (wait and re-test), the key file permissions blocking OpenDKIM from reading the private key, or the public key record being truncated. Verify the key round-trip:
opendkim-testkey -d example.com -s mail -vvv
All lines should report key OK. Any "key not secure" warning means DNSSEC is not signing the record, which is advisory only and won't break DKIM.
SPF permerror
This means you exceeded 10 DNS lookups. Flatten your SPF record using a tool like dmarcian SPF Surveyor to replace include: chains with the resolved IP ranges directly.
Frequently asked questions
- Do I need DKIM if I already have SPF?
- Yes. SPF only validates the envelope sender IP; it breaks when mail is forwarded. DKIM signs the message itself and survives forwarding. DMARC alignment also requires at least one of them to pass, and DKIM gives you the more durable signal.
- Why does Gmail still mark my mail as spam even though SPF, DKIM, and DMARC all pass?
- Authentication is necessary but not sufficient. Gmail also weighs IP and domain reputation, engagement history, and content signals. Register your domain in Google Postmaster Tools to see your reputation score, and ensure you are warming the IP slowly.
- Should I use -all or ~all in my SPF record?
- Use -all (hard fail) in production once you are certain the record covers every legitimate sending source. Start with ~all while auditing. Using ~all permanently signals that you are not fully confident in your own record, which slightly reduces trust.
- How long does IP warm-up take?
- Typically four to six weeks to reach full volume with major providers. The exact timeline depends on engagement rates. Sending only to recipients who expect and open your mail compresses the timeline significantly.
- My PTR is set correctly but Outlook still rejects my mail. What next?
- Microsoft uses its own IP reputation database independent of DNSBL listings. Register at the Microsoft SNDS and JMRP portals and submit a delisting request through the Smart Network Data Services interface. Allow 24–48 hours for the change to propagate.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.