DMARC Reports: How to Read Aggregate (RUA) and Forensic (RUF) Reports

Flat illustration of a security shield beside a report sheet with a small bar chart, examined under a magnifying glass.

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 reportFailure report (forensic)
Requested byrua tagruf tag, shaped by fo
ContainsCounts per sending IP with SPF, DKIM and disposition resultsDetails of a single failed message, possibly headers or body
FormatXML, normally gzip-compressedEmail message in Abuse Reporting Format
FrequencyTypically one UTC day per reportNear real time, per failure
Who sends itMost large receivers, including Google and MicrosoftFew receivers
PrivacyStatistics onlyMay 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>
ElementWhat it tells you
report_metadata/org_name, date_rangeWho sent the report and the period (begin and end are Unix timestamps)
policy_publishedThe policy the receiver found in your DNS: domain, p, adkim, aspf
row/source_ipThe connecting IP address
row/countMessages from that IP with these results
policy_evaluated/dispositionWhat the receiver did: none, quarantine or reject
policy_evaluated/dkim, spfThe DMARC alignment result for each mechanism
identifiers/header_fromThe visible From domain, the one DMARC protects
identifiers/envelope_fromThe domain SPF checked (optional)
auth_results/dkimSigning domain (d=), selector (s=) and raw result
auth_results/spfThe 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 seeLikely causeFix
Known provider IP, spf fail, dkim passProvider uses its own bounce domainNothing 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’sProvider signs with its own domainSet 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 failProvider not configured for your domainFinish the provider’s sender-domain setup before enforcing
Forwarder or mailing-list IP, spf fail, dkim passRelay changes the connecting IP, so SPF breaks; unmodified DKIM signatures surviveUsually nothing; this is why DKIM alignment matters
Unknown IP, passesSomeone set up a sender (a shadow tool) with your DNS credentials or keysFind the owner; document it or revoke it
Unknown IP, failsSpoofing or an abandoned toolLeave 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.