Blog

Your DMARC reports look like garbage. Here is how to actually read them.

Aleksej DixBy Aleksej Dix··8 min read

TL;DR. DMARC aggregate reports look like garbage because they are: zipped XML attachments, one per receiver per day, landing in the mailbox you named in rua=. The schema is machine-to-machine feedback, not a human document. Once you know which fields matter, the file reads like a ledger: which IPs sent as your domain, how many messages each sent, whether SPF and DKIM passed, and what policy your receiver applied. This post walks the XML field by field, the three patterns that drive real action, the noise worth ignoring, and the aggregators that collapse the whole thing into a weekly email. If you have not published DMARC yet, start with the DMARC Academy guide first.

If you followed our Google Workspace or Microsoft 365 walkthroughs, you are now at p=none with a rua= mailbox that is filling up. This is the step most teams give up on. The reports look opaque, the volume is high, and the link to action is not obvious. It does not have to be.

What an aggregate report actually is

An aggregate report (sometimes called an RUA report, after the DMARC tag that addresses it) is a daily summary every DMARC-capable receiver sends back to you. It contains no message content, no subject lines, no recipients. Only three pieces of information, aggregated per source IP over 24 hours:

  1. Which IP sent mail claiming to be your domain
  2. How many messages it sent
  3. Whether SPF and DKIM passed, and what the receiver decided

The file arrives as .zip or .gz containing a single XML document. The filename convention is <receiver-domain>!<your-domain>!<begin>!<end>.xml.gz, for example google.com!sudory.com!1745280000!1745366400.xml.gz. The two numbers are Unix timestamps bounding the reporting window.

The XML schema itself is defined in Appendix C of RFC 7489. It is worth skimming once. Most aggregators silently conform to it, and the odd broken report (usually from a small receiver) becomes much easier to debug when you know the shape.

The XML, piece by piece

A trimmed but realistic report, one <record> element shown:

<feedback>
  <report_metadata>
    <org_name>google.com</org_name>
    <email>noreply-dmarc-support@google.com</email>
    <report_id>17452800000000000000</report_id>
    <date_range>
      <begin>1745280000</begin>
      <end>1745366400</end>
    </date_range>
  </report_metadata>

  <policy_published>
    <domain>sudory.com</domain>
    <adkim>r</adkim>
    <aspf>r</aspf>
    <p>none</p>
    <sp>none</sp>
    <pct>100</pct>
  </policy_published>

  <record>
    <row>
      <source_ip>198.51.100.42</source_ip>
      <count>247</count>
      <policy_evaluated>
        <disposition>none</disposition>
        <dkim>pass</dkim>
        <spf>pass</spf>
      </policy_evaluated>
    </row>
    <identifiers>
      <header_from>sudory.com</header_from>
    </identifiers>
    <auth_results>
      <dkim>
        <domain>sudory.com</domain>
        <result>pass</result>
        <selector>resend</selector>
      </dkim>
      <spf>
        <domain>send.mail.sudory.com</domain>
        <result>pass</result>
      </spf>
    </auth_results>
  </record>
</feedback>

Four blocks. Each one tells you something different.

report_metadata

Who sent the report and when. org_name identifies the receiver (Google, Microsoft, Yahoo, and so on). report_id is an opaque string used to deduplicate if a receiver re-sends. date_range is a Unix-timestamp window, almost always 24 hours.

None of these fields trigger action on their own. They are useful for filing and for noticing which receivers are reporting. If a major provider stops appearing for several days, your rua= mailbox may be dropping messages.

policy_published

An echo of your DMARC record at the time the report was generated. domain is the organizational domain, p is the policy (none, quarantine, or reject), sp is the subdomain policy, adkim and aspf are alignment modes (r relaxed, s strict), and pct is the percentage of failing mail the policy applies to.

This block is a sanity check. If what you see here does not match what you published, either DNS is misconfigured or the receiver is caching an old version. If p shows none after you moved to quarantine, wait 24 hours and check again; receivers cache policies on roughly the TTL of your TXT record.

record

