DMARC reports, explained

DMARC (Domain-based Message Authentication, Reporting, and Conformance) is an email authentication protocol that domain owners use to reduce phishing and spoofing. A domain owner publishes a DMARC record in DNS that declares which authentication methods — SPF and DKIM — receiving mail servers should check, and tells them what to do with messages that fail those checks: quarantine or reject.

DMARC also includes a reporting mechanism. Domain owners can receive aggregate XML reports from receiving providers detailing which messages passed or failed authentication checks and where those messages came from. That data is the raw material for DMARC management: configuring policies, monitoring authentication activity, adjusting the policy or authentication setup as email infrastructure changes, and coordinating with third-party vendors that send mail on your behalf.

An example DMARC record looks like:

v=DMARC1; p=reject; rua=mailto:[email protected]

This says the record uses DMARC version 1, unauthenticated mail should be rejected, and aggregate reports should be sent to [email protected].

Cloudflare's DMARC management service receives those report emails, parses the XML, and visualizes the results in the dashboard. It is built almost entirely on Cloudflare Workers.

Email Workers as the ingestion point

The service depends on Email Workers, which grew out of Email Routing. An Email Worker is a Workers script that handles incoming email instead of forwarding it to a mailbox. Rules and verified addresses are configured through the Email Routing API. A minimal Email Worker:

export default {
  async email(message, env, ctx) {
    const allowList = ["[email protected]", "[email protected]"];
    if (allowList.indexOf(message.headers.get("from")) == -1) {
      message.setReject("Address not allowed");
    } else {
      await message.forward("inbox@corp");
    }
  }
}

The Cloudflare edge runs Email Routing and the underlying workerd runtime in every datacenter, so incoming mail lands on a node that can process it. When Email Routing matches a message to a Worker rule, it streams the full email body into the Worker via an internal RPC, defined in a Capnproto schema. If the script wants to forward the message, it calls back into Email Routing using a capability included in the original request.

## Architecture of the DMARC report processor The DMARC report processor needs to do three things with each inbound message: 1. Extract the report from the email. 2. Publish the report's findings to an analytics platform. 3. Store the raw report for future re-processing. Each step uses a different piece of the Workers platform. ### Validate and route the recipient Every DMARC report email comes to the address declared in the domain's rua parameter. The Worker looks up the "to" address of the incoming SMTP message and treats it as the key for the report. That address is checked against information stored in Workers KV, which holds per-domain configuration for all the zones using the service. If the address is not recognized, the Worker stops. ### Parse the email and archive the report The Worker buffers the email stream into an arrayBuffer. Very large report emails may hit memory limits on the free Workers plan; the team recommends the Workers Unbound resource model for bigger payloads. Parsing the email means parsing its MIME structure. One option is postal-mime. Once the MIME parts are available, the attachments are DMARC reports, usually compressed. They are first stored in R2 in their original form via a simple put() on the R2 binding, organized in time-based directories so files are easy to retrieve later. The report attachments come as XML, often compressed with zlib or ZIP. The MIME type announces the compression method, which determines the decompression library: pako for Zlib, unzipit for ZIP. Decompressed XML is parsed with fast-xml-parser. A DMARC report in XML looks like:
<feedback>
  <report_metadata>
    <org_name>example.com</org_name>
    <[email protected]</email>
   <extra_contact_info>http://example.com/dmarc/support</extra_contact_info>
    <report_id>9391651994964116463</report_id>
    <date_range>
      <begin>1335521200</begin>
      <end>1335652599</end>
    </date_range>
  </report_metadata>
  <policy_published>
    <domain>business.example</domain>
    <adkim>r</adkim>
    <aspf>r</aspf>
    <p>none</p>
    <sp>none</sp>
    <pct>100</pct>
  </policy_published>
  <record>
    <row>
      <source_ip>192.0.2.1</source_ip>
      <count>2</count>
      <policy_evaluated>
        <disposition>none</disposition>
        <dkim>fail</dkim>
        <spf>pass</spf>
      </policy_evaluated>
    </row>
    <identifiers>
      <header_from>business.example</header_from>
    </identifiers>
    <auth_results>
      <dkim>
        <domain>business.example</domain>
        <result>fail</result>
        <human_result></human_result>
      </dkim>
      <spf>
        <domain>business.example</domain>
        <result>pass</result>
      </spf>
    </auth_results>
  </record>
</feedback>
### Publish to Analytics Engine A Workers script can push rows to Workers Analytics Engine and later read them back through GraphQL or SQL APIs. The DMARC worker defines a schema that captures the report fields relevant for dashboard queries, writes the enriched data from the parsed XML, and stores the raw XML in R2 first. The full flow is: 1. Read the recipient (RUA) from the "to" field of the email. 2. Look up zone state in Workers KV, keyed by RUA. 3. Stream the complete email into memory. 4. ParseMIME attachments with a library such as postal-mime. 5. Store the raw attachment in R2, spread over time-based directories. 6. Decompress (Zlib with pako, ZIP with unzipit) and parse XML with fast-xml-parser. 7. Write enriched report data to Workers Analytics Engine for dashboard queries. The team built a managed service on top of these building blocks, with a dashboard UI, but the same processing pipeline is deployable in any user's own account. An open-source Worker implements exactly this flow. It is designed to be useful on its own, or to be extended to write results to other storage, for instance querying Analytics Engine from another Worker, or sending rows to a SQL database via D1. A possible future improvement is pipelining report parsing through Queues, so the email sender does not wait for the full parse and store operation to finish.