Bali, Indonesia
Insights

Reading DMARC Aggregate Reports Weekly to Catch Senders Failing Alignment

October 25, 2026
0:00
A domain publishes a DMARC record, the main mail server passes every check, and everyone assumes the job is done. Then invoices from a billing tool or replies from a helpdesk start landing in spam, and the first signal is a customer asking why they never got the email. The mail server was never the problem. A sender nobody thought about was failing alignment, and the evidence had been sitting in the DMARC aggregate reports for weeks. This article walks through what a receiving mail server computes when it evaluates DMARC, how that verdict turns into a row of XML, and how a short weekly read of those reports catches unaligned or unknown senders before they turn into tickets.
When a message arrives at a receiver that enforces DMARC, it runs two independent checks and then one comparison step. SPF checks whether the connecting IP is allowed to send for the envelope sender domain. That domain comes from the SMTP MAIL FROM command, which ends up in the Return-Path header. It is not the address the recipient sees. DKIM checks a cryptographic signature in the message headers. The signature carries a d= tag naming the signing domain, and the receiver fetches the public key from selector._domainkey.d-domain in DNS. Neither check looks at the visible From: header. That is the gap DMARC closes. After SPF and DKIM run, the receiver takes the domain from the From: header and asks a second question for each result: did it pass, and does its domain match the From domain? That match is called alignment. Alignment has two modes. Relaxed mode, the default, accepts a match on the organizational domain, so mail.example.com aligns with example.com. Strict mode requires the exact same domain. The aspf and adkim tags in the DMARC record choose the mode for SPF and DKIM separately. DMARC passes if at least one mechanism both passes and aligns. Then the receiver applies the published policy (none, quarantine, or reject) to messages that fail, and it records the outcome. Once a day by default, it bundles those outcomes per sending IP into an aggregate report and mails it to the address in the rua tag. The important detail is that the receiver keeps two separate records of the same evaluation: what SPF and DKIM said on their own, and what they meant for DMARC after alignment. The report contains both.
An aggregate report is a gzip or zip attachment containing one XML document. The top has report_metadata (who sent the report, the reporting period as Unix timestamps) and policy_published (the DMARC record the receiver saw). Then comes one record element per combination of source IP and authentication outcome. Illustrative example of one record, with placeholder domains and a documentation IP:
XML
<record>
  <row>
    <source_ip>203.0.113.25</source_ip>
    <count>40</count>
    <policy_evaluated>
      <disposition>none</disposition>
      <dkim>fail</dkim>
      <spf>fail</spf>
    </policy_evaluated>
  </row>
  <identifiers>
    <header_from>example.com</header_from>
  </identifiers>
  <auth_results>
    <dkim>
      <domain>crm-vendor.example</domain>
      <selector>s1</selector>
      <result>pass</result>
    </dkim>
    <spf>
      <domain>bounces.crm-vendor.example</domain>
      <result>pass</result>
    </spf>
  </auth_results>
</record>
Read it top to bottom. Forty messages came from 203.0.113.25 with example.com in the From header. Under policy_evaluated, both DKIM and SPF say fail. Under auth_results, both say pass. That looks contradictory until you remember the two layers. auth_results holds the raw results: DKIM verified a valid signature for crm-vendor.example, and SPF authorized the IP for bounces.crm-vendor.example. policy_evaluated holds the DMARC view: neither of those domains is example.com, so neither aligned, so DMARC failed. The disposition is none only because this domain still runs p=none. Switch the policy to quarantine and these forty messages go to spam.
Most of the useful signal in these reports comes from comparing those two blocks. A few combinations come up again and again. Raw pass, aligned fail, vendor domain in auth_results. This is the classic third-party sender that was never configured. The CRM or helpdesk sends on your behalf, signs with its own domain, and uses its own bounce domain. Mail is authenticated, just not as you. The fix usually lives in the vendor's settings: publish the DKIM key they give you so they sign with d=yourdomain, and if they support it, set a custom return-path subdomain so SPF aligns too. DKIM alignment alone is enough to pass DMARC, so that is typically the first thing to set up. SPF fail, DKIM pass and aligned, unfamiliar IP. Often this is forwarding. A recipient forwards mail from a university or old ISP address, the forwarder's IP is not in your SPF, but the DKIM signature survives the hop untouched. DMARC still passes. No action needed beyond recognizing the pattern. Both fail, DKIM signature with your domain but result of fail. Something modified the message after signing. Mailing lists that add footers or rewrite subjects are a common cause. Worth knowing about, rarely worth chasing. Both fail, no DKIM at all, IP with no relationship to you. That is either spoofing or a sender somebody in the company signed up for without telling anyone. Reverse DNS on the IP and a look at the volume usually tells you which. The first case is the one that bites. It looks fine at the transport level, every vendor dashboard shows delivered, and the only place the misalignment is visible is in these reports. For support staff, this is also the clearest way to answer a customer who says SPF passes so the problem must be the receiving side. The report shows SPF passing for a domain that is not theirs.
Nobody reads raw DMARC XML by hand for long. Daily reports from several receivers pile up fast. A weekly review only needs one answer: which senders failed DMARC this week, and how much mail did they send? The script below uses only the Python standard library. Point it at a folder of downloaded report attachments and it prints every source where neither SPF nor DKIM aligned, sorted by message count.
PYTHON
#!/usr/bin/env python3
# Summarize DMARC aggregate reports: show rows where neither SPF nor DKIM aligned.
# Usage: python3 dmarc_week.py reports/*.xml reports/*.gz reports/*.zip
import sys, gzip, zipfile
import xml.etree.ElementTree as ET
from collections import Counter

