Skip to content

The publication for web craftspeople Tuesday, 1 September 2026

Email & deliverability

DMARC: going from p=none to p=reject without blocking legitimate email

Publishing a DMARC record at p=none protects nothing: real protection starts when the policy moves to quarantine and then to reject. Aggregate reports are the compass that makes that move safe, as long as they are read correctly.

A domain can display flawless SPF, DKIM and DMARC and still be, in practice, as spoofable as one with no authentication at all. The reason is three characters long: p=none. As long as the policy keeps that value, receiving servers report abuse without ever blocking it. Tightening toward quarantine and then reject is the only step that truly protects, and aggregate reports are what makes that step possible without accidentally sending legitimate mail to the junk folder.

What p=none protects, and what it does not

DMARC (RFC 7489) adds two guarantees on top of SPF and DKIM. The first is alignment: the domain shown in the From: header must match the domain validated by SPF or by DKIM. The second is a published policy telling the receiving server what to do with a non-aligned message. That policy is carried by the p tag.

With p=none, the instruction sent to receivers is explicit: observe, but do not block. A message spoofing the domain therefore passes exactly as it did before DMARC was installed. The only gain at this stage is visibility: the domain starts receiving reports describing who sends in its name. That collection step is essential, but mistaking it for protection is the most common error in deliverability.

Anatomy of an aggregate report

An aggregate report (the rua tag) is an XML file that a receiving server generates, by default once a day, for a given domain. It contains no message content: only source IP addresses, volumes, and the SPF, DKIM and DMARC evaluation results. Here is an excerpt reduced to the essentials.

<record>
  <row>
    <source_ip>203.0.113.24</source_ip>
    <count>148</count>
    <policy_evaluated>
      <disposition>none</disposition>
      <dkim>fail</dkim>
      <spf>pass</spf>
    </policy_evaluated>
  </row>
  <identifiers>
    <header_from>exemple.fr</header_from>
  </identifiers>
  <auth_results>
    <spf><domain>mailer.prestataire.com</domain><result>pass</result></spf>
  </auth_results>
</record>

This block describes 148 messages sent from a single IP, presenting themselves as exemple.fr. SPF passes, but on the domain mailer.prestataire.com: there is authentication, not alignment. DKIM fails. DMARC result: not aligned. Under p=none the disposition stays none and the message goes through anyway. Under p=reject, those 148 messages would be refused. The whole question is whether they are legitimate.

XML fieldWhat it revealsUse in diagnosis
source_ipThe real sending addressIdentify the sending service
countThe volume over the periodPrioritise high-volume flows
policy_evaluated/dkim|spfThe alignment DMARC keptSpot alignment failures
header_fromThe displayed domainConfirm identity spoofing
auth_resultsThe domain actually authenticatedTell a legitimate provider from fraud

An aggregate report does not say whether an email arrived: it says who is allowed to write in the domain’s name, and who steps outside that.

Reading the reports to find legitimate senders that fail

By hand, the reports become unreadable past a few providers. The useful reflex is not to inspect everything, but to isolate one category: non-aligned, high-volume sources. Those are the ones that will break on tightening if they are legitimate, or that prove fraud if they are not. A few lines of script are enough to extract that list from a decompressed report.

import xml.etree.ElementTree as ET

tree = ET.parse("rapport_agrege.xml")
for rec in tree.findall(".//record"):
    dkim = rec.findtext("row/policy_evaluated/dkim")
    spf = rec.findtext("row/policy_evaluated/spf")
    if dkim != "pass" and spf != "pass":          # non aligné sur les deux
        ip = rec.findtext("row/source_ip")
        count = rec.findtext("row/count")
        frm = rec.findtext("identifiers/header_from")
        print(f"{count:>6}  {ip:

Each output line is a decision to make: does this IP belong to a service used legitimately (marketing router, billing, CRM, site form) or to a third party spoofing the domain? For the former, the answer is to fix their SPF or enable their DKIM signature before tightening. For the latter, tightening is precisely the goal: they will be blocked.

Ramping up: from none to reject

The move is never made in one jump. It follows a ramp, where each step is held long enough for an aggregate report to confirm the absence of any legitimate sender in failure. The pct tag historically applies the policy to only a fraction of messages, to observe the effect before generalising it.

; palier intermédiaire : quarantine sur un quart du trafic
_dmarc.exemple.fr.  IN  TXT  "v=DMARC1; p=quarantine; pct=25; rua=mailto:[email protected]; adkim=s; aspf=s"
PolicyReceiver actionWhen to adopt it
p=noneNo blocking, reports onlyInitial collection, inventory of sources
p=quarantineMessage filed as spamAll legitimate sources are aligned
p=rejectMessage refused at deliveryNo legitimate failure across several report cycles

A realistic pace holds each step one to two weeks: p=none, then p=quarantine; pct=25, then pct=100, then p=reject. The ongoing revision of the specification tends to discourage pct in favour of a gradual rollout by subdomains and a distinct policy through the sp tag; the principle stays the same, only the mechanics of the step change.

Failure reports (ruf) and feedback loops

Alongside aggregate reports, the ruf tag requests failure reports, message by message, in ARF format (RFC 5965). They are more precise, but rarely sent: most large receivers suppress them so as not to pass on personal data. Relying on them means giving up visibility.

Watch point. Failure reports can contain headers, or even excerpts of real messages. Pointing ruf at a shared mailbox or an analysis provider exposes that data. Day-to-day deliverability monitoring is better served by the large operators' postmaster dashboards and by aggregate reports, which carry no content.

Key takeaways

DMARC only protects from quarantine onward. The path to reject is a matter of reading, not configuration: each step waits for an aggregate report to confirm that no legitimate source is dropping. The sequence is always the same: collect at none, align the real senders spotted in the reports, tighten in steps, verify, refuse. A domain left for months at p=none is not being secured: it is on hold.

On the domains I work with, the blocker is almost never technical: it is the fear of breaking some forgotten flow. My rule has become simple: I never tighten a step without two clean cycles of aggregate reports in front of me. That discipline has saved me more than one Monday morning spent working out why the invoices stopped going out. Tightening DMARC is not risky in itself; doing it blind is. — Simon Janvier

Going further

DMARC specification and the aggregate report schema: RFC 7489 (IETF).

Read next