Learn
How to read DMARC aggregate reports
DMARC aggregate reports tell you every server that sent mail as your domain and whether it passed. Here is what each part of the XML means, a worked example, and the readings that trip people up.
By Ahmet Karaca · Updated · Checked against RFC 9990 (DMARC aggregate reports) and RFC 9989 (DMARC, May 2026) on
What an aggregate report is
When your DMARC record has an rua tag, mailbox providers that support reporting send you a summary of the mail they received that claimed to be from your domain. One report covers one reporting period, usually a single UTC day, and one policy domain. It lists every IP address that sent mail as you during that period, how many messages came from each, and how SPF, DKIM and DMARC came out for them. No message content is included, only counts, domains and results.
The current specification is RFC 9990, published in May 2026 alongside RFC 9989, the new core DMARC standard. Together they replace RFC 7489, the 2015 document most existing guides still quote. RFC 9989 asks receivers to send reports at least once every 24 hours. Each report arrives as an email with the XML attached, either as plain .xml or compressed with gzip as .xml.gz, and the file name follows a fixed pattern: the receiver, your domain and the start and end of the period as Unix timestamps, separated by !. The subject line reads Report Domain: example.com Submitter: receiver.example, often followed by a report ID.
Expect one report per receiver per day, not one per day in total. A domain that sends to Gmail, Yahoo, Outlook.com and a handful of corporate mail servers can get a dozen reports a day, and a busy domain far more. Receivers are not obliged to report at all; RFC 9989 names resource limits, local policy and privacy as the usual reasons not to. So a quiet inbox does not prove that nobody sent mail as you.
The structure, element by element
The root element is feedback. Under it come three kinds of children in a fixed order: one report_metadata, one policy_published and one or more record elements. Everything interesting lives in the records.
report_metadata- Who sent the report (org_name, a contact email), a unique report_id and the date_range, given as begin and end in seconds since 1970, UTC. An optional error element lists problems the receiver found in your DMARC record.
policy_published- Your DMARC record as the receiver saw it: domain, p, and where present sp, np, adkim, aspf, fo and testing (the t tag). Tags you did not publish appear with their default value or not at all.
record- One per combination of sending IP, results and identifiers. A report has at least one, and the format sets no upper limit.
row- Inside each record: source_ip, count (the number of messages) and policy_evaluated, which holds the disposition the receiver applied and the DMARC-level dkim and spf results.
identifiers- header_from, the domain in the visible From address, which is the domain DMARC protects; envelope_from, the Return-Path domain SPF was checked against; and sometimes envelope_to.
auth_results- The raw results before DMARC: one dkim element per signature checked (domain, selector, result) and an spf element (domain, scope, result).
The single most important thing to understand is that a record carries two sets of results. The dkim and spf inside policy_evaluated answer the DMARC question: did this method pass and align with the From domain? The ones inside auth_results answer the plain question: did the method pass for whatever domain it was checked against? RFC 9990 calls the latter results "uninterpreted with respect to DMARC". Most confusion about reports comes from mixing the two up.
Reports you receive today come in two generations. Reports written to RFC 9990 declare the namespace urn:ietf:params:xml:ns:dmarc-2.0, may include a testing element and a discovery_method (psl or treewalk), must include the DKIM selector, and can use pass as a disposition for passing mail under an enforcing policy. Reports written to the older RFC 7489 schema often carry a pct element and may give forwarded or sampled_out as an override reason; both were dropped in the new format. A tool that reads reports has to accept both for the foreseeable future, because receivers update on their own schedules.
If you have not published a reporting address yet, the DMARC generator builds a record with one. Import your current record first and it keeps every address already in it.
An annotated example
Here is a short report of the kind a receiver would send for example.com after one day. Real reports are longer, but every one of them is built from exactly these pieces.
<feedback xmlns="urn:ietf:params:xml:ns:dmarc-2.0">
<version>1.0</version>
<report_metadata>
<org_name>receiver.example</org_name>
<email>dmarc-reports@receiver.example</email>
<report_id>2026-10-09_example.com@receiver.example</report_id>
<date_range>
<begin>1791504000</begin>
<end>1791590399</end>
</date_range>
</report_metadata>
<policy_published>
<domain>example.com</domain>
<p>none</p>
<adkim>r</adkim>
<aspf>r</aspf>
<testing>n</testing>
</policy_published>
<record>
<row>
<source_ip>192.0.2.10</source_ip>
<count>412</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.com</header_from>
<envelope_from>bounces.example.net</envelope_from>
</identifiers>
<auth_results>
<dkim>
<domain>example.com</domain>
<selector>s1</selector>
<result>pass</result>
</dkim>
<spf>
<domain>bounces.example.net</domain>
<scope>mfrom</scope>
<result>pass</result>
</spf>
</auth_results>
</record>
<record>
<row>
<source_ip>192.0.2.77</source_ip>
<count>3</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>fail</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.com</header_from>
<envelope_from>example.com</envelope_from>
</identifiers>
<auth_results>
<spf>
<domain>example.com</domain>
<scope>mfrom</scope>
<result>fail</result>
</spf>
</auth_results>
</record>
</feedback>The header
report_metadata says the report comes from receiver.example and covers the period from 1791504000 to 1791590399, which converts to October 9, 2026, from midnight to one second before the next midnight, UTC. The report ID is unique per report, so if the same file arrives twice you can drop the duplicate. policy_published shows that the receiver found p=none with relaxed alignment for both DKIM and SPF and testing mode off. If this does not match the record you think you published, stop here: the receiver saw something else, perhaps an old record still cached, perhaps a typo.
The first record: SPF passed, but not for you
412 messages came from 192.0.2.10. In auth_results, SPF passed, but for bounces.example.net, the envelope domain of the email service that sent them. That domain is not example.com or a subdomain of it, so in policy_evaluated SPF shows fail: it passed, but it is not aligned. DKIM saved the day. The signature used d=example.com with the selector s1, passed and aligned, so DMARC passed for all 412 messages. This is a healthy record for a service you set up with your own DKIM key. If you want SPF to align as well, many services offer a custom return path on a subdomain of yours.
The second record: nothing passed
Three messages came from 192.0.2.77 with example.com in both the From address and the envelope. SPF failed because the IP is not in the SPF record, and there is no dkim element at all in auth_results, which means the messages carried no DKIM signature the receiver could check. DMARC failed. The disposition is none only because the policy is p=none; under p=reject the receiver would have been asked to refuse them. Whether that is good news depends on who owns the IP, which is the next question to answer.
Readings people get wrong
"SPF passes, so why does DMARC fail?"
Because the SPF pass you are looking at is in auth_results, and it was for the envelope domain, which is often the email service's own. DMARC only counts SPF when that domain aligns with the From domain. Always read policy_evaluated first to see what DMARC decided, then auth_results to see why. The same applies to DKIM: a passing signature from d=esp.example.net does nothing for example.com. Why DMARC fails even though SPF passes covers the fixes.
Forwarded mail looks like spoofing
When a recipient forwards your message, through an alias, a university address or a mailing list, the next receiver sees it arrive from the forwarder's IP. SPF fails there, since that IP is not yours. If the forwarder left the message untouched, your DKIM signature still passes and DMARC passes with it. Mailing lists that add a footer or tag the subject often break the signature, and then the record shows a source you have never heard of with your From domain and both methods failing. Before you treat such a source as an attack, look at who owns the IP and whether DKIM ever passed from it. Some receivers recognise this case and say so with a reason element of type mailing_list or trusted_forwarder.
Counting recipients instead of messages
count is the number of messages the receiver handled for that row, not people and not campaigns. A newsletter to 10,000 Gmail users appears in Google's report as Google counted it, and the same send appears again, with different numbers, in every other receiver's report. Add counts across receivers for a picture of volume, but do not expect them to match your sending logs exactly.
Assuming the disposition is your policy
The disposition is what the receiver actually did, and it can differ from what you asked. RFC 9990 requires a reason element when a failing message was treated differently from your published policy. The defined types are local_policy, mailing_list, trusted_forwarder, policy_test_mode (your record has t=y) and other. Older reports may also show sampled_out, left over from the pct tag, which RFC 9989 retired.
Reading yesterday's policy
Reports lag. A report covers a past period and arrives after it closes, and if you changed your record during the period, RFC 9990 allows the receiver to report the old policy, the new one, or a mix under a single policy_published. Give a policy change a few days of reports before judging its effect. Also remember that mail rejected early in the SMTP conversation, for example on a strict SPF -all before the message content is seen, never reaches the report at all, as RFC 9989 points out.
From report to action
Almost every failing source falls into one of three groups, and each has a different fix.
- Your own service, not set up yet. A CRM, help desk or billing tool sending as you with its own envelope and DKIM domain. Publish the DKIM key it gives you under your domain and, if it offers one, a custom return path. Add it to SPF only if it sends with your domain in the envelope. The SPF checker shows whether one more include would cross the limit of ten DNS lookups.
- A forwarder. SPF fails but DKIM passes, or the source belongs to a known forwarding service. Usually nothing to fix on your side. Make sure every service signs with your domain, so DKIM can carry the mail through forwarding.
- A stranger. An IP with no relation to any service you use, no aligned DKIM ever, often small counts from many networks. That is what DMARC enforcement exists for.
When every legitimate source passes with alignment for a few weeks of reports, it is time to tighten the policy toward p=reject. Move to p=quarantine, keep reading reports, then to p=reject. Under RFC 9989 you can add t=y while testing a step, which asks receivers to apply one level less than the published policy. The DMARC checker confirms what your record says after each change, and What is DMARC? explains what each policy does. If you are doing this because of the mailbox providers' new rules, see the Gmail, Yahoo and Outlook sender requirements for what they actually ask.
Reading them with Patchkit
RFC 9990 itself notes that the XML is not meant to be read by hand in bulk. Patchkit gives you a reporting address for your domain and parses every report that arrives there, in both the old and the new format. Senders are grouped across all reports and receivers, and each one is labelled as authorized, misconfigured, a forwarder or unknown. Services we recognise, such as major email platforms, are shown by name with a setup guide for the exact fix. Each report keeps its own page with what the receiver did, the dispositions it applied and how DKIM and SPF alignment came out for each source.
Alerts appear in your dashboard when the reports show trouble: an unknown source sending a meaningful volume as you, one of your services suddenly passing alignment far less often than usual, or reports that stop arriving. A weekly summary email lists what happened. The free plan covers one domain; the pricing page lists what each plan includes. If you would rather start without an account, the domain health check shows in a few seconds whether your DMARC record asks for reports at all.