"Our emails are going to spam" is one of the most common tickets in managed hosting, and one of the most misunderstood. The customer thinks it's the server. It's almost never the server — it's three DNS records, and usually two of them are wrong.
Here's what each one actually proves, how they interact, and the specific mistakes I see most often.
The problem all three solve
SMTP was designed with no authentication whatsoever. Anyone can connect to a mail server and claim to be sending from any address. That's not a bug that got fixed — it's still true today. SPF, DKIM and DMARC are three layers bolted on top to let a receiver answer one question: is this sender allowed to use this domain?
The short version: SPF says which servers may send for your domain. DKIM proves the message wasn't altered and came from someone holding your key. DMARC says what to do when the first two fail — and asks for reports so you can see what's happening.
SPF — which servers may send
SPF is a TXT record listing the IPs and hosts permitted to send mail using your domain in the envelope sender. The receiver looks up the record and checks whether the connecting IP is in it.
example.com. IN TXT "v=spf1 mx a:mail.example.com include:_spf.google.com ~all"
v=spf1— version. Must be first.mx— the hosts in your MX records may also send.a:mail.example.com— that specific host may send.include:_spf.google.com— pull in another domain's record. This is how you authorise Google Workspace, Mailchimp, SendGrid and the rest.~all— soft fail: anything not listed is suspicious but shouldn't be rejected outright.
The mistakes I see constantly
1. More than one SPF record. This is the most common fault by far. The spec permits exactly one. Two records is not "extra coverage" — it's a permanent error, and receivers treat the whole thing as invalid.
example.com. IN TXT "v=spf1 include:_spf.google.com ~all"
example.com. IN TXT "v=spf1 include:sendgrid.net ~all"
# Correct — merge into one
example.com. IN TXT "v=spf1 include:_spf.google.com include:sendgrid.net ~all"
2. Exceeding ten DNS lookups. SPF evaluation is capped at ten DNS-querying mechanisms (include, a, mx, ptr, exists, redirect). Go over and the result is permerror — which many receivers treat as a hard fail. Every include can pull in nested includes, so a record with four providers can easily blow the budget.
# See the record
dig +short TXT example.com | grep spf1
# Then expand each include manually to count nested lookups
dig +short TXT _spf.google.com
3. Using +all. This authorises the entire internet to send as your domain. It appears in the wild more often than you'd believe, usually as a "temporary fix" that stopped a bounce and was never revisited.
4. Forgetting SPF only checks the envelope sender. SPF validates the MAIL FROM address, not the From: header the recipient sees. A forwarder or mailing list rewrites the envelope and SPF passes — for the forwarder's domain, not yours. This is exactly the gap DKIM and DMARC alignment close.
DKIM — a cryptographic signature
DKIM signs the message with a private key held by your mail server; the public key lives in DNS. The receiver verifies the signature, which proves two things: the message really came from someone holding your key, and the signed headers and body weren't modified in transit.
default._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSq..."
default is the selector — an arbitrary label letting you publish several keys at
once, which is how you rotate keys or run multiple sending platforms.
# Generate a 2048-bit key pair
opendkim-genkey -b 2048 -d example.com -s default -D /etc/opendkim/keys/example.com/
chown opendkim:opendkim /etc/opendkim/keys/example.com/default.private
chmod 600 /etc/opendkim/keys/example.com/default.private
# The DNS record to publish
cat /etc/opendkim/keys/example.com/default.txt
# Verify what the world can see
dig +short TXT default._domainkey.example.com
On cPanel, this is managed for you — but verify it's actually enabled per domain, because it often isn't on older accounts:
whmapi1 validate_current_installed_email_dkim domain=example.com
whmapi1 install_dkim_private_key domain=example.com
Every sending source needs its own DKIM. If mail leaves through your server and a marketing platform and a CRM, each one needs a signing key with its own selector published in your DNS. The one you forget is the one that lands in spam.
DMARC — policy and, crucially, visibility
DMARC ties the other two together. It tells receivers what to do when SPF and DKIM fail,
and it introduces alignment: the domain that passed SPF or DKIM must match the
domain in the visible From: header. That's what closes the forwarding gap.
_dmarc.example.com. IN TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com; pct=100; adkim=r; aspf=r"
p=— policy:none(monitor only),quarantine(spam folder),reject(refuse at SMTP).rua=— where aggregate reports go. This is the most valuable part of the record.pct=— apply the policy to this percentage of mail, for gradual rollout.adkim/aspf— alignment strictness:rrelaxed (subdomains count),sstrict (exact match).
Roll it out gradually — always
Going straight to p=reject is how you discover, painfully, that your invoicing system
has been sending unsigned mail as your domain for three years. The safe sequence:
- Weeks 1–2:
p=nonewithrua=. Change nothing; just collect reports. - Read the reports. They tell you every source sending as your domain — including the ones nobody remembered. Fix SPF and DKIM for the legitimate ones.
- Weeks 3–4:
p=quarantine; pct=10, then raise the percentage as reports stay clean. - Finally:
p=reject, once you can account for every source in the reports.
The reports are the real prize. Even at p=none, DMARC aggregate reports give you a complete inventory of who is sending mail as your domain. Almost every deliverability problem I've fixed was visible in those reports within 48 hours of turning them on.
Debugging a real complaint
When a customer says mail is going to spam, work in this order:
dig +short TXT example.com | grep spf1
dig +short TXT default._domainkey.example.com
dig +short TXT _dmarc.example.com
# Reverse DNS must exist and match the sending hostname
dig +short -x 203.0.113.45
# Is the IP listed anywhere?
dig +short 45.113.0.203.zen.spamhaus.org # note: reversed octets
Then read the headers of a message that actually landed in spam. The receiving server writes its verdict there:
Authentication-Results: mx.google.com;
dkim=pass header.i=@example.com header.s=default;
spf=pass (google.com: domain of bounce@example.com designates
203.0.113.45 as permitted sender);
dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=example.com
If all three say pass and mail still lands in spam, the records aren't the problem —
it's reputation or content. Check blacklists, look at sending volume patterns, and ask
the awkward question about whether the customer has been buying mailing lists.
The checklist
- Exactly one SPF record, under ten DNS lookups, ending
~allor-all. - DKIM signing enabled and verified for every sending source, each with its own selector.
- DMARC published, starting at
p=nonewith a workingrua=address. - Reverse DNS set, and matching the hostname the server announces in HELO.
- The sending IP checked against the major blacklists.
- TLS available on outbound connections — some receivers now penalise plaintext.
Get those six right and the overwhelming majority of "our mail goes to spam" tickets close themselves. The remaining ones are reputation problems, and those are a slower, more honest conversation about what the customer has actually been sending.