A DMARC report is an XML summary that a mailbox provider sends to the address in your rua tag, usually once a day, listing every IP address that sent mail using your domain and whether that mail passed DMARC. You read it to find two things: legitimate senders that fail, which you must fix before enforcing, and unknown senders, which you must understand. Per-message “forensic” reports (called failure reports in the current standard) are a different, much rarer feed: Google states that “Gmail doesn’t support the ruf tag,” and Microsoft says Microsoft 365 “doesn’t send DMARC Forensic reports.”
Aggregate reports versus failure (forensic) reports
“Forensic” is the older name. The current standard, RFC 9989 (May 2026, replacing RFC 7489), splits reporting into RFC 9990 for aggregate reports and RFC 9991 for failure reports. People still search for the old term, so both appear below.
| Aggregate report | Failure report (forensic) | |
|---|---|---|
| Requested by | rua tag | ruf tag, shaped by fo |
| Contains | Counts per sending IP with SPF, DKIM and disposition results | Details of a single failed message, possibly headers or body |
| Format | XML, normally gzip-compressed | Email message in Abuse Reporting Format |
| Frequency | Typically one UTC day per report | Near real time, per failure |
| Who sends it | Most large receivers, including Google and Microsoft | Few receivers |
| Privacy | Statistics only | May contain personal data |
On frequency, RFC 9990 says “Typically, the reporting period will encompass a single UTC day, beginning at 0000UTC.” On privacy, RFC 9991 notes that “many large-scale providers limit or entirely disable the generation of failure reports” and that aggregate reports “remain the recommended mechanism for gaining visibility into authentication results while preserving the confidentiality of end-user communications.”
RFC 9989 lists the older pct, rf and ri tags as historic; t replaces pct. Our guide to setting up DMARC covers the full record.
How to start receiving reports
Publish a rua tag in your DMARC record. Without it, RFC 9989 says receivers “MUST NOT generate aggregate feedback reports for the domain.”
_dmarc.example.com TXT "v=DMARC1; p=none; rua=mailto:[email protected]"
Use a dedicated mailbox or group. Google’s setup page says “Large organizations might get up to hundreds or even thousands of reports daily,” and recommends a group or dedicated mailbox rather than a personal address.
If the address is on a different domain (a report service, for example), that domain must authorize it. Per RFC 9990, receivers look up a TXT record at <your-domain>._report._dmarc.<destination-domain> that starts with v=DMARC1. For example.com reporting to reports.example.net:
example.com._report._dmarc.reports.example.net TXT "v=DMARC1;"
Without it, receivers must ignore the address. Microsoft adds a condition on its side: it sends aggregate reports only “as long as the MX record for the Microsoft 365 domain points directly to Microsoft 365.”
Anatomy of an aggregate report
RFC 9990 names attachments receiver!policy-domain!begin!end.xml.gz. Inside is one <feedback> element with a metadata block, the policy the receiver saw, and one <record> per combination of sending IP and results. This trimmed record is a legitimate email provider that authenticates but does not align:
<record>
<row>
<source_ip>198.51.100.25</source_ip>
<count>312</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>fail</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.com</header_from>
<envelope_from>bounce.esp.example.net</envelope_from>
</identifiers>
<auth_results>
<dkim>
<domain>esp.example.net</domain>
<selector>esp1</selector>
<result>pass</result>
</dkim>
<spf>
<domain>bounce.esp.example.net</domain>
<scope>mfrom</scope>
<result>pass</result>
</spf>
</auth_results>
</record>
| Element | What it tells you |
|---|---|
report_metadata/org_name, date_range | Who sent the report and the period (begin and end are Unix timestamps) |
policy_published | The policy the receiver found in your DNS: domain, p, adkim, aspf |
row/source_ip | The connecting IP address |
row/count | Messages from that IP with these results |
policy_evaluated/disposition | What the receiver did: none, quarantine or reject |
policy_evaluated/dkim, spf | The DMARC alignment result for each mechanism |
identifiers/header_from | The visible From domain, the one DMARC protects |
identifiers/envelope_from | The domain SPF checked (optional) |
auth_results/dkim | Signing domain (d=), selector (s=) and raw result |
auth_results/spf | The checked domain and raw result |
Disposition stays none here even though both checks fail, because the policy is p=none.
Authenticated is not aligned
SPF and DKIM can both pass in auth_results while DMARC fails in policy_evaluated, because DMARC passes only when an authenticated domain aligns with the From domain. RFC 9990 describes auth_results as “uninterpreted with respect to DMARC,” while policy_evaluated holds the result of the alignment test.
In the sample above, SPF passed for bounce.esp.example.net and DKIM passed for esp.example.net, but the From domain is example.com. Neither matches, so both alignment results fail. RFC 9989 explains why: anyone “can publish an SPF record for its domain and send an email that will obtain an SPF ‘pass’ result,” so only an identifier that aligns with the Author Domain validates use of that domain.
Relaxed alignment (the default for adkim and aspf) accepts a subdomain of the same organizational domain. Strict (s) requires an exact match. DMARC passes if either SPF or DKIM passes with alignment, so one aligned mechanism is enough. The fix is almost always on the sender side: sign with your own domain’s DKIM key and use a custom bounce domain under your domain.
How to triage a report
Sort records by count, then look up each unfamiliar source_ip with reverse DNS to see who operates it.
| What you see | Likely cause | Fix |
|---|---|---|
Known provider IP, spf fail, dkim pass | Provider uses its own bounce domain | Nothing urgent: DKIM alone passes DMARC. For SPF alignment, configure a custom return-path on your domain |
Known provider IP, dkim fail, auth_results/dkim domain is the provider’s | Provider signs with its own domain | Set up domain authentication so it signs with your domain. The report’s selector shows which key was used; see DKIM selector |
| Known provider IP, both fail | Provider not configured for your domain | Finish the provider’s sender-domain setup before enforcing |
Forwarder or mailing-list IP, spf fail, dkim pass | Relay changes the connecting IP, so SPF breaks; unmodified DKIM signatures survive | Usually nothing; this is why DKIM alignment matters |
| Unknown IP, passes | Someone set up a sender (a shadow tool) with your DNS credentials or keys | Find the owner; document it or revoke it |
| Unknown IP, fails | Spoofing or an abandoned tool | Leave it: this is the traffic enforcement is meant to block |
Triage rule for unknown sources: an unknown IP that fails is the reason to enforce, and an unknown IP that passes is the one to investigate first. The second kind is a sender you did not know existed.
Parse reports yourself or use a service
A short script is enough for a small domain. This one uses the standard library, reads .xml.gz, .zip or plain .xml, and strips any XML namespace:
import gzip, sys, zipfile
import xml.etree.ElementTree as ET
def load(path):
if path.endswith(".gz"):
return gzip.open(path).read()
if path.endswith(".zip"):
with zipfile.ZipFile(path) as z:
return z.read(z.namelist()[0])
return open(path, "rb").read()
def text(node, path):
found = node.find(path)
return found.text if found is not None and found.text else "-"
root = ET.fromstring(load(sys.argv[1]))
for el in root.iter():
el.tag = el.tag.rpartition("}")[2]
print(text(root, "report_metadata/org_name"), "for", text(root, "policy_published/domain"))
for rec in root.findall("record"):
row = rec.find("row")
print(
text(row, "source_ip"), text(row, "count"),
"disposition=" + text(row, "policy_evaluated/disposition"),
"dkim=" + text(row, "policy_evaluated/dkim"),
"spf=" + text(row, "policy_evaluated/spf"),
"| dkim d=" + text(rec, "auth_results/dkim/domain"),
"spf d=" + text(rec, "auth_results/spf/domain"),
)
Run python3 parse_dmarc.py report.xml.gz for one line per record. RFC 9990 warns that archive decompressors and XML parsers are “at risk to resource exhaustion attacks (zip bomb or XML bomb)”, so only parse reports from your own mailbox.
Beyond a handful of domains, a report service saves work. It adds aggregation across receivers and days, and mapping of IPs to named senders. Reports reveal your sending infrastructure, so check who can access the data.
When the reports say you are ready to enforce
You are ready to move to an enforcing policy when:
- Every legitimate source you can identify passes DMARC through at least one aligned mechanism.
- The remaining failures are forwarders, mailing lists or unknown IPs you have decided to block.
- Reports from all the major receivers you send to have covered a representative period. Google’s rollout page says “One week is usually enough time for the daily reports to contain data representative of all your mail streams,” but low-volume or seasonal senders may need longer.
Then move from p=none to quarantine and then reject, one step at a time, with t=y as an optional test step. Keep reading reports after each change.
Frequently Asked Questions
What is a DMARC report?
A DMARC report is feedback a receiving mail server sends to the domain owner about mail using that domain. Aggregate reports (rua) are daily XML statistics per sending IP. Failure reports (ruf, formerly forensic) describe individual failed messages and are rarely sent.
How do I read a DMARC aggregate report?
Find each <record>, then check source_ip, count, and the policy_evaluated results for dkim and spf. Compare them with auth_results to see whether a pass failed on alignment. Identify unfamiliar IPs by reverse DNS. A parser script or report service saves reading raw XML.
Why am I not receiving DMARC reports?
Check that your record has a valid rua=mailto: address, that only one DMARC record exists at _dmarc, and that an address on another domain has its _report._dmarc authorization record. Microsoft sends none if your MX does not point to Microsoft 365.
Why does my report show SPF pass but DMARC fail?
auth_results shows whether SPF passed for the domain it checked, usually the provider’s bounce domain. DMARC also needs that domain to align with your From domain. Use a custom return-path on your domain, or rely on an aligned DKIM signature.
What is a DMARC forensic (RUF) report?
It is a per-message failure report sent to your ruf address, now called a failure report in RFC 9991. It can include message headers or content, which is why many providers do not send it. Google’s documentation says Gmail does not support ruf, and Microsoft says Microsoft 365 does not send them.
Who sends DMARC reports, and how often?
Receiving mail providers send them, typically once per UTC day per domain. Each report covers one policy domain from one receiver, so a busy domain gets many reports a day.
Is it safe to send DMARC reports to a third-party service?
Aggregate reports contain IP addresses, counts and domain names, not message content, but they reveal your sending infrastructure. The third party must publish the authorization record in its DNS. Review its data handling terms, and avoid sending ruf reports to outside parties, since RFC 9989 notes they may contain private data.
I’ve spent my career building software at scale with a soft spot for email: deliverability, lifecycle campaigns, and getting messages to actually land. I started Coldletter to fix what bugged me about transactional and marketing email tools. I’m based in Vancouver.
