Your first DMARC aggregate report lands in your inbox as a .xml.gz attachment with a filename like google.com!yourdomain.com!1759708800!1759795200.xml.gz. You open it expecting answers and get a wall of nested tags instead: source_ip, policy_evaluated, identifiers, none of it labeled in a way that tells you what to actually do.
That gap, a record full of real data you can’t act on, is why most domains sit at p=none indefinitely instead of advancing. This post covers how to actually read the XML, how to tell a legitimate sending source from a spoofer, and the specific rollout path from p=none to p=reject that doesn’t block your own mail along the way.
The two types of DMARC reports
Aggregate reports (rua) are daily summaries from each participating mailbox provider: which IPs sent mail claiming to be from your domain, how many messages, and whether each one passed or failed. These are the reports that matter for rollout decisions. They contain no message content, only authentication results and volume.
Forensic reports (ruf) cover individual failed messages in more detail. Most major providers, including Gmail, don’t send them at all, for recipient privacy reasons. Plan your rollout entirely around aggregate reports; treat forensic reports as a bonus if a provider sends them, not something to wait on.
Reading the raw XML, field by field
A single record inside an aggregate report looks like this:
<record>
<row>
<source_ip>198.51.100.24</source_ip>
<count>1482</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>yourstore.com</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>klaviyomail.com</domain>
<result>pass</result>
</dkim>
<spf>
<domain>klaviyomail.com</domain>
<result>pass</result>
</spf>
</auth_results>
</record>
Here’s what each piece actually tells you:
source_ip: the server that sent the mail. Cross-reference this against your known sending tools, not just your ESP; your helpdesk, review app, and invoicing tool all have their own IPs.count: how many messages came from that IP during the report window. Asource_ipyou don’t recognize sending 1 message is noise. One sending 10,000 is worth investigating immediately.policy_evaluated→disposition: what actually happened to the mail (none,quarantine, orreject), based on your published policy at the time.policy_evaluated→dkim/spf: whether each check passed, at the raw protocol level, before alignment is considered.identifiers→header_from: the domain the recipient saw, the one DMARC alignment is measured against.auth_results→dkim/spf→domain: the domain that actually signed (DKIM) or was authorized (SPF). Compare this toheader_from. If they don’t match, that’s an alignment failure, even whenpolicy_evaluatedshows a protocol-level pass.
That last comparison is the one raw XML makes hardest to see at a glance, and it’s the one that matters most. In the example above, DKIM and SPF both pass at the protocol level, but the record only tells you they’re aligned if auth_results’s domain matches identifiers’s header_from. Our common DMARC errors guide covers this exact alignment gap if you’re seeing it in your own reports.
Turning raw records into a decision
Once you’ve read enough individual records, the pattern that matters is which sources are sending, not each individual record. Group by source_ip or by the sending domain in auth_results, and you get something like this:
| Source | Emails | SPF aligned | DKIM aligned | Verdict |
|---|---|---|---|---|
| Klaviyo | 1,482 | No | Yes | Legitimate, passing on DKIM |
| Google Workspace | 640 | Yes | Yes | Legitimate |
| Unknown IP (203.0.113.25) | 210 | No | Yes | Likely forwarding |
| Unknown IP (192.0.2.77) | 320 | No | No | Investigate |
A DMARC report analyzer does this grouping automatically and usually names the service behind an IP range (Klaviyo, SendGrid, Google Workspace) instead of leaving you to look it up. Worth using once you have more than one or two sending sources, since reading raw XML by hand stops scaling fast.
Get your reports parsed and grouped automatically
Sending sources named by service, alignment failures tracked, alerts when your pass rate drops.
Telling a legitimate sender from a spoofer
Three patterns cover almost everything you’ll see once you’re reading reports regularly.
Known service, everything aligned. Your ESP, Google Workspace, your transactional provider. Nothing to do.
DKIM aligned, SPF failing, from an IP you don’t recognize. Usually forwarding, not spoofing. A recipient auto-forwards your mail to another address, which breaks SPF because the forwarding server isn’t in your SPF record, while the DKIM signature survives intact because the message content wasn’t altered. This is exactly why DKIM alignment matters more than SPF alignment when you’re deciding how worried to be about a given record.
Both failing, unfamiliar IPs, meaningful volume. Worth investigating, but check internally before assuming an attack. In practice, across client audits, it’s a marketing or helpdesk tool someone in the business signed up for without telling anyone about as often as it’s an actual spoofing attempt. A quick internal Slack message asking “did anyone add a new tool that sends email” resolves more of these than threat-hunting does.
The rollout path: p=none to p=reject
A Realistic Rollout Timeline
Stage 1: p=none
Publish v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com and wait. This delivers mail exactly as before, nothing is blocked, but it starts the reports flowing. Stay here for at least 2 to 4 weeks, and long enough to cover a full sending cycle for your business, not just a calendar minimum. Monthly or quarterly sends, invoicing, seasonal campaigns, a review-request flow triggered on a delay, won’t show up in two weeks of reports, and each one will fail the moment you enforce if you haven’t seen it pass first.
Target comfortably above 98% of legitimate volume passing and aligned before you consider moving on. If a known source is below that, fix its authentication and wait for the next report to confirm before advancing.
Stage 2: p=quarantine
Move to p=quarantine once your reports are clean. This is the safety net: failing mail goes to spam instead of being blocked outright, so a source your audit missed is recoverable, not lost. Watch for one to two weeks. If legitimate mail starts landing in spam during this window, that’s almost always a source that wasn’t passing alignment before enforcement started catching it, not a new problem. Check your aggregate reports from the days right after the policy change for any known source showing a fail or a non-aligned pass, fix that source’s authentication, and confirm it clears in the next report.
Stage 3: p=reject
Move to p=reject only once p=quarantine shows nothing legitimate failing. At this point, failing mail is blocked outright rather than delivered to spam, so there’s no more safety net if something was missed. This is also the point where BIMI becomes available if a logo in the inbox matters to you.
Under DMARCbis, if you want a softer intermediate step than the old percentage ramp, use t=y as a testing flag rather than a partial pct= value, which RFC 9989 deprecated.
The mistake that breaks this rollout
Jumping straight from p=none to p=reject, skipping p=quarantine entirely. It feels faster, but it removes the one stage where a missed source costs you spam placement instead of a bounced, unrecoverable send. Two steps, not one, is what makes this rollout safe rather than just fast.
Check where your domain’s policy actually stands
Free, no signup. Confirms your current policy and flags sources still failing before you advance it.
The bottom line
- Read
auth_resultsagainstidentifiers, not justpolicy_evaluated. That comparison is where alignment, the thing that actually determines pass or fail, actually lives in the XML. - Group records by source before reading them individually. The decision you need to make is about a sending source, not about any single record.
- DKIM-aligned, SPF-failing from an unfamiliar IP is usually forwarding, not an attack. Check both-failing sources internally before assuming the worst.
- Rollout in two steps:
p=quarantinebeforep=reject. Skipping the middle stage removes the only safety net in the process. - A full sending cycle, not two calendar weeks, is what “long enough at p=none” actually means. Quarterly and seasonal sends need to show up in reports before you enforce.

