How to read DMARC aggregate reports
What is inside a DMARC rua report, how to tell your own senders from forwarding and spoofing, and a small script to summarise the XML files.
What aggregate reports are
When your DMARC record contains rua=mailto:..., receivers that support reporting send a periodic summary of the mail they saw using your domain in the From header. The format is XML, defined in RFC 7489 Appendix C, and the usual interval is one day. Each report comes from one receiver, such as a large mailbox provider, and covers only mail delivered to that receiver.
Reports contain counts, IP addresses, domains and authentication results. They do not contain message content, subjects or recipient addresses, which is why most large providers send them. Failure reports (ruf) are different and rarely sent.
The file and its structure
Reports arrive as attachments, usually gzip or zip compressed. RFC 7489 §7.2.1.1 describes the file name as the receiver domain, the policy domain and the start and end of the reporting period as Unix timestamps, separated by !. That makes it easy to sort files by date and receiver before opening them.
<feedback>
<report_metadata>
<org_name>receiver.example.net</org_name>
<email>noreply-dmarc@receiver.example.net</email>
<report_id>1234567890</report_id>
<date_range><begin>1789430400</begin><end>1789516799</end></date_range>
</report_metadata>
<policy_published>
<domain>example.com</domain>
<adkim>r</adkim><aspf>r</aspf>
<p>none</p><sp>none</sp><pct>100</pct>
</policy_published>
<record>
<row>
<source_ip>192.0.2.25</source_ip>
<count>42</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers><header_from>example.com</header_from></identifiers>
<auth_results>
<dkim><domain>example.com</domain><selector>s1</selector><result>pass</result></dkim>
<spf><domain>bounces.esp.example.net</domain><scope>mfrom</scope><result>pass</result></spf>
</auth_results>
</record>
</feedback>| Field | What it tells you |
|---|---|
report_metadata/org_name | Which receiver sent the report. |
date_range | The period covered, as Unix timestamps in UTC. |
policy_published | The DMARC record the receiver saw. Check it matches what you publish. |
row/source_ip and count | Which IP sent how many messages with this combination of results. |
policy_evaluated/disposition | What the receiver did: none, quarantine or reject. |
policy_evaluated/dkim and spf | The DMARC view: pass only when the check passed and aligned. |
identifiers/header_from | The From domain of the messages. |
auth_results/dkim and spf | Raw results with the domain that was checked, before alignment. |
policy_evaluated versus auth_results
This distinction explains most confusing rows. In the example above, auth_results/spf says pass, but policy_evaluated/spf says fail. SPF passed for bounces.esp.example.net, which is not aligned with example.com, so for DMARC purposes SPF did not help.
DKIM in the same row passed for example.com, which is aligned, so policy_evaluated/dkim is pass and the message passed DMARC. A message passes when either aligned result passes. This sender is fine, although fixing SPF alignment with a custom bounce domain would add a second layer.
Relaxed and strict alignment
adkim=r, aspf=r), mail.example.com aligns with example.com because they share the organizational domain. With strict alignment (s), the domains must match exactly.Sorting rows into four groups
1. Your senders, passing
IP addresses of your mailbox provider, your marketing tool or your own servers with an aligned DKIM or SPF pass. These are the goal state and need no action. Confirm the volumes look plausible compared with what you send.
2. Your senders, failing
Addresses you recognise, or that belong to a service you use, where both aligned results fail. Typical causes are a service that signs with its own domain, a new tool nobody configured, or an expired DKIM key. Fix these before enforcing: they are the mail p=reject would block.
3. Forwarding
Unfamiliar IPs, often belonging to universities, companies or mailbox providers, with SPF failing but DKIM passing aligned. A recipient forwards your mail and the forwarding server is not in your SPF record. As long as DKIM passes, DMARC passes and there is nothing to do.
4. Spoofing and noise
Unknown IPs with both SPF and DKIM failing, often in small counts from many networks. That is mail pretending to be you, or occasionally a legitimate system nobody told you about. Confirm the second possibility with the network owner before enforcing; after that, enforcement is exactly what stops this group.
To identify an IP, look up its reverse DNS with DNS Lookup and its network with IP Intelligence. A PTR name like mail-out.esp.example.net or an ASN owned by a known provider settles most rows in seconds.
Summarising reports with a script
For a small domain, a script that totals messages per source IP and result is enough. The example below uses only the Python standard library and reads a folder of .xml, .xml.gz and .zip files. It prints the rows that failed DMARC first, because those are the ones that need attention.
import gzip, sys, zipfile
from collections import Counter
from pathlib import Path
import xml.etree.ElementTree as ET
def open_report(path):
if path.suffix == ".gz":
return gzip.open(path).read()
if path.suffix == ".zip":
with zipfile.ZipFile(path) as archive:
return archive.read(archive.namelist()[0])
return path.read_bytes()
totals = Counter()
for path in Path(sys.argv[1]).iterdir():
root = ET.fromstring(open_report(path))
org = root.findtext("report_metadata/org_name")
for record in root.iter("record"):
ip = record.findtext("row/source_ip")
count = int(record.findtext("row/count") or 0)
dkim = record.findtext("row/policy_evaluated/dkim")
spf = record.findtext("row/policy_evaluated/spf")
passed = "pass" in (dkim, spf)
totals[(passed, ip, dkim, spf, org)] += count
for (passed, ip, dkim, spf, org), count in sorted(totals.items()):
print(f"{'PASS' if passed else 'FAIL'} {ip:<39} dkim={dkim:<4} spf={spf:<4} {count:>6} {org}")Only parse reports you received at your own report address, and treat the files as untrusted input. The standard library parser does not fetch external entities, but very large or malformed files from unknown senders should still be skipped rather than processed blindly.
A worked example
Here is a summarised week of reports for example.com, which publishes p=none and uses a mailbox provider, a newsletter service and an application server. The addresses come from the documentation ranges reserved in RFC 5737, so they stand in for real networks. Each row is one source IP with its total message count across receivers.
| Source IP | Messages | DKIM (aligned) | SPF (aligned) | Identified as |
|---|---|---|---|---|
| 192.0.2.10 | 18,240 | pass | pass | Mailbox provider outbound server |
| 192.0.2.25 | 9,730 | pass | fail | Newsletter service, own bounce domain |
| 198.51.100.7 | 1,120 | fail | pass | Application server sending receipts |
| 203.0.113.44 | 310 | pass | fail | University mail server forwarding to students |
| 203.0.113.90 | 95 | fail | fail | Unknown hosting network, no PTR record |
The first row is the goal state. The newsletter service passes through DKIM alone, which is enough for DMARC; setting a custom bounce domain would add SPF alignment but is optional. The university row is forwarding and needs nothing, because DKIM survives.
Two rows need a decision. The application server passes SPF but not DKIM, so its receipts will fail DMARC as soon as a customer forwards them; enable DKIM signing on the relay it uses. The last row fails everything from a network nobody recognises, which is typical spoofing and exactly what enforcement will stop.
When reports do not arrive
Silence is not a good sign. A domain that sends mail to large providers should see its first reports within a day or two of publishing rua. If nothing arrives, work through the causes below in order, starting with the record itself.
- Check that exactly one DMARC record exists at
_dmarc.example.comand that it parses; a second record makes receivers ignore both. - Check the
ruasyntax: each address needs themailto:prefix, and several addresses are separated by commas. - If the address is on another domain, confirm that domain publishes
example.com._report._dmarc.<its domain>withv=DMARC1. - Confirm the mailbox accepts large compressed attachments and is not filtering report mail as spam.
- Remember that receivers only report on mail they received: a domain that sent nothing to a provider gets no report from it.
The Email Security covers the first three points in one run, including the external authorization lookup. For the fourth, send a test message with a large attachment to the report address and confirm it arrives.
Report services or your own scripts
Both approaches read the same XML, so the choice is about volume and effort. A small domain with a handful of senders is well served by a weekly script run. A domain with many vendors, several brands or strict compliance needs benefits from a service that keeps history, resolves IP owners and alerts on changes.
| Consideration | Own script | Report service |
|---|---|---|
| Cost | Your time | Subscription, often with a free tier |
| Data location | Stays in your mailbox and systems | Reports are processed by the provider |
| IP owner lookup | Manual, with reverse DNS and ASN tools | Usually automatic |
| Alerts and history | Only what you build | Built in |
| Setup | A mailbox and a script | DNS changes to point rua at the service |
Whichever you choose, keep the raw reports for a while. When a service misclassifies a source or a script has a bug, the XML is the evidence you go back to.
Subdomains, several domains and time ranges
A report covers the policy domain it was generated for, but the rows can include mail from subdomains that inherit that policy. The identifiers/header_from field shows the exact From domain for each row, so filter on it when you want to see only news.example.com. Subdomains with their own DMARC record and rua address get separate reports.
If you manage several domains, point them all at the same report address and group by policy_published/domain. One mailbox with a consistent parser is easier to keep working than a separate process per domain. Each external address still needs its authorization record for every domain that uses it.
The date_range values are Unix timestamps in UTC, and most receivers cover a full UTC day. When you compare reports with your own sending logs, convert both to UTC first. A campaign sent late in the evening in a timezone ahead of UTC appears in the previous day's report, which often explains apparent mismatches.
Privacy, volume and retention
Aggregate reports list IP addresses of sending servers and, sometimes, the envelope recipient domain. They are generally not personal data about individual recipients, but they are operational data you should keep in a controlled mailbox. Decide how long you keep them; a few months of history is usually enough to spot trends.
A busy domain receives many reports a day from many receivers. A mailbox that nobody reads becomes a liability quickly, so either automate the summary or use a report service. If the service is on another domain, check with the Email Security that it authorises your reports.
- Use a dedicated address for
rua, not a personal inbox. - Exclude the mailbox from marketing and ticketing systems.
- Watch for sudden drops in report volume: a broken
ruaaddress or a DNS typo stops the flow silently. - Compare
policy_publishedin the reports with your record after every DNS change.
Common surprises
- Your own provider's IPs failing: DKIM signing is not enabled for your domain in the provider's admin console, so only the provider's own signature is present.
- Mail you never sent passing DMARC: an old service still has a valid DKIM key or SPF include for your domain; revoke what you no longer use.
- Counts much lower than you send: reports come only from receivers that report, and only for mail delivered to them.
- The same IP with different results: different messages from one server took different paths, such as direct delivery and forwarding.
- `disposition` of `none` under an enforcing policy: the receiver applied a local override, for example for a known mailing list, and says so in the
reasonelement.
None of these need panic, but each deserves a line in your notes. Patterns that repeat week after week are the ones worth fixing.
Turning reports into actions
| Finding | Next step |
|---|---|
| A known service fails DKIM alignment | Enable custom-domain DKIM in the service; verify with a test message. |
| A known service passes only SPF | Add DKIM as well so forwarded mail keeps passing. |
| Your own servers fail both | Send through an authenticated relay that signs with your domain. |
policy_published differs from your record | DNS caching or a second record; check with Email Security. |
| Large failing volumes from unknown networks | Spoofing; move toward p=reject once legitimate senders pass. |
| No reports at all | Check the rua syntax and external authorization. |
When group 2 is empty for a full reporting window, you are ready to enforce. The p=none to p=reject plan describes the stages and the rollback.
FAQ
Why do I get reports from companies I have never emailed?
Reports come from any receiver that saw mail using your domain, including forwarding destinations and targets of spoofing. That visibility is the point of reporting.
Why does SPF pass in auth_results but fail in policy_evaluated?
SPF passed for a different domain, typically the bounce domain of a sending service, which is not aligned with your From domain. DMARC only counts aligned results.
How often are aggregate reports sent?
The ri tag requests an interval in seconds, 86400 (one day) by default. Receivers decide the actual schedule, and most large providers send daily.
Do all receivers send reports?
No. Many large mailbox providers do, but smaller receivers often do not. Reports are a large sample of your mail, not a complete log.
Can reports contain personal data?
Aggregate reports contain IP addresses, domains and counts, not message content or individual recipient addresses. Failure reports (ruf) can include headers, which is why few providers send them.
Should I use ruf as well as rua?
Aggregate reports are enough for a rollout. Add ruf only if you have a process for handling message-level data and a receiver that actually sends failure reports.