This is where the signal lives. One <record> element per source IP per reporting window. Large domains receive hundreds. The four things to read on every record:

  • row.source_ip + row.count: which IP sent how many messages claiming to be your domain. Pass the IP through a WHOIS lookup or ipinfo.io to get the ASN (Google, AWS, SendGrid, and so on).
  • row.policy_evaluated.disposition: what the receiver did (none, quarantine, or reject). Under p=none, the disposition is always none, and the usefulness is in the next two fields.
  • row.policy_evaluated.dkim and row.policy_evaluated.spf: the DMARC-aligned verdicts (pass or fail). If either passes, DMARC passes overall. If both fail, DMARC fails.
  • auth_results: the raw SPF and DKIM outcomes before alignment was considered. Useful when policy_evaluated shows fail but the raw result was pass: that is the alignment step failing, and the fix is usually to publish on a subdomain that aligns under relaxed mode. The envelope-sender subdomain post covers that in detail.

Everything else (auth_results.dkim.selector, auth_results.spf.scope, policy_override_reason) is useful once you have a specific question. Ignore on first read.

Three red flags worth acting on

After you read a few reports the shape becomes familiar. Most records are green: your ESP, your transactional provider, maybe your helpdesk, all passing SPF and DKIM, all aligned. The interesting records are the ones that are not.

1. High-volume pass from an IP you do not recognise. Hundreds of authenticated messages under your header-From from an ASN you did not onboard. Usually this is marketing signing up for a new ESP without telling ops. Less commonly it is a legitimate tool that publishes SPF or DKIM on its own. Either way, add it to your inventory so the next audit does not flag it.

2. Fail-fail from a known sender. Your invoicing tool, your CRM, or your own application server appears with spf=fail and dkim=fail but recognisable volume. The cause is almost always that the sender was added to your email flow but the DNS records were never published. Two fixes depending on which one failed:

  • If auth_results.dkim.result is none or fail, the sender is not signing. Most ESPs provide a CNAME or TXT pair to publish at <selector>._domainkey.<yourdomain>. Do that and the next report shows dkim=pass.
  • If auth_results.spf.result is fail or softfail, the sender's IPs are not in your SPF record. Add their SPF include fragment (include:<vendor>.com) to your existing v=spf1 record. Do not publish a second SPF record: two records with v=spf1 produce a permerror and break SPF entirely.

3. Fail-fail from an unfamiliar ASN. Low to medium volume from a residential ISP, a bulletproof host, a cheap VPS provider, or a geography you have no presence in. If none of your vendors match, this is almost always an active spoofing attempt. The right response is to tighten policy once your legitimate senders are clean: move p=none to p=quarantine, then to p=reject, following the rollout plan in the Academy. Your strict policy then stops exactly these messages.

Noise to ignore

Not everything that fails is a problem. Four patterns that show up in almost every report and do not warrant action:

  • Forwarders. A handful of messages with spf=fail but dkim=pass from a residential IP usually means a user auto-forwarded to their personal Gmail. Forwarding breaks SPF by design: the forwarder re-sends from its own IP, which is not in your SPF record. DKIM survives because the signature travels with the body. Leave it alone. ARC (RFC 8617) is the forwarding-friendly alternative, and the forwarder has to implement it, not you.
  • Mailing lists. Discussion lists often rewrite subjects and add footers, which breaks DKIM. Some also rewrite the From header, which breaks alignment. Nothing you publish at your end fixes list behaviour.
  • Tiny counts from a wide IP spread. One or two messages each from dozens of random IPs is usually background noise: spammers probing your domain, or legitimate senders on ephemeral infrastructure. Ignore until volume aggregates into a recognisable ASN.
  • Duplicate report_id values. Receivers occasionally re-send reports. Deduplicate on report_metadata.report_id and you will not double-count.

Do not parse the XML yourself

You will read two or three reports by hand to build intuition. After that, send rua= to an aggregator and never open an XML file again. Four options that have a free tier as of this writing:

  • Postmark DMARC (dmarc.postmarkapp.com). Weekly email digest, no account dashboard. Set-and-forget for a single domain.
  • Dmarcian (dmarcian.com). Free tier for small domains, paid tiers for higher volume and forensic reports. Strong source-categorisation UI.
  • Valimail Monitor (valimail.com/products/valimail-monitor). Free forever for basic reporting. Maps source IPs to vendor identities (Mailchimp, HubSpot, SendGrid) rather than asking you to WHOIS every ASN.
  • Cloudflare Email Security. Free for domains already using Cloudflare DNS. Adds a DMARC Management dashboard alongside your existing records.

Pick one. Set the rua= address for your DMARC record to the one it provides. Skim the first weekly email carefully. Skim subsequent ones in 30 seconds. That is the steady state.

What the report should drive you to change

Three and only three actions come out of reading DMARC reports:

  1. Add an SPF include when a legitimate sender shows spf=fail and you can identify the vendor.
  2. Publish DKIM records when a legitimate sender shows dkim=fail or dkim=none. The vendor's onboarding docs show which selector and value.
  3. Tighten policy once two consecutive weekly digests show every known source at pass: move p=none to p=quarantine at pct=25, then pct=100, then p=reject. The cadence is covered in the DMARC Academy entry.

Anything else the report seems to imply (blocklists, geo-restrictions, per-IP reputation) is outside DMARC's scope. Resist the temptation to over-engineer from an RUA file.

The one-paragraph version

Aggregate reports are daily ledgers of who sent mail as your domain, from which IP, and whether SPF and DKIM held up. Read a few by hand to understand the schema, then forward rua= to an aggregator and let it collapse the XML into a weekly digest. Act on three patterns: unknown high-volume passes (onboarding you missed), fail-fail from known senders (DNS you forgot to publish), and fail-fail from unfamiliar ASNs (spoofing, which tighter policy blocks). Ignore forwarders, mailing lists, and background noise. When two weeks look clean, tighten policy one step. Repeat until p=reject.


FAQ

Why do DMARC aggregate reports arrive as XML?

Because they are machine-to-machine feedback, not human documents. RFC 7489 specifies an XML schema so that any receiver can send a structured report and any aggregator can parse it. The format predates the expectation that humans would read it, which is why the raw files look intimidating.

Where do DMARC aggregate reports come from?

Every receiver that evaluates DMARC for your domain sends one daily summary to the address in your rua= tag. Google, Microsoft, Yahoo, Mail.ru, Comcast, ProtonMail, and hundreds of smaller providers all participate. Small domains get a handful per day, large senders get hundreds.

What is the difference between policy_evaluated and auth_results in a DMARC report?

auth_results shows the raw SPF and DKIM outcomes before alignment. policy_evaluated shows what DMARC actually decided after alignment, overrides, and your published policy were applied. When the two disagree, the DMARC-specific step (alignment with the header-From domain) is the reason.

Should I read the raw XML myself?

Not past the first couple of reports. Aggregators exist precisely to collapse thousands of records into a sources-and-verdicts table. Read two or three files by hand to build intuition, then forward rua= to Dmarcian, Valimail, Postmark DMARC, or Cloudflare Email Security and check the weekly digest.

What does a spike of fail/fail from an unknown ASN mean?

Most commonly, an active spoofing attempt. Less commonly, a legitimate sender you forgot about that runs from a hosting provider you do not recognise. Run the source IP through a WHOIS lookup. If the ASN belongs to a residential ISP, a bulletproof host, or a country where you have no business presence, treat it as hostile and tighten policy to at least p=quarantine.

Why are forwarders showing up with spf=fail?

Forwarding breaks SPF by design: the forwarder re-sends the message from its own IP, which is not in your SPF record. DKIM usually survives because the signature is attached to the message body. This is expected noise, not a misconfiguration. The ARC protocol (RFC 8617) is the forwarding-friendly alternative, and is something the forwarder has to implement, not you.


Want to know where your domain stands right now? Scan your domain. Sudory checks SPF, DKIM, DMARC, and 17 other DNS and HTTP controls in one pass. No login, no setup, about 3 seconds.

Aleksej Dix
Aleksej DixFounder of Sudory

Founder of Sudory. Frontend engineer based in Zurich with 20+ years shipping production web apps; now building continuous compliance scanning and writing about the DNS and email-auth controls behind it. Co-founder of WebZurich.