You published v=DMARC1; p=none; rua=mailto:... a few weeks ago, the XML started arriving, and now you are looking at your first summary. A large share of your own mail is marked fail. The obvious question is whether someone is forging your domain.
Almost certainly not. What you are looking at is the word fail used for two different things, and the tools that show you a single “failure rate” usually merge them. One of the two means a message was blocked. The other means a message was delivered normally, by a receiver that was perfectly happy with it. Telling them apart takes about a minute per row, and the rest of this article is that minute.
Two different things called “fail”
A DMARC aggregate report (the format is specified in RFC 9990) is not a list of messages. It is a list of groups: each <record> covers a batch of messages that arrived from the same source IP with the same authentication outcome, and <count> says how many messages that batch represents. Any percentage you see anywhere is weighted by that number.
Inside each record there are three layers, and they answer three different questions.
<record>
<row>
<source_ip>203.0.113.47</source_ip>
<count>128</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.com</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>example.com</domain>
<selector>s1</selector>
<result>pass</result>
</dkim>
<spf>
<domain>fwd.example.net</domain>
<result>pass</result>
</spf>
</auth_results>
</record>
<auth_results> — did the protocols verify at all? These are the raw outcomes, uninterpreted. Here SPF returned pass, but look at the domain it passed for: fwd.example.net, not example.com. DKIM verified a signature carrying d=example.com.
<policy_evaluated>/<spf> and /<dkim> — did they verify and line up with the From: domain? This is the DMARC view, and it has only two values: pass or fail. SPF verified for someone else’s domain, so from DMARC’s point of view it is fail. DKIM verified for your domain, so it is pass.
<policy_evaluated>/<disposition> — what did the receiver actually do? Three values: none (delivered), quarantine, reject.
The record above contains the word fail, and nothing whatsoever went wrong. DMARC passes when at least one aligned method passes — the aligned DKIM signature was enough — so the message was authenticated, delivered, and correct. If your dashboard charts an “SPF pass rate” and you watch that number, this record will worry you every single day for no reason.
The rule
Here is the whole thing, and it is worth keeping somewhere you can paste it.
A group of messages in an aggregate report is a real failure if and only if:
(A) the receiver blocked it —
dispositionisquarantineorreject; or(B) it was delivered (
dispositionisnone), the policy in force for that report wasp=none, and both raw results in<auth_results>are exactlyfail.Everything else belongs in your volume, not in your failures.
Case (A) needs no interpretation. The receiver made a decision and told you about it. It does not matter what the raw results were and it does not matter what your policy says: if disposition is quarantine or reject, mail of yours did not reach an inbox, and that deserves attention whether the cause is an attacker or a misconfigured vendor.
Case (B) is the one nobody writes down. The message was delivered, both protocols failed outright, and the only reason it landed in an inbox is that you have not asked anyone to stop it yet. That is latent spoofing: it looks harmless today and becomes a blocked message the day you move to enforcement.
Two details in case (B) are easy to get wrong.
The policy that matters is the one in that report, not the one in your DNS today. Each report carries the policy the receiver saw, in <policy_published>/<p>, and it describes a window that has already closed — usually the previous day. If you changed your record on Tuesday, Monday’s reports still describe Monday’s policy. Read p from the report you are holding.
Only the exact value fail counts. RFC 9990 allows several other values in <auth_results> — none, neutral, policy, temperror, permerror, and for SPF also softfail — and none of them is proof of anything. A temperror means the receiver could not complete the check. A softfail means your own record asked for indecision. Treating those as failures is how a monitoring tool invents problems that do not exist.
Why case (B) requires p=none exactly
This is the part that gets skipped, and skipping it inverts the meaning of the row.
Suppose your domain is already at p=quarantine, and a report shows a batch that failed both methods and was still delivered, disposition=none. Under a naive reading that is worse — the spoofing got through anyway. It is the opposite. You asked the receiver to quarantine that mail, and the receiver chose not to. RFC 9989 is explicit that final handling is always the receiver’s local decision; the protocol expresses a preference, not a command. A delivered message on an enforcing domain therefore means an override was applied: a local allowlist, a trusted-forwarder rule, or — on records that still carry the legacy pct tag, which RFC 9989 removed — a sampling decision.
Enforcement exists on that domain. The receiver looked at your policy and made an exception to it. That is a different situation from “no enforcement exists yet”, and it is not latent spoofing.
The same reasoning covers the third case: if you do not know which policy was published, case (B) does not apply at all. You cannot conclude “delivered only because enforcement was missing” without knowing whether enforcement was there.
Sometimes the receiver tells you why
When a receiver applies an override, it may say so. <policy_evaluated> can carry one or more <reason> elements, and RFC 9990 defines five types: local_policy, mailing_list, other, policy_test_mode, and trusted_forwarder. Some reporters add a free-text <comment> as well.
<policy_evaluated>
<disposition>none</disposition>
<dkim>fail</dkim>
<spf>fail</spf>
<reason>
<type>mailing_list</type>
</reason>
</policy_evaluated>
That is a receiver telling you, in the report, that it recognised a mailing list and decided not to hold your policy against it. It is the most useful field in the format, and it is optional — most records will not have it, but when it is there it saves you the investigation. Reporters also move more slowly than the specification: until 2026 the reference was RFC 7489, whose list of reasons also included forwarded and sampled_out, and you will still see both in the wild.
The three ordinary explanations
Most of the alarming volume comes from three situations.
Automatic forwarding. A user forwards their mail elsewhere, or an old alias still points offsite. The forwarding server sends from its own IP, which is not in your SPF record, so SPF no longer aligns with your domain. If the forwarder passes the message through untouched, the DKIM signature survives, DMARC passes on DKIM, and the message is delivered. In the report: <spf>fail</spf>, <dkim>pass</dkim>, disposition none. What to do: nothing. This is precisely what having two authentication methods is for.
Mailing lists. A list adds a subject prefix or a footer, which invalidates the DKIM signature over the modified body, and rewrites the envelope sender to its own domain, which moves SPF away from yours. Both methods fail alignment, and the message is delivered anyway because the receiver recognises the pattern. In the report: both fail, disposition none, sometimes with <reason><type>mailing_list</type>. What to do: nothing. The mechanism designed to carry authentication across this kind of rewrite is ARC (RFC 8617), and implementing it is the list’s job, not yours.
A vendor signing with its own domain. Your invoicing platform, CRM or newsletter tool sends on your behalf and signs with d=vendor.example, not d=example.com. Look at <auth_results>: the DKIM result is pass, but <domain> is the vendor’s. Raw success, no alignment, so <policy_evaluated>/<dkim> is fail. This one needs action — not urgent action, but real action. Ask the vendor for DKIM signing under your own domain (almost always a CNAME pointing at a selector they give you) and, if they send with an envelope sender in your domain, make sure their include: is in your SPF record. Until then that traffic has no aligned identifier at all, and it is exactly the traffic that will break on the day you enforce.
The records that are neither
There is a fourth shape, and it fits neither bucket cleanly: SPF fail, and no <dkim> element at all, because nothing signed the message.
The rule above does not count these as failures, deliberately. A missing DKIM element is not a DKIM failure — it is the absence of evidence on that side, and absence of evidence is not proof. But these records are not “fine” either, and they deserve their own queue, because on a domain at p=none there are exactly two explanations and they look identical in the report:
- someone is forging your domain, or
- a legitimate sender of yours has no authentication at all — a form plugin on a small VPS, a monitoring script, a departmental application nobody remembers deploying.
The report cannot tell you which. The source IP usually can. Work through this list before you raise your policy, because these are the messages that will start disappearing the moment you do.
When it really is spoofing
The signals, in the order they are worth checking:
- The source IP has no relationship to you. Not your provider, not your office, not a vendor you can name. This is the strongest signal there is, and the only one that stands on its own.
- Nothing passes, raw. No aligned DKIM signature, and no SPF pass for any domain you recognise.
- The sending infrastructure does not look like a mail system. No usable reverse DNS, hosting ranges rather than mail ranges, a source that appears once and never returns.
<header_from>is your domain while everything else belongs to someone else. That is the definition of the attack DMARC was built to describe.
There are deliberately no thresholds here. “More than X% from a single IP” is the kind of number that gets copied from article to article and tells you nothing about your own domain: a legitimate email service provider concentrates enormous volume on very few addresses. Judge sources, not percentages.
What to do with the number
Do not raise your policy to make a number go down. It is the wrong number, and raising the policy will not do what you expect it to.
Work in this order instead. Identify every source in your reports until none is unexplained — that is the real work, and on a domain of any age it takes weeks, not an afternoon. Fix alignment for the legitimate ones, starting with the vendors signing under their own domain. Then move the policy, and not before.
And know what happens to your dashboard when you do. A batch of latent spoofing at p=none is case (B): delivered, counted as a failure. At p=reject that same batch becomes case (A): rejected, still counted as a failure. Your failure count does not have to fall for enforcement to be working — the mail stopped reaching inboxes, which was the entire point. What should fall is the volume you cannot explain.
Aggregate reports summarise; they never contain the message itself. When one specific message is bothering you and you want to know what the receiving server actually concluded about it, paste its headers into our email header analyzer: it reads the receiver’s own declarations, needs no account, and keeps nothing.
