The first DMARC aggregate report for a new domain arrives a day or more after you publish the record. It comes from a receiver you do not control, on a schedule you cannot change. If the code that reads those reports has a bug, you find out a day late, and whatever it dropped is gone unless the sender happens to send it again.

Most of that pipeline can be tested today. I split it into three parts: the DNS half that decides whether anyone sends you a report at all, the mail half that carries the file to your code, and the parser. Everything below uses only Python's standard library. Section numbers refer to RFC 9990, which took over aggregate reporting from RFC 7489 in May 2026.

What you cannot speed up

Start with the part no test fixes. RFC 7489 had an ri tag that asked receivers for a reporting interval. It defaulted to 86400 seconds, and anything other than daily was "accommodated on a best-effort basis". RFC 9989, the new DMARC core spec, lists ri under "Tags Removed" and marks it historic in the IANA table. RFC 9990 describes "daily (or more frequent)" reports and says a reporting period will typically "encompass a single UTC day, beginning at 0000UTC".

So the earliest a real report can cover your new record is the UTC day you publish it, and it arrives after that day closes and the receiver gets round to sending. Nothing in your DNS changes that. Which receivers send first, and which never send at all, is outside the spec.

The DNS half: will anyone send at all

A receiver discovers the rua tag when it looks up your DMARC record for mail it received (§3.4). No rua, no reports.

If the rua address is in a different organisational domain from the one publishing the policy, section 4 adds a step. The receiver must look up a TXT record at <policy domain>._report._dmarc.<destination host> and find one that starts with v=DMARC1. If it does not, "the URI MUST be ignored". Nothing tells you this happened. You just get no reports.

That check runs in DNS, so you can run it yourself. This script reads a domain's DMARC record, and for each rua destination on another domain it queries the authorisation name through Google's DNS-over-HTTPS JSON API. It also queries a name that cannot exist under the destination, to see whether the destination publishes a wildcard, which section 4 explicitly allows (*._report._dmarc.example.com).

import json import sys import urllib.request def txt(name): url = f"https://dns.google/resolve?name={name}&type=TXT" with urllib.request.urlopen(url) as r: d = json.load(r) return d["Status"], [a["data"].strip('"') for a in d.get("Answer", []) if a["type"] == 16] def check(domain): _, records = txt(f"_dmarc.{domain}") dmarc = next(r for r in records if r.startswith("v=DMARC1")) tags = dict(p.strip().split("=", 1) for p in dmarc.split(";") if "=" in p) uris = [u for u in tags.get("rua", "").split(",") if u.strip()] if not uris: print("no rua tag: nobody will send you aggregate reports") for uri in uris: host = uri.strip().split("@", 1)[1] # crude org-domain test: last two labels; good enough for .com/.org/.net  if host.split(".")[-2:] == domain.split(".")[-2:]: print(f"{uri.strip()}: same organisation, no check needed") continue status, ok = txt(f"{domain}._report._dmarc.{host}") _, wild = txt(f"zz-not-a-real-name.invalid._report._dmarc.{host}") print(f"{uri.strip()}: authorised={any(r.startswith('v=DMARC1') for r in ok)} " f"(status {status}), wildcard={bool(wild)}") for d in sys.argv[1:]: print(f"== {d}") check(d) 

Run on 13 October 2026 at 07:41 UTC:

== mozilla.org mailto:dmarc_agg@vali.email: authorised=True (status 0), wildcard=True mailto:dmarc@mozilla.com: authorised=True (status 0), wildcard=False == wikipedia.org mailto:dmarc-rua@wikimedia.org: authorised=True (status 0), wildcard=False == stackoverflow.com mailto:dmarc-aggregates@stackoverflow.com: same organisation, no check needed == example.com no rua tag: nobody will send you aggregate reports 

status 0 is NOERROR. Two patterns show up. The hosted reporting address answers for a made-up name too, so it publishes a wildcard and any domain pointing at it passes. Two other hosted report addresses I queried the same morning behaved the same way. The organisations that receive their own reports on a sister domain publish one record per domain: mozilla.org._report._dmarc.mozilla.com exists, and the made-up name under mozilla.com came back NXDOMAIN.

The second pattern is the one to test if you run your own report mailbox. Add a new sending domain, forget its record on the receiving domain, and that domain's reports stop before they start. The organisational-domain comparison in the script is the crude last-two-labels version. RFC 9989 uses a DNS tree walk, so replace it before you trust the script on something like a .co.uk domain.

The mail half: build the message a receiver would send

Section 3.5.2 is specific about the envelope. The XML SHOULD be gzipped and sent as application/gzip (text/xml if it is not compressed). The attachment filename follows an ABNF: receiver!policy-domain!begin!end[!unique-id].xml[.gz]. The subject follows another: Report Domain: <domain> Submitter: <receiver>, with an optional Report-ID: after it. A re-sent report MUST use the same filename as the original.

That is enough to build a realistic message locally. This half of the script writes a report in the RFC 9990 namespace, gzips it and attaches it to an EmailMessage the way a receiver would:

"""Build an RFC 9990 DMARC aggregate report, mail it to yourself in memory, and push it through the same ingest path a real report would take. Standard library only. Run: python3 dmarc_agg_test.py """ import gzip import re import xml.etree.ElementTree as ET from email import message_from_bytes, policy from email.message import EmailMessage NS = "urn:ietf:params:xml:ns:dmarc-2.0" def build_report(policy_domain, receiver, begin, end, rows, ns=NS, with_selector=True): q = (lambda t: f"{{{ns}}}{t}") if ns else (lambda t: t) if ns: ET.register_namespace("", ns) def sub(parent, tag, text=None): el = ET.SubElement(parent, q(tag)) if text is not None: el.text = str(text) return el root = ET.Element(q("feedback")) sub(root, "version", "1.0") md = sub(root, "report_metadata") sub(md, "org_name", receiver) sub(md, "email", f"dmarc-noreply@{receiver}") report_id = f"{begin}-{policy_domain}@{receiver}" sub(md, "report_id", report_id) dr = sub(md, "date_range") sub(dr, "begin", begin) sub(dr, "end", end) pp = sub(root, "policy_published") sub(pp, "domain", policy_domain) sub(pp, "p", "none") for ip, count, dkim, spf in rows: rec = sub(root, "record") row = sub(rec, "row") sub(row, "source_ip", ip) sub(row, "count", count) pe = sub(row, "policy_evaluated") sub(pe, "disposition", "none") sub(pe, "dkim", dkim) sub(pe, "spf", spf) ids = sub(rec, "identifiers") sub(ids, "header_from", policy_domain) ar = sub(rec, "auth_results") d = sub(ar, "dkim") sub(d, "domain", policy_domain) if with_selector: sub(d, "selector", "s1") sub(d, "result", dkim) s = sub(ar, "spf") sub(s, "domain", policy_domain) sub(s, "result", spf) return report_id, ET.tostring(root, xml_declaration=True, encoding="utf-8") def wrap_in_email(xml_bytes, policy_domain, receiver, begin, end, report_id, rua): msg = EmailMessage() msg["From"] = f"dmarc-noreply@{receiver}" msg["To"] = rua msg["Subject"] = (f"Report Domain: {policy_domain} Submitter: {receiver} " f"Report-ID: <{report_id}>") msg.set_content("DMARC aggregate report attached.") filename = f"{receiver}!{policy_domain}!{begin}!{end}.xml.gz" msg.add_attachment(gzip.compress(xml_bytes), maintype="application", subtype="gzip", filename=filename) return msg.as_bytes() 

Building the whole message, and not just the XML, means the ingest code gets exercised from the same entry point a real report hits: subject, attachment, filename, decompression, then parsing.

The parser, and three cases to test it against

Here is the ingest side:

FILENAME = re.compile( r"^(?P<receiver>[^!]+)!(?P<domain>[^!]+)!(?P<begin>\d+)!(?P<end>\d+)" r"(?:![A-Za-z0-9]+)?\.(?P<ext>xml|xml\.gz)$") SUBJECT = re.compile(r"^Report\s+Domain:\s+(\S+)\s+Submitter:\s+(\S+)" r"(?:\s+Report-ID:\s+(\S+))?$") def local(tag): return tag.rsplit("}", 1)[-1] def kids(el, name): return [c for c in el if local(c.tag) == name] def one(el, name): found = kids(el, name) if len(found) != 1: raise ValueError(f"<{local(el.tag)}> needs exactly one <{name}>, has {len(found)}") return found[0] def parse_report(xml_bytes): root = ET.fromstring(xml_bytes) if local(root.tag) != "feedback": raise ValueError(f"root is <{local(root.tag)}>, not <feedback>") ns = root.tag[1:].split("}")[0] if root.tag.startswith("{") else None warnings = [] md = one(root, "report_metadata") dr = one(md, "date_range") pp = one(root, "policy_published") out = { "namespace": ns, "org": one(md, "org_name").text, "report_id": one(md, "report_id").text, "begin": int(one(dr, "begin").text), "end": int(one(dr, "end").text), "domain": one(pp, "domain").text, "p": one(pp, "p").text, "rows": [], } records = kids(root, "record") if not records: raise ValueError("no <record>; RFC 9990 requires at least one") for rec in records: row = one(rec, "row") pe = one(row, "policy_evaluated") for d in kids(one(rec, "auth_results"), "dkim"): if not kids(d, "selector"): warnings.append("dkim result without <selector> (required since RFC 9990)") out["rows"].append((one(row, "source_ip").text, int(one(row, "count").text), one(pe, "dkim").text, one(pe, "spf").text)) return out, warnings seen = set() def ingest(raw_message): msg = message_from_bytes(raw_message, policy=policy.default) m = SUBJECT.match(" ".join(str(msg["Subject"]).split())) if not m: return f"skip: subject does not match RFC 9990: {msg['Subject']!r}" for part in msg.iter_attachments(): name = part.get_filename() or "" fn = FILENAME.match(name) if not fn: return f"skip: attachment name does not match RFC 9990: {name!r}" data = part.get_content() if fn["ext"] == "xml.gz": data = gzip.decompress(data) report, warnings = parse_report(data) key = (fn["receiver"], report["report_id"]) if key in seen: return f"duplicate, ignored: {key}" seen.add(key) return report, warnings return "skip: no attachment" 

The namespace

RFC 9990 reports put every element in urn:ietf:params:xml:ns:dmarc-2.0. The schema in RFC 7489 declared a different target namespace, http://dmarc.org/dmarc-xml/0.1. ElementTree's find("report_metadata") only matches the un-namespaced name, so on an RFC 9990 report it returns None, and a parser written that way sees an empty report and moves on. Matching on the local name works whichever namespace a report carries, or none.

Re-sent reports

Section 3.5.4 leaves duplicate handling to the report consumer but requires senders to reuse their identifiers when they re-send. Key your store on the receiver and the report_id, and a second copy becomes a no-op instead of doubled counts.

Old-format reports

RFC 9990 made <selector> required inside a DKIM result (its Appendix C: "Selector is now required when reporting a DKIM signature"). Receivers ...