def load(path):
    if path.endswith('.gz'):
        with gzip.open(path) as f:
            return f.read()
    if path.endswith('.zip'):
        with zipfile.ZipFile(path) as z:
            return z.read(z.namelist()[0])
    with open(path, 'rb') as f:
        return f.read()

fails = Counter()
for path in sys.argv[1:]:
    root = ET.fromstring(load(path))
    # Strip XML namespaces so namespaced and plain reports parse the same way
    for el in root.iter():
        el.tag = el.tag.split('}')[-1]
    org = root.findtext('report_metadata/org_name', '?')
    for rec in root.iter('record'):
        pe = rec.find('row/policy_evaluated')
        if pe.findtext('dkim') == 'pass' or pe.findtext('spf') == 'pass':
            continue  # DMARC passed via at least one aligned mechanism
        ip = rec.findtext('row/source_ip')
        hfrom = rec.findtext('identifiers/header_from')
        dkim_d = ','.join(d.findtext('domain', '') for d in rec.findall('auth_results/dkim')) or '-'
        spf_d = rec.findtext('auth_results/spf/domain', '-')
        fails[(hfrom, ip, dkim_d, spf_d, org)] += int(rec.findtext('row/count', '0'))

for (hfrom, ip, dkim_d, spf_d, org), n in fails.most_common():
    print(f'{n:>6}  {hfrom}  {ip}  dkim_d={dkim_d}  spf_domain={spf_d}  reported_by={org}')
The dkim_d and spf_domain columns do most of the work. A vendor's domain showing up there next to your From domain is the unconfigured-sender case. A dash in dkim_d with an unknown IP points toward spoofing or shadow IT. The reported_by column helps when one receiver sees a pattern the others do not. It is deliberately small. If the volume of reports grows past what a folder and a script handle comfortably, a dedicated DMARC report processor or a hosted reporting service is the next step, but the logic is the same. When the review turns up a new source, check DNS before touching anything. These lookups are read-only:
BASH
# Current DMARC policy and where reports go
dig +short TXT _dmarc.example.com

# Whether the vendor's DKIM key is published under your domain (selector depends on the vendor)
dig +short TXT s1._domainkey.example.com

# If rua points to a different domain, that domain must authorize receiving your reports
dig +short TXT example.com._report._dmarc.reports.example.net
The last check matters more than it looks. If rua sends reports to a mailbox on another domain and that domain has not published the authorization record, many receivers will simply not send reports. An empty inbox can mean silence, not health.
DMARC failures are quiet by design. Under p=none, nothing is blocked, so a misconfigured sender keeps working and nobody notices. Under quarantine or reject, the failure is visible to the recipient but not to the sender, who sees delivered in their own logs. Either way, the reports are the only place the problem is written down. New senders also arrive without warning. Marketing adds a survey tool, finance switches invoicing platforms, someone connects a scheduling app to a shared address. Each one sends mail as your domain from day one. A weekly look at the failing-sources list catches them in the first week rather than after a policy change or a complaint. The habit is simple: pull the week's reports, run the summary, and for every row ask whether you know who that sender is. Known vendor with misalignment means fix their DKIM or return-path in their settings. Forwarding means leave it. Unknown means find out before you tighten the policy, because tightening it is exactly when an unknown legitimate sender starts losing mail.