A zone file that serves a website and a zone file that can send mail are the same file. That is why the usual failure looks like this: the site is live, an MX record got added, everything looks configured, and then the first invoice lands in a customer's spam folder.
Deliverability comes down to order more than to any single record. DMARC published before DKIM exists rejects legitimate mail. Two SPF records left behind by a mail migration return permerror, which is worse than having no SPF at all. A sending subdomain with no DMARC record of its own quietly inherits the parent policy.
Here is the sequence I use. Each step ends with a check that has to pass before the next one starts. Nothing below depends on a particular registrar or mail provider.
0. Write the sending map before touching DNS
List every system that will put your domain in a From: header, and give each one its own DKIM selector:
| Sender | Envelope domain | Auth |
|---|---|---|
| People in the mail client | example.com | SPF + DKIM (s1) |
| App / transactional mail | example.com | DKIM (s2) |
| Newsletter platform | mail.example.com | DKIM (s3), own DMARC |
| Website contact form | example.com | DKIM (s2) |
The map matters because selectors are per-sender, and you need all of them before you configure DMARC. Anything you forget here becomes the thing that breaks in step 6.
1. Register the domain and keep DNS in one place
Use the registrar's nameservers or delegate the zone to a DNS host, but pick one location and keep the whole setup there. Splits, where some records sit at the registrar and some at the hosting panel, are where "the record exists but nothing resolves" comes from.
Lower the TTL on the records you are about to create to 300. You will edit them several times; a 3600 TTL turns each edit into an hour of waiting.
2. Publish and verify the web records first
Do the boring part before mail, so a broken A record is never confused with a broken mail setup.
example.com. 300 IN A 203.0.113.10
example.com. 300 IN AAAA 2001:db8::10
www.example.com. 300 IN CNAME example.com.
dig +short example.com A @1.1.1.1
dig +short example.com A @8.8.8.8
dig +short www.example.com CNAME @1.1.1.1
No CNAME at the apex. If your DNS host offers ALIAS or ANAME, that is a different record type and it is fine; a literal CNAME on example.com is not.
If the web half is being handed to someone else, that is a separate job from the mail half, and it is worth knowing which one you are buying. AumCreate's Setup service ($399, what's included) installs a WordPress template on the client's own hosting, replaces every page with the client's content, sends a preview link with one round of revisions, and goes live within five working days of the material being complete. Domain and hosting stay in the client's name and are paid to the third party. The records in this article still have to be published by whoever owns the zone — a site handover does not touch mail.
3. Create MX records only after the mail host exists
The MX target has to be a hostname that already accepts mail for the domain, and its own A record has to resolve.
example.com. 300 IN MX 10 mx1.mailhost.example.
example.com. 300 IN MX 20 mx2.mailhost.example.
dig +short example.com MX @1.1.1.1
Two MX records at the same priority is a coin flip per delivery attempt. Different priorities give you a fallback instead.
If you run your own MTA on a VPS, the PTR for the IP has to resolve forward to the same hostname you use in HELO. The PTR is set at the hosting provider, not in your zone, so start that request now — it is the one item in this list you cannot fix from the zone file.
4. SPF: one record, at the apex, under ten lookups
example.com. 300 IN TXT "v=spf1 include:_spf.mailhost.example include:_spf.newsletter.example -all"
The rules that break people:
- Exactly one
v=spf1record per name. Two TXT strings starting withv=spf1is apermerror, not a merge. When you migrate, delete the old record in the same change that adds the new one. - The ten-DNS-lookup limit counts
include,a,mx,ptr,existsandredirect.ip4andip6are free. Nested includes count everything they pull in, which is where the limit actually bites. -
-allmeans everything else is unauthorized.~allis a soft fail you use during a migration. Pick one on purpose. - SPF is evaluated against the envelope sender (
MAIL FROM, falling back to HELO), not theFrom:header. A perfect record for the wrong domain does nothing for DMARC alignment.
Check with dig +short example.com TXT. If the string appears twice, stop and fix it.
5. DKIM for every sender, verified before DMARC goes on
The provider generates a key pair. You publish the public half as a TXT record at <selector>._domainkey.example.com.
s1._domainkey.example.com. 300 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A..."
- Use 2048-bit keys.
- A long
p=value exceeds 255 characters. In raw zone text you split it into quoted strings that the resolver concatenates; most control panels do this for you, and if you paste a key into a panel that does not, you get a truncated key. - Verify the record resolves and is complete before anything depends on it:
dig +short s1._domainkey.example.com TXT @1.1.1.1 | wc -c
Compare the length against the key the provider shows you. A truncated p= fails silently — mail still goes out, just unsigned.
Then send a real message and read the Authentication-Results header at the receiving end. You want dkim=pass with d= equal to your domain, not the provider's.
6. DMARC: start at none, read the reports, then enforce
_dmarc.example.com. 300 IN TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com; adkim=r; aspf=r; fo=1"
p=none tells receivers to evaluate and report without acting on the policy. Leave it there through a couple of weeks of real traffic, including the newsletter and the form mail.
Then move in this order, a few days apart:
-
p=quarantine; pct=25, thenpct=50, thenpct=100. -
p=reject. - Only then tighten to
adkim=s; aspf=sif everything is genuinely aligned.
Two things that catch people here:
- A subdomain without its own
_dmarcrecord inherits the organizational policy, includingsp=, which defaults top=. Bulk mail sent asmail.example.comwith aFrom:on that subdomain needs either alignment or its own policy record. -
ruahas to point at a mailbox that exists and that someone reads. The reports arrive as XML attachments — ugly, and the only way to see which sender is failing.
dig +short _dmarc.example.com TXT. It has to be a TXT record; a CNAME at _dmarc resolves at some resolvers and fails at others.
7. What comes after mail already flows
Not required for deliverability, but cheap once the zone is clean:
- MTA-STS: TXT at
_mta-sts.example.comwithv=STSv1; id=<timestamp>, plus the policy file served over HTTPS athttps://mta-sts.example.com/.well-known/mta-sts.txt. That host needs a valid certificate, so do this after TLS is settled. - TLS-RPT: TXT at
_smtp._tls.example.comwithv=TLSRPTv1; rua=mailto:tls@example.com. - BIMI last, and only with a VMC. Skip it until DMARC has been at
p=rejectfor a while.
8. The failures that show up weeks later
- A recipient forwards your mail. SPF breaks at the forwarder; DKIM survives if the body was not rewritten. This is the main reason DKIM has to be in place before enforcement.
- A sender is retired and its
includestays in SPF. You keep authorizing an IP range you no longer control. - The newsletter moves to a new provider with a new selector and nobody publishes the matching DKIM record. DMARC keeps passing on SPF until the day alignment slips.
- TTL stays at 300 forever. Harmless, but raise it once you are done, or a slow resolver somewhere will hold your setup-time mistakes longer than you would like.
The check to run before calling it finished
check() {
for ns in 1.1.1.1 8.8.8.8 9.9.9.9; do
printf '%-26s @%-8s ' "$2 $1" "$ns"
dig +short "$1" "$2" @"$ns" | tr '\n' ' '
echo
done
}
check example.com MX
check example.com TXT
check _dmarc.example.com TXT
check s1._domainkey.example.com TXT
If all three resolvers return the same answers, the zone is consistent and the rest is sending real mail and reading the headers at the other end.
Top comments (0)