@mehmetsahindev/dmarc-report-parser
v0.1.0
Published
Parse DMARC aggregate (RUA) reports from .zip, .gz or .xml files, in Node.js or the browser. Comes with a CLI.
Maintainers
Readme
dmarc-report-parser
Read the DMARC aggregate reports that Gmail, Outlook, Yahoo and others send to your rua address.
Give it the .zip, .gz or .xml attachment; get back who sent mail as your domain and whether it passed.
Try it online: the free DMARC report analyzer runs this library in your browser. Drop a report and see the table; the file is not uploaded anywhere.
- Works in Node.js and in the browser (no Node built-ins in the library)
- Detects the format from the bytes, not the file name or content type
- Safe on attachments from strangers: zip and gzip bombs are stopped before they fill memory
- Handles Google, Microsoft and Yahoo quirks, namespaced DMARCbis reports and single-record reports
- Typed, two small dependencies (fast-xml-parser, fflate)
Command line
npx @mehmetsahindev/dmarc-report-parser "google.com!example.com!1790985600!1791071999.zip"example.com · google.com · 2026-10-03 → 2026-10-03 · p=quarantine
report 0123456789012345678
SOURCE MESSAGES DMARC DKIM SPF DISPOSITION
54.240.3.12 (a3-12.smtp-out.eu-west-1.amazonses.com) 12 pass pass fail none
203.0.113.7 3 fail fail fail quarantine
15 messages, 12 passed (80%), 3 failedPass a folder to read every report in it. Options:
| Option | |
| --- | --- |
| -r, --resolve | look up the host name of each source IP |
| -j, --json | print the parsed reports as JSON |
Library
npm install @mehmetsahindev/dmarc-report-parserimport { readFile } from "node:fs/promises";
import { dmarcPassed, parseReportFile, summarize } from "@mehmetsahindev/dmarc-report-parser";
const reports = parseReportFile(await readFile("report.zip"));
for (const report of reports) {
console.log(report.orgName, report.domain, report.policy.p);
for (const row of report.records) {
console.log(row.sourceIp, row.count, dmarcPassed(row) ? "pass" : "fail");
}
}
console.log(summarize(reports)); // { messages: 15, passed: 12, failed: 3, ... }In the browser, read the file into a Uint8Array first:
const reports = parseReportFile(new Uint8Array(await file.arrayBuffer()));Output
parseReportFile returns an array, one entry per report in the attachment (a zip may hold more than one).
This is the Google report from the command line example (dateBegin and dateEnd are Date objects, shown here as
JSON prints them):
[
{
"orgName": "google.com",
"email": "[email protected]",
"reportId": "0123456789012345678",
"dateBegin": "2026-10-03T00:00:00.000Z",
"dateEnd": "2026-10-03T23:59:59.000Z",
"domain": "example.com",
"policy": { "p": "quarantine", "sp": "quarantine", "adkim": "r", "aspf": "r", "pct": 100 },
"records": [
{
"sourceIp": "54.240.3.12",
"count": 12,
"disposition": "none",
"dkimAligned": true,
"spfAligned": false,
"headerFrom": "example.com",
"envelopeFrom": null,
"authResults": {
"dkim": [
{ "domain": "example.com", "selector": "abc123", "result": "pass" },
{ "domain": "amazonses.com", "selector": "xyz", "result": "pass" }
],
"spf": [{ "domain": "eu-central-1.amazonses.com", "result": "pass" }]
}
},
{
"sourceIp": "203.0.113.7",
"count": 3,
"disposition": "quarantine",
"dkimAligned": false,
"spfAligned": false,
"headerFrom": "example.com",
"envelopeFrom": null,
"authResults": {
"dkim": [],
"spf": [{ "domain": "example.com", "result": "softfail" }]
}
}
]
}
]The first row is mail sent through Amazon SES. SPF passed, but for SES's own bounce domain, not example.com, so
spfAligned is false; DKIM passed for example.com, which is enough to pass DMARC. The second row passed neither,
and Gmail put those three messages in spam.
summarize(reports) adds the rows up:
{
"messages": 15,
"passed": 12,
"failed": 3,
"dkimAligned": 12,
"spfAligned": 0,
"disposition": { "none": 12, "quarantine": 3 }
}API
| Function | |
| --- | --- |
| parseReportFile(data, limits?) | every report in one attachment (.zip, .gz or XML) |
| extractReportXml(data, limits?) | only unpack: the XML documents in an attachment |
| parseDmarcReport(xml, limits?) | parse one XML report; throws if it is not one |
| summarize(reports) | message totals: passed, failed, DKIM and SPF aligned, by disposition |
| dmarcPassed(record) | a row passes when DKIM or SPF passed aligned with the From domain |
| sourceOrganization(host) | group a sending host by organisation: mail-sor-f41.google.com → google.com |
Each report has orgName, email, reportId, dateBegin, dateEnd, domain, policy and records.
Each record has sourceIp, count, disposition, dkimAligned, spfAligned, headerFrom, envelopeFrom
and the raw authResults. See src/index.ts for the full types.
Limits
Defaults: 20 MB of XML per attachment, 5 XML files per zip, 10,000 rows per report. Lower or raise them per call:
parseReportFile(data, { maxXmlBytes: 1024 * 1024, maxZipFiles: 1, maxRecords: 1000 });Values that no genuine report has (dates before 2000 or in the future, counts beyond 32 bits) make the parser throw, so a crafted report cannot slip nonsense into your database.
Background
Written for BasicMailer, where it reads the reports sent for every hosted domain and powers the free DMARC report analyzer.
License
MIT © Mehmet Şahin
