You moved the site to your own VPS, everything works, and then a customer mentions they never got the password reset. You check the logs: sent, accepted, no bounce. It is in their spam folder, or it was dropped silently at the gateway, which is worse.
Key takeaway: stop guessing and read the headers of a message that did arrive. Every major receiver stamps an Authentication-Results line into the message telling you exactly which of SPF, DKIM and DMARC passed. That single line converts a vague "email goes to spam" into a specific, fixable failure — and it will usually point at alignment, not at a missing record.
Step 0: Read the Authentication-Results Header
Send a message from your server to an account you control at a large provider. Open the raw source ("Show original" in Gmail, "View message source" in Outlook) and find:
Authentication-Results: mx.google.com;
dkim=pass header.i=@example.com header.s=mail202608 header.b=Xa3...;
spf=pass (google.com: domain of bounce@example.com designates 203.0.113.10
as permitted sender) smtp.mailfrom=bounce@example.com;
dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com
Three verdicts. Work on the ones that don't say pass, in the order below, and ignore everything else until they all do — advice about subject lines and spam-trigger words is irrelevant while authentication is failing.
If dmarc=fail while both spf=pass and dkim=pass, you have an alignment problem: section four, and by far the most common cause of the symptom in this article.
Step 1: SPF — Who Is Allowed to Send
SPF is a single TXT record at your domain root listing the hosts permitted to send mail for it.
example.com. IN TXT "v=spf1 ip4:203.0.113.10 include:_spf.google.com ~all"
Four things break SPF in practice:
Two SPF records. Publishing a second v=spf1 record is a permanent error, not a merge — receivers treat the domain as having no valid policy. This happens constantly when someone adds a new sending service without editing the existing record. There must be exactly one; merge the mechanisms into it.
The ten-lookup limit. Every include:, a, mx, ptr and redirect costs a DNS lookup, and the limit is ten including lookups nested inside your includes. Add a marketing platform, a helpdesk and a transactional provider and you can quietly cross it — after which SPF returns permerror and you have effectively unpublished it. Count with any SPF validator; ip4: and ip6: cost nothing, so swapping an include: for the provider's published ranges buys headroom.
~all versus -all. Softfail (~all) says "probably not us"; hardfail (-all) says "definitely not us". Start on ~all while you find every legitimate sender, then tighten. +all is the same as no record at all, and appears surprisingly often in copied examples.
SPF checks the envelope, not the From header. SPF validates the MAIL FROM / Return-Path domain, which is often your provider's bounce domain rather than the address your recipient sees. That mismatch is legal, it passes SPF, and it fails DMARC — see step four.
Changes here are DNS changes, so if a fix doesn't seem to take effect, the cause is usually caching rather than syntax — why your DNS changes aren't showing up covers how to check what resolvers are actually serving.
Step 2: DKIM — Sign It Cryptographically
DKIM adds a signature header to outbound mail; the receiver fetches your public key from DNS and verifies it.
mail202608._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBg..."
The mail202608 part is the selector, chosen by you, published in the signature header of every message, and the reason you can hold several keys at once. That makes rotation painless: publish a new selector, switch the signer to it, leave the old key up for a few days, then remove it. Dating the selector, as above, tells you at a glance how stale a key is.
Practical points:
- Use a 2048-bit key. Some DNS interfaces need the record split into multiple quoted strings, because a single TXT string is capped at 255 characters; the strings are concatenated on read, so split wherever the interface requires.
- Sign the headers that matter — From, To, Subject, Date, Message-ID — plus the body. Over-signing unstable headers causes failures when an intermediate touches them.
- Body modification breaks DKIM. A mailing list that appends a footer invalidates the signature. That is expected behaviour, not a bug, and it is what ARC exists to paper over.
If you run your own signer this lives in the MTA (OpenDKIM or Rspamd alongside Postfix, or the equivalent in Exim). If you relay through a provider they sign for you — but only once you've published their key at your selector, which is the step people skip.
Step 3: DMARC — The Policy That Ties It Together
_dmarc.example.com. IN TXT "v=DMARC1; p=none; rua=mailto:[email protected]; adkim=r; aspf=r"
p= is the instruction to receivers for messages that fail: none (monitor only), quarantine (spam folder), reject (refuse at SMTP time). rua= is where aggregate XML reports go, and those reports are the only way to discover every system sending as your domain — including the CRM someone in sales set up three years ago.
Always start at p=none and read reports for a few weeks before tightening. Going straight to p=reject on a domain whose full sending inventory you don't know is how you stop your own invoices from being delivered.
Step 4: Alignment — The Reason Pass + Pass Still Equals Fail
Here is the rule most people miss, and the reason a domain with valid SPF and valid DKIM still fails DMARC:
DMARC passes when SPF or DKIM passes AND the domain it passed for aligns with the domain in the visible From: header.
An unaligned pass is not a pass. Two everyday examples:
- Your app sends
From: [email protected]but the Return-Path is[email protected]. SPF passes — forsendingplatform.net. That doesn't align withexample.com, so it contributes nothing to DMARC. The fix is a custom Return-Path on your own subdomain, which every serious platform supports. - Your platform signs DKIM with
d=sendingplatform.net. Same story; the fix is publishing your own selector so the signature carriesd=example.com.
adkim and aspf control strictness. Relaxed (r, the default) accepts a match at the organisational domain, so mail.example.com aligns with example.com; strict (s) requires an exact match. Stay relaxed unless you have a reason.
Anchor on DKIM where you can, because DKIM survives forwarding and SPF does not: when a recipient auto-forwards, the forwarding server's IP is not in your SPF record, so SPF breaks unless that forwarder rewrites the sender.
Step 5: The Server-Level Basics Nobody Checks
Authentication can be perfect and delivery still poor, because the connection itself looks wrong.
Reverse DNS. Your sending IP needs a PTR record resolving to a hostname that resolves forward to the same IP, and your HELO/EHLO should be that same FQDN. Missing or generic rDNS is a hard rejection at some receivers. PTR is set by whoever controls the IP — your host — not in your own zone.
Port 25 is blocked outbound by default at most cloud providers. That is standard abuse prevention, not a misconfiguration on your side. Some hosts unblock on request, some never will; if yours won't, relay through an authenticated SMTP service on port 587 rather than fighting it. Check this before you migrate — it's part of the same due diligence as setting up a first VPS.
IP reputation. A new IP has no history and a recycled one may have someone else's. Check it against the Spamhaus zones, ramp volume gradually, and never send bulk mail from the same IP as your transactional mail.
TLS. Receivers expect opportunistic TLS on delivery, and your submission port should be doing it with a valid certificate — the hygiene covered in issuing and renewing TLS certificates. MTA-STS and TLS-RPT are worth adding once the basics are solid.
Step 6: What the Big Receivers Now Require
Google and Yahoo introduced shared bulk-sender requirements in February 2024, and Microsoft applied comparable rules to Outlook.com consumer mail from May 2025. The consistent shape:
- SPF and DKIM on all sending, with DMARC published for higher-volume senders.
- Aligned domains — the
From:domain must align with the authenticated one. - One-click unsubscribe on bulk mail:
List-UnsubscribewithList-Unsubscribe-Postper RFC 8058, honoured within two days. - A low spam-complaint rate; Google publishes 0.3% as the level to stay under.
The volume thresholds apply to bulk senders, but the authentication expectations are now effectively the floor for everyone — a transactional-only server should meet them anyway.
A Working Diagnostic Order
- Send to an address you control at a major provider; read
Authentication-Results. dmarc=failwith SPF and DKIM passing → fix alignment (step 4). Usually the answer.spf=fail/permerror→ check for duplicate records and count DNS lookups.dkim=fail→ confirm the selector in the message header resolves in DNS, and that nothing in the path modifies the body.- All three pass, still filtered → check rDNS, blocklists, IP age and volume.
- Still filtered → now, and only now, look at content: image-only messages, URL shorteners, link domains that don't match the sender, a missing plaintext part.
Register the domain with Google Postmaster Tools so you can watch reputation over time rather than inferring it from complaints.
FAQ
Why do my WordPress emails go to spam?
PHP's mail() hands the message to a local sendmail binary with no authentication and, usually, no DKIM signature and a Return-Path that doesn't align with your From address. Configure an SMTP plugin to relay through an authenticated service that signs with your own domain, and the problem generally disappears.
Can I go straight to p=reject?
You can, and it's the right destination, but not as a first move. Run p=none until the aggregate reports show every legitimate source authenticating and aligning, then step through quarantine before reject.
My host blocks port 25. Is that a bad host? No — it's the default at most providers and it's aimed at abuse, not at you. Ask about the unblock process, and if the answer is no, relay via an authenticated submission service on 587. Just make sure you know the policy before you commit to a provider.
Before You Blame the Server
Nearly every "our email goes to spam" ticket resolves to one of three things: an unaligned Return-Path, an SPF record over the lookup limit, or a DKIM selector that was never published — all three visible in one header of one delivered message.
If your host blocks outbound mail with no path to an exception, that's a hosting decision rather than a configuration one. Compare providers and plans on Just Server on what they actually permit for outbound mail, then make the move deliberately.