Most domains that have DMARC at all are sitting on p=none. Someone added the record while setting up email, pointed rua= at a mailbox, and the reports have been arriving every day since: small zip and gzip attachments from Google, Outlook and other receivers, usually unopened.
Those files answer the one question you need settled before tightening the policy: who is sending mail with your domain in the From line, and would that mail survive p=quarantine or p=reject? Below is how to read them with nothing more than dig, unzip and a small XML tool, and how to move up the policy ladder without losing invoices or password resets on the way. If SPF and DKIM aren't set up yet, start with how to set up business email on your own domain and come back once those records exist.
First, confirm what you've published
Reports describe how receivers judged your mail against your DNS records, so read those records the way a receiver would. Three lookups cover it:
dig +short TXT _dmarc.example.com
dig +short TXT example.com
dig +short TXT s1._domainkey.example.com
The DMARC record lives at the _dmarc label, not on the domain itself. p=none means "report, but don't act", and rua= is where aggregate reports go. If that address is on a different domain, such as a reporting service, the other domain has to publish a small authorization record (example.com._report._dmarc.reports.example.net with the value v=DMARC1). Without it, receivers quietly skip sending.
On the domain itself, look for exactly one TXT record starting with v=spf1. Two of them make SPF return a permanent error, which counts as a fail for DMARC. Other TXT records, like site verification strings, can sit alongside it.
DKIM is the awkward one, because there's no way to list a domain's keys. You need the selector, which is the s= tag in the DKIM-Signature header of any message you've sent (use "show original" or "view source" in your mail client). Your mailbox provider uses one selector, and every other service that signs as your domain uses its own. A 2048-bit key is longer than the 255-character limit for a single TXT string, so dig prints it as two quoted chunks. That's normal. Receivers join them back together.
Open one report
Reports arrive as attachments named after the receiver, your domain and the time window they cover:
google.com!example.com!1792281600!1792367999.zip
enterprise.protection.outlook.com!example.com!1792281600!1792368000.xml.gz
The two long numbers are the start and end of the window in Unix time. date -u -d @1792281600 turns the first one into midnight UTC on October 18. Some receivers zip the file and others gzip it, so unzip or gunzip gets you to the XML. After a short header naming the reporter and repeating the policy it found, there's one <record> per combination of sending IP and result. This one deserves a close look:
<record>
<row>
<source_ip>198.51.100.24</source_ip>
<count>212</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>fail</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.com</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>mailer.example.net</domain>
<result>pass</result>
<selector>m1</selector>
</dkim>
<spf>
<domain>bounces.mailer.example.net</domain>
<result>pass</result>
</spf>
</auth_results>
</record>
Read it in this order:
-
source_ipandcount: 212 messages from one IP, all showingexample.comin the From header (header_from). -
auth_results: the raw checks. DKIM passed and so did SPF. On their own, they look fine. -
policy_evaluated: DMARC's verdict, and both sayfail. DMARC only counts a pass when the domain that passed matches the From domain. DKIM was signed bymailer.example.netand SPF checkedbounces.mailer.example.net. Neither isexample.com, so neither is aligned. -
disposition: what the receiver actually did.none, because that's what the policy asked for. Underp=reject, those 212 messages would have bounced.
Passing in auth_results but failing in policy_evaluated is the pattern you'll see most, and it's nearly always a legitimate service you forgot about, not an attacker. Reports that follow the updated aggregate reporting spec (RFC 9990) may add an xmlns attribute and a few extra fields, but the record layout is the same.
Turn a folder of reports into a sender list
One report is readable by eye. A month of them from a dozen receivers isn't, so pull out the useful columns with xmlstarlet (apt install xmlstarlet on Debian and Ubuntu, brew install xmlstarlet on a Mac):
unzip 'google.com!example.com!1792281600!1792367999.zip'
gunzip 'enterprise.protection.outlook.com!example.com!1792281600!1792368000.xml.gz'
xmlstarlet sel -t -m //record -v row/source_ip -o ' ' -v row/count \
-o ' dkim=' -v row/policy_evaluated/dkim -o ' spf=' -v row/policy_evaluated/spf \
-o ' signer=' -v auth_results/dkim/domain -n *.xml | sort -k2 -rn
Then ask DNS who each unfamiliar IP belongs to with a reverse lookup, for example dig +short -x 198.51.100.24.
Every row lands in one of four buckets, and each bucket has its own fix:
- Your mailbox provider (192.0.2.25 here). DKIM and SPF are both aligned. Nothing to do.
-
A service sending on your behalf (198.51.100.24, which reverse-resolves to a newsletter platform). It passes, but for the wrong domain. Most of these services have a "domain authentication" or "custom sending domain" setting that hands you one or two CNAME records, such as
m1._domainkey.example.compointing at their key. Once they sign withd=example.com, DKIM aligns and DMARC passes. Aligning SPF too, with a custom return-path on a subdomain, is nice to have but not required. One aligned pass is enough. - Your own servers (203.0.113.80, the web server). A contact form or shop sending straight from PHP, with no DKIM signature and an IP that isn't in SPF. Resist the urge to just add the IP to SPF. Send that mail through your mailbox provider's SMTP submission port with a real account instead, so it gets the same aligned signature as everything else.
-
Nobody you recognize (203.0.113.199, no reverse DNS, nine messages). That's someone using your domain without permission, and it's exactly the mail
p=rejectexists to stop. Leave it failing.
Expect a fuzzy fifth group as well: forwarded mail and mailing lists. A list that adds a footer breaks the DKIM signature, and a forwarder's IP was never in your SPF, so a small trickle of failures from university or ISP ranges is normal and can't be fixed from your side. Some large receivers use ARC to give that mail the benefit of the doubt. Don't chase it.
When the volume outgrows a shell one-liner, open-source parsers like parsedmarc can load reports into a dashboard, and hosted report services do the same job. The four buckets stay the same.
Tighten in steps, not in one edit
Once every legitimate source in the last few weeks of reports is aligned, raise the policy one level at a time:
v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.com
v=DMARC1; p=reject; sp=reject; rua=mailto:dmarc-reports@example.com
Older guides ramp up with pct=10, then pct=25, and so on. RFC 9989, the DMARC standard published in May 2026, retires pct because receivers applied the in-between values inconsistently. Its replacement is t=y, which asks receivers to apply one level below what you published, so p=reject; t=y behaves like quarantine at receivers that support it. Plain steps work everywhere: stay on quarantine for a couple of weeks, confirm that nothing legitimate shows up with a quarantine disposition, then move to reject.
A few habits keep each change uneventful:
-
Keep
rua=for good. Reports are how you'll spot the next forgotten sender, like a helpdesk or billing tool someone signs up for next year. -
Lower the TTL on the
_dmarcrecord to 300 or 600 seconds before each change, so stepping back takes minutes rather than hours. -
Decide on subdomains deliberately.
sp=sets the policy for existing subdomains, which otherwise inheritp. Receivers that follow RFC 9989 also readnp=reject, which covers subdomains that don't exist at all. -
Lock down domains that never send mail. A parked domain can publish
v=spf1 -allandv=DMARC1; p=rejecton day one. There's nothing to monitor.
After each step, send a test message from every service in bucket two and check that the Authentication-Results header on the received copy shows dmarc=pass.
What reject actually buys you
At p=reject, receivers that honor DMARC refuse mail that claims to be from your domain and can't prove it. Phishing that copies your exact address stops reaching customers' inboxes, and your own mail stops sharing a reputation with whoever was spoofing it. Look-alike domains are a separate problem, and DMARC doesn't cover them.
None of this needs special software. A few weeks of reading the reports, fixing the senders you recognize, and reject becomes a short DNS edit instead of a gamble. If you're moving mailboxes as part of the cleanup, Pro Gineous business email gives each address a mailbox on your own domain, and every check above works the same there as with any other provider.
Related reading
- Why Is My Email Going to Spam?
- How to Read dig Output When DNS Looks Wrong
- Setting Up DNS Records for Your Website and Mail
Originally published on the Pro Gineous blog.



Top comments (0)