Deliverability

How to actually read a DMARC aggregate report

A DMARC aggregate report is gzipped XML from every mailbox provider you send to. What its three blocks mean, and how to read the record rows.

By Dana Okafor · · Updated

A DMARC aggregate report is a gzipped XML file one mailbox provider sends you, usually once a day, listing every IP address that sent mail claiming your domain in that window, how many messages it sent, and whether each one passed SPF and DKIM in alignment with your domain. Publish a rua= tag in your DMARC record and Google, Yahoo, Microsoft and a long tail of smaller providers each start mailing you one of these, daily, on their own schedule. None of them are meant to be opened as XML by a person, but the structure underneath is small enough that you don’t need a parser to read one.

Key Takeaways

  • A report has three blocks: report_metadata (who sent it, and the window it covers), policy_published (the DMARC policy they checked your mail against), and one record per sending source.
  • Each record’s policy_evaluated gives the simplified, DMARC-aligned pass or fail. Its auth_results gives the raw SPF and DKIM verdict underneath, and the two frequently disagree.
  • A source you don’t recognize that authenticates cleanly under someone else’s domain, at a steady volume, is a forgotten sender to fix, not a spoofer to worry about.
  • Before moving from p=none to p=quarantine, every legitimate source needs to align on at least one of SPF or DKIM, checked over more than one day.

The three blocks inside every report

Every aggregate report is one <feedback> document, defined in RFC 7489, Appendix C. It has three parts.

report_metadata says who sent the report and when. org_name and email identify the mailbox provider, report_id is a unique identifier for that report, and date_range gives the window it covers as two Unix timestamps, begin and end, in seconds since the epoch.

policy_published is what the provider found when it looked up your DMARC record: the domain it checked, the alignment mode for DKIM and SPF (adkim and aspf, each either r for relaxed or s for strict), the policy itself (p, and sp for subdomains), the pct of mail it applies to, and fo, the failure-reporting option you published. SPF, DKIM and DMARC without the acronym soup covers what relaxed and strict alignment actually mean and why almost everyone starts at p=none.

record is repeated once per sending source. Each one has a row (the source IP, a count of matching messages, and policy_evaluated, the DMARC-aligned verdict), identifiers (the domain in the visible From: header and the one in the envelope sender), and auth_results (the raw SPF and DKIM outcome, before DMARC alignment is applied to it).

That last distinction is the one worth sitting with. policy_evaluated only has two possible values for dkim and spf: pass or fail, meaning “aligned and passing” or not. auth_results carries the full underlying result exactly as SPF and DKIM produced it (pass, fail, neutral, none, temperror or permerror, plus softfail for SPF and policy for DKIM), with no opinion yet about whether the domain matches yours. A single row can show auth_results cleanly passing and policy_evaluated failing at the same time. That gap is where a forgotten sender and a forwarded message live.

An annotated aggregate report

Here’s a trimmed but schema-complete report for a domain called example.com, with three rows, one for each case below.

<?xml version="1.0"?>
<feedback>
  <version>1.0</version>
  <report_metadata>
    <org_name>google.com</org_name>
    <email>noreply-dmarc-support@google.com</email>
    <report_id>9391772351622218855</report_id>
    <date_range>
      <begin>1793491200</begin> <!-- 2026-11-01 00:00:00 UTC -->
      <end>1793577599</end>     <!-- 2026-11-01 23:59:59 UTC -->
    </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>
    <fo>0</fo>
  </policy_published>

  <!-- Row 1: a real service you forgot to authenticate -->
  <record>
    <row>
      <source_ip>198.51.100.77</source_ip>
      <count>340</count>
      <policy_evaluated>
        <disposition>none</disposition>
        <dkim>fail</dkim>
        <spf>fail</spf>
      </policy_evaluated>
    </row>
    <identifiers>
      <header_from>example.com</header_from>
      <envelope_from>mailrelay.example.net</envelope_from>
    </identifiers>
    <auth_results>
      <dkim>
        <domain>mailrelay.example.net</domain>
        <selector>s1</selector>
        <result>pass</result>
      </dkim>
      <spf>
        <domain>mailrelay.example.net</domain>
        <scope>mfrom</scope>
        <result>pass</result>
      </spf>
    </auth_results>
  </record>

  <!-- Row 2: your own mail, forwarded by someone who received it -->
  <record>
    <row>
      <source_ip>192.0.2.18</source_ip>
      <count>3</count>
      <policy_evaluated>
        <disposition>none</disposition>
        <dkim>fail</dkim>
        <spf>fail</spf>
        <reason>
          <type>forwarded</type>
          <comment>Local heuristics identified this as a forwarded message.</comment>
        </reason>
      </policy_evaluated>
    </row>
    <identifiers>
      <header_from>example.com</header_from>
      <envelope_from>example.com</envelope_from>
    </identifiers>
    <auth_results>
      <dkim>
        <domain>example.com</domain>
        <selector>efig1</selector>
        <result>fail</result>
      </dkim>
      <spf>
        <domain>example.com</domain>
        <scope>mfrom</scope>
        <result>fail</result>
      </spf>
    </auth_results>
  </record>

  <!-- Row 3: nobody you sent to, with no valid signature at all -->
  <record>
    <row>
      <source_ip>198.51.100.201</source_ip>
      <count>1</count>
      <policy_evaluated>
        <disposition>none</disposition>
        <dkim>fail</dkim>
        <spf>fail</spf>
      </policy_evaluated>
    </row>
    <identifiers>
      <header_from>example.com</header_from>
      <envelope_from>random-relay.example.net</envelope_from>
    </identifiers>
    <auth_results>
      <spf>
        <domain>random-relay.example.net</domain>
        <scope>mfrom</scope>
        <result>pass</result>
      </spf>
    </auth_results>
  </record>
</feedback>

Every disposition reads none because the published policy is p=none: nothing gets blocked at this stage regardless of what a row shows, which is the entire point of starting there. What separates the three rows is everything else in them.

A legitimate sender that fails alignment

Row 1 authenticates perfectly, just not for you. auth_results shows both SPF and DKIM passing cleanly for mailrelay.example.net, a real domain with a real signing key and a real SPF record of its own. What fails is alignment: neither the SPF domain nor the DKIM domain is example.com or a subdomain of it, so policy_evaluated marks both fail and DMARC counts the whole message as unauthenticated, even though nothing about it was forged.

This is what a forgotten sender looks like in the data: a steady count across the reporting window, from the same IP or IP range, under a domain name that’s usually recognizable once you see it, a marketing tool, a helpdesk, a billing system. The fix isn’t authentication in the abstract, it’s alignment specifically. Either add that service’s return-path domain to your SPF record, or set up the custom DKIM signing most providers offer so they sign as a subdomain of yours instead of their own domain. Your DNS records, one at a time covers what each of those records actually does at the DNS level.

Mail that got forwarded

Row 2 is your own mail, sent correctly, that failed both checks anyway. header_from and envelope_from both read example.com, meaning this genuinely was your message. But it arrived at the reporting mailbox provider from 192.0.2.18, an IP you never sent from, because someone who received it forwarded it onward. Forwarding often breaks SPF outright, since the receiving server checks the forwarding host’s IP against your SPF record, not the original sender’s. It can also break DKIM if the forwarder alters the message, adding a footer or a subject prefix invalidates the signature, since DKIM signs the message content.

The reason element is what tells you this wasn’t a spoof: type reads forwarded, meaning the reporting provider’s own heuristics recognized the pattern and are telling you not to read this row as an attack. Not every provider includes it, and its absence doesn’t mean a failure is real. But when it’s there, it’s the one thing in the report that distinguishes “this broke because of how mail routing works” from “this broke because someone else sent it.” Either way, there’s no DNS record that fixes a forward. If the original DKIM signature would have survived an unmodified hop, that’s already the best you can do.

A spoofed message

Row 3 looks unremarkable at a glance and is the one worth the closest read. auth_results shows SPF passing, but for random-relay.example.net, a domain with no relationship to example.com and none of the volume history a real vendor builds up. There’s no dkim element at all, meaning no DKIM signature was present to evaluate. The count is 1, not part of a pattern that repeats day to day.

That combination, an unfamiliar envelope domain, no DKIM signature, a single message or a handful, no reappearance in the next report, is what someone sending mail as your domain without your permission produces. It’s the row p=quarantine and p=reject exist to stop, and it’s exactly what a domain sitting at p=none indefinitely leaves fully exposed to.

Frequently asked questions

How often do mailbox providers send DMARC aggregate reports?

Daily is the default and the floor. RFC 7489 says implementations must be able to provide daily reports and should be able to provide hourly ones when requested, which is handled on a best-effort basis. Each provider sends you one report per reporting window, covering every source it saw.

What does policy_evaluated mean in a DMARC aggregate report?

policy_evaluated is the simplified, DMARC-aligned verdict for a record: pass or fail, nothing else. That’s a different question from auth_results, which carries the raw SPF and DKIM outcome underneath, and the two can disagree on the same row: a message can pass SPF or DKIM outright and still fail policy_evaluated because the passing domain doesn’t align with the one in your From header.

What’s the difference between DMARC’s p=none and p=quarantine policies?

p=none asks receiving mail servers to report on authentication failures without acting on them, so nothing you publish there ever blocks or folders a message. p=quarantine asks receivers to treat a failing message as suspicious, typically routing it to spam, which is why you only move to it once you’ve confirmed every legitimate source in your reports is aligned.

Do Gmail and Yahoo require DMARC p=reject?

No. Both accept p=none, the weakest setting, for bulk senders (Google’s sender guidelines and Yahoo’s sender best practices). Moving to p=quarantine or p=reject protects your domain from being spoofed, but it isn’t a bar either provider requires you to clear.

What does the pct tag do in a DMARC record?

pct sets what percentage of mail a stricter policy applies to, letting you test a policy change on a slice of your traffic instead of committing all of it at once. Publishing p=quarantine; pct=25, for example, applies quarantine to a quarter of matching mail and leaves the rest under the previous behavior, so a mistake in reading your reports affects only that slice.

What to check before moving to p=quarantine

Read more than one day’s report before deciding anything; a service that mails weekly or monthly won’t show up in a single day’s window at all. Once you have a few reports:

  • Every legitimate source authenticates and aligns, policy_evaluated shows pass, not just auth_results. A row that passes raw SPF or DKIM but fails alignment is still going to be quarantined once you tighten the policy.
  • Every unaligned legitimate source has been fixed, an SPF include added, or DKIM signing moved under your domain, and shown up passing in a subsequent report, not just changed in your DNS.
  • Rows carrying a reason of forwarded are read as forwarding, not failure, and left alone; there’s no record to add for them.
  • Use pct to phase the stricter policy onto a slice of your mail first, so a bad read of the reports costs you less than committing all of it at once.

Gmail, Yahoo and Microsoft’s bulk sender rules covers what each provider actually requires your DMARC policy to be, which for all three is p=none at minimum, not p=reject. Tightening further protects your domain from being spoofed; it isn’t a bar you need to clear to keep sending. On EmailFig, the DMARC record you publish is v=DMARC1; p=none; with no rua= address, so no reports arrive until you add one at your DNS provider, for example rua=mailto:dmarc-reports@example.com. The DNS records we generate walks through all of them.

Keep reading

All posts →