You published a DMARC record with a rua address, and now something lands in that mailbox every morning: a compressed file with a name that reads like a shell command, holding a page of XML. Most people open one, see a wall of tags, and close it again. That file is the only free, continuous view you have of who is putting your domain in the From header of an email, so it is worth twenty minutes to learn to read one properly.
The format got its own specification in May 2026. RFC 9990 covers aggregate reporting, RFC 9991 covers failure reporting, and RFC 9989 covers the protocol itself. Together they replace RFC 7489, which had been the reference since 2015. Almost every guide online still describes the old document. The XML kept its shape but gained a few fields, and knowing which ones are new tells you which receivers have caught up.
Where the reports come from
Every receiver that supports DMARC reporting reads your policy record, notices the reporting address, and starts counting. It groups the mail it saw claiming your domain by sending IP and by authentication outcome, then sends you the totals for the period. It does not send you the messages.
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com"
Reports are attached as XML, compressed with gzip or zip, and RFC 9990 fixes the filename so you can sort a mailbox by hand:
receiver!policy-domain!begin-timestamp!end-timestamp[!unique-id].extension
google.com!example.com!1788566400!1788652800.zip
The first field is who is telling you, the second is the domain they are telling you about, and the two numbers are the window in Unix seconds. Put the reporting address on a group rather than on somebody's mailbox, because a domain with any real volume collects a dozen of these a day from providers you have never heard of, and the address has to outlive whoever set it up.
Aggregate reports carry counts and verdicts, never content: no subject lines, no recipients, no bodies. Per message detail lives in failure reports, the ruf tag now specified in RFC 9991, and most large receivers do not send those at all for privacy reasons. Plan your work around aggregate data, because in practice it is all you get.
The three parts of the file
Every report has the same skeleton: who sent it and for when, what they read your policy as saying, and then one block for every distinct combination of sending IP and authentication outcome they saw. Here is a complete report with a single passing record, trimmed of nothing that matters:
<feedback>
<report_metadata>
<org_name>google.com</org_name>
<email>noreply-dmarc-support@google.com</email>
<report_id>3921764408171295000</report_id>
<date_range>
<begin>1788566400</begin>
<end>1788652800</end>
</date_range>
</report_metadata>
<policy_published>
<domain>example.com</domain>
<adkim>r</adkim>
<aspf>r</aspf>
<p>none</p>
<sp>none</sp>
<np>reject</np>
<testing>n</testing>
<discovery_method>treewalk</discovery_method>
</policy_published>
<record>
<row>
<source_ip>209.85.220.41</source_ip>
<count>128</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>pass</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.com</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>example.com</domain>
<selector>google</selector>
<result>pass</result>
</dkim>
<spf>
<domain>example.com</domain>
<scope>mfrom</scope>
<result>pass</result>
</spf>
</auth_results>
</record>
</feedback>
That is what healthy looks like: your own mail, your own selector, both checks aligned, nothing to do. Real reports have twenty of these blocks and two of them are interesting. If you would rather not count tags at three in the afternoon, paste a report and get the same breakdown as a table:
report_metadata: who is telling you, and about when
The org_name and email identify the receiver, not you. report_id is unique for that reporter, which matters when you deduplicate a year of reports, because a retransmitted report reuses its original filename and identifier.
The two numbers in date_range are seconds since the start of 1970. Convert them on any Linux or macOS shell:
date -u -d @1788566400 # 2026-09-05 00:00:00 UTC
date -u -r 1788566400 # the macOS spelling
That window belongs to the reporter, not to your working day. It usually runs midnight to midnight UTC, six hours ahead of San Salvador, so the burst you saw on Tuesday afternoon can land in what the report calls Wednesday. Convert before you correlate anything against your own logs.
policy_published: your record as somebody else read it
This block is the receiver reading your DNS back to you, and it is the fastest way to catch a typo you have been staring past for a month. If p says none and you were sure you moved to quarantine last week, either the change never propagated or you edited the wrong zone.
Three of the fields in the example above did not exist under the old specification, and they are worth recognising:
npcarries your non-existent subdomain policy, the tag that lets you reject mail from subdomains that are not in DNS at all without touching the policy for the ones that are.testingcarries the value of the newttag, which replaced the percentage based rollout. The oldpcttag is deprecated in RFC 9989, along withrfandri. A record that still carries them is not broken, since receivers ignore what they do not recognise, but it was written against a superseded document.discovery_methodsays how the receiver worked out your organizational domain:pslfor the old Public Suffix List, ortreewalkfor the DNS based method defined in RFC 9989. It is the clearest indicator of how current a given receiver is.
adkim and aspf are the alignment modes, r for relaxed and s for strict. Relaxed accepts a match at the organizational domain, so mail signed by mail.example.com aligns with a From address at example.com. Strict demands the exact name. Start relaxed. Strict is a hardening step for later, and it breaks subdomain senders quietly.
The record block, read in the right order
Each record is one group of messages, not one message. Read it in this order and it stops being confusing.
source_ip and count. The IP that connected, and how many messages came from it in the window. Sort every report by count descending. The top three rows are your real mail streams, and any failing row in the top ten is a system you own and forgot about, not a criminal.
header_from. The domain in the From line the reader sees. This is the identity DMARC protects. envelope_from, when present, is the bounce address, and it is the one that SPF actually checks.
policy_evaluated. The verdict after alignment, plus the disposition the receiver applied: none, quarantine or reject. At p=none the disposition is always none, so this field tells you nothing until you enforce. The dkim and spf children are the ones that matter.
auth_results. The raw outcome of each check, with the domain that was checked. For DKIM you also get the selector, which is how you tell your Workspace signature apart from your invoicing platform's.
The pair of fields everyone misreads
Here is the row that generates most of the confusion, and it is worth reading twice:
<policy_evaluated>
<disposition>none</disposition>
<dkim>fail</dkim>
<spf>fail</spf>
</policy_evaluated>
...
<identifiers>
<header_from>example.com</header_from>
</identifiers>
<auth_results>
<spf>
<domain>mailer.vendor.net</domain>
<scope>mfrom</scope>
<result>pass</result>
</spf>
</auth_results>
SPF passed and DMARC failed on the same message, and nothing is broken about SPF. The check passed for mailer.vendor.net, the vendor's own bounce domain, while the reader sees example.com. Two different domains, no alignment, no DMARC pass. The results inside auth_results are the raw checks; the results inside policy_evaluated are those same checks after asking whether they were about the domain in the From header.
DMARC needs only one of the two to pass and align. That is why the durable fix for a marketing or invoicing platform is almost always domain authentication with a DKIM key on your own domain, rather than another include bolted onto an SPF record that is already close to the ten lookup limit.
Forwarding, and the rows that look like an attack but are not
A row with your header_from, a failing SPF result and a passing, aligned DKIM signature is usually a forwarder. Someone put an auto-forward on their old address, or a mailing list relayed your message. Forwarding rewrites the connection, so the sending IP is no longer yours and SPF has nothing to say. The DKIM signature travels with the message and survives, which is exactly what it is for.
The reverse pattern, DKIM failing while SPF passes and aligns, often means something modified the message in transit: a list appended a footer, or a security gateway rewrote the links. Small counts and a source that resolves to a university, a mailing list host or a corporate gateway: that is normal internet behaviour, and not a reason to delay enforcement.
Some reporters also fill in a reason element inside policy_evaluated with values such as forwarded, mailing_list, trusted_forwarder or local_policy, which tells you the receiver made an exception. Older reports may carry sampled_out, a leftover from the deprecated percentage tag. Treat them as hints, since filling them in is optional and most reporters skip it.
Spoofing versus your own untidy DNS
After a fortnight of reports the failing sources sort themselves into two piles.
Your own systems look like infrastructure: the same IP or the same small range every single day, a count that follows your business rhythm, and an auth_results block with no DKIM entry at all because nobody ever completed domain authentication at that vendor. The IP resolves to a name you can identify, a CRM, a form handler, a payroll system, the invoicing provider, the multifunction printer on the second floor.
Impersonation looks like weather: a scatter of unrelated IPs in networks you have no relationship with, counts that spike for two days and disappear, no DKIM signature or a signature from a domain that is not yours, and no pattern that matches anything you run. It arrives and it stops.
The order to work in is the boring one. Fix everything in the first pile until every legitimate stream authenticates and aligns. The second pile is precisely what an enforcement policy is for, and you cannot safely enforce until the first pile is empty. If some of those failing rows are already producing bounces, the Gmail rejection guide covers the error strings you will see.
What to do with what you found
Reading reports is not the goal. The goal is a policy that protects the domain, and reports are how you get there without an outage. Once every source in the first pile authenticates, walk the policy up: monitoring, then quarantine, then reject, checking the reports at each step. The rollout guide has the sequence and the timing.
Two habits are worth keeping after enforcement. Look at the reports monthly rather than daily, since a new failing source usually means a department signed up for a tool without telling anyone. And never remove the rua address. It costs nothing, it is the only warning you get, and Google requires a DMARC record from domains sending more than 5,000 messages a day to Gmail accounts.
How Guanacos Tech helps
We read these reports every week, in English and Spanish, for companies across North America and Latin America. A typical engagement starts with two weeks of aggregate data, ends with every legitimate sender aligned and the policy at enforcement, and leaves you with a short list of which IP belongs to which system so the next report takes five minutes instead of an afternoon. If you have reports piling up in a mailbox and no idea which rows matter, book a call and we will go through a real one with you.