# How to read a DMARC aggregate report: the XML, decoded field by field

Published: 2026-09-07 · Updated: 2026-10-15  
Author: Guanacos Tech (https://guanacostech.com)  
Canonical: https://guanacostech.com/blog/how-to-read-dmarc-aggregate-reports

> The XML inside a DMARC RUA report, field by field: what each element means, which rows are real spoofing, and which are just a forwarder doing its job.

**Short answer:** A DMARC aggregate (RUA) report is an XML file with three parts: report_metadata, policy_published and record blocks, each one a group of messages. Sort records by count and read source_ip first, then compare auth_results with policy_evaluated: SPF or DKIM can pass for a vendor domain while DMARC still fails, because that domain does not align with your From domain.

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:

Free tool

Read your DMARC aggregate report in seconds

Upload the XML a mailbox provider sent you and see who is sending as your domain and whether they align.

[Open the DMARC analyzer](https://guanacostech.com/dmarc-analyzer)

## 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:

- `np` carries 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.
- `testing` carries the value of the new `t` tag, which replaced the percentage based rollout. The old `pct` tag is deprecated in RFC 9989, along with `rf` and `ri`. 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_method` says how the receiver worked out your organizational domain: `psl` for the old Public Suffix List, or `treewalk` for 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](https://guanacostech.com/blog/spf-too-many-dns-lookups-fix).

## 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](https://guanacostech.com/blog/gmail-550-5-7-1-low-reputation-fix) 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](https://guanacostech.com/blog/dmarc-p-none-to-p-reject-rollout) 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](https://calendar.app.google/pq2seCCcch9U2GFV8) and we will go through a real one with you.

## Sources

- [RFC 9990: DMARC Aggregate Reporting, May 2026](https://www.rfc-editor.org/info/rfc9990/)
- [RFC 9989: Domain-Based Message Authentication, Reporting, and Conformance (DMARC), May 2026](https://www.rfc-editor.org/info/rfc9989/)
- [RFC 9991: DMARC Failure Reporting, May 2026](https://www.rfc-editor.org/info/rfc9991/)
- [Define your DMARC record - Google Workspace Admin Help](https://support.google.com/a/answer/10032169)
- [Email sender guidelines - Gmail Help](https://support.google.com/mail/answer/81126)

## Frequently asked questions

### How often do DMARC aggregate reports arrive?

Most large receivers send one report per policy domain per day, covering a window that usually runs midnight to midnight UTC. The interval is the receiver’s choice, not yours: the old ri tag that asked for a different cadence was deprecated in RFC 9989, and receivers were already ignoring it in practice. Expect a handful of reports daily from the big providers and occasional ones from small mail hosts.

### Why does a report show SPF passing and DMARC failing on the same message?

Because they are answering different questions. The result inside auth_results says the SPF check passed for whatever domain was in the envelope sender, often a vendor bounce domain. The result inside policy_evaluated says whether that domain aligned with the domain in the From header the reader sees. A pass with no alignment is a DMARC fail, and it is the single most common pattern in reports from marketing, CRM and invoicing platforms.

### Do aggregate reports show me the actual emails?

No. They carry counts, sending IPs and authentication outcomes, with no subject lines, recipients or message bodies. Per message detail belongs to failure reports, the ruf tag specified in RFC 9991, and most large receivers do not send those for privacy reasons. Any deliverability work you plan should assume aggregate data is all you will get.

---

Guanacos Tech is an independent consultancy with Google-certified engineers: email deliverability, Google Workspace and Google Cloud, in English and Spanish. Book a free 30-minute call: https://calendar.app.google/pq2seCCcch9U2GFV8
