Skip to content

Latest commit

 

History

19 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@koduhai/dmarc-parser

CI codecov npm version OpenSSF Scorecard License: MIT Node

Parse DMARC aggregate (RUA) reports into typed JSON, from any form mailbox providers send them in: raw .xml, gzipped .xml.gz, zipped .zip, or a whole .eml MIME email. Ships a zero-config CLI and a tiny, dependency-light library. No service, no account, runs fully offline.

📚 API reference

$ dmarc-parser google.com!example.com!1717.xml.gz

  DMARC aggregate report  #RID-12345
  domain    example.com
  reporter  google.com
  window    2024-06-01 → 2024-06-02
  policy    p=quarantine pct=100

  DMARC pass rate  71.4%   (5/7 messages)

  source ip    count  dkim  spf   disposition
  203.0.113.1      5  pass  pass   none
  198.51.100.7     2  fail  pass   quarantine

Why

Every mailbox provider (Google, Yahoo, Microsoft, ...) emails you DMARC aggregate reports as gzipped XML buried inside a MIME message. The format is awkward: array-vs-single <record> quirks, compressed attachments, inconsistent fields. This turns the whole mess into one typed object (or a readable summary) in a single call, so you can actually see who is sending mail as your domain and whether it is passing authentication.

Install

# CLI (no install)
npx @koduhai/dmarc-parser report.xml.gz

# or as a library
npm install @koduhai/dmarc-parser

CLI

dmarc-parser <file>...        # one or more .xml, .xml.gz, .gz, .zip, or .eml files
dmarc-parser report.eml --json
dmarc-parser reports/*.xml.gz --csv > reports.csv
cat report.xml | dmarc-parser -          # stdin auto-detects xml / gz / zip / eml
dmarc-parser reports/*.gz --fail-under 95   # gate a CI job on deliverability
Flag Effect
--json Print parsed report(s) as pretty JSON (an array when given multiple files)
--ndjson Print one compact JSON report per line (stream-friendly)
--csv Print one CSV row per record across all inputs
--fail-under <n> Exit 3 if the combined DMARC pass rate is below n percent
-h, --help Show usage

Exit codes: 0 ok · 1 parse/read error · 2 usage error · 3 pass rate below --fail-under.

Library

import {
  parseDmarcXml,     // (xml: string) => DmarcReport            — pure, sync
  decompressReport,  // (filename, bytes: Uint8Array) => string — .gz/.zip/.xml -> xml
  extractReportXml,  // (rawMime) => Promise<string>            — pull xml out of a MIME email
  parseReportEmail,  // (rawMime) => Promise<DmarcReport>       — extract + parse in one call
  summarize,         // (report) => DmarcSummary                — totals, pass rate, per-IP rollup
  aggregate,         // (reports[]) => DmarcAggregate           — combined rollup across many reports
  recordPassesDmarc, // (record) => boolean                     — true if DKIM or SPF is aligned-pass
  DmarcParseError,
} from '@koduhai/dmarc-parser';

import { readFileSync } from 'node:fs';

// From a raw report email (e.g. an S3 object or an IMAP fetch):
const report = await parseReportEmail(readFileSync('report.eml'));

console.log(report.meta.domain, report.meta.orgName);
for (const r of report.records) {
  console.log(r.sourceIp, r.count, recordPassesDmarc(r) ? 'PASS' : 'FAIL');
}

// Or skip the manual loop and get totals + a per-source-IP breakdown:
const { total, passing, passRate, bySourceIp } = summarize(report);
console.log(`${passRate}% pass (${passing}/${total})`);

// Roll up many reports (e.g. a mailbox or S3 prefix) into one view over a date window:
const reports = await Promise.all(emls.map((eml) => parseReportEmail(eml)));
const roll = aggregate(reports);
console.log(`${roll.reportCount} reports, ${roll.passRate}% pass`);
console.log(roll.dateBegin, '→', roll.dateEnd, roll.domains);
for (const ip of roll.bySourceIp) console.log(ip.sourceIp, ip.count, `${ip.passRate}%`);

Types

interface DmarcReport {
  meta: DmarcReportMeta;
  records: DmarcRecord[];
}

interface DmarcReportMeta {
  orgName: string;            // reporting org, e.g. "google.com"
  reportId: string;           // unique id (use for idempotent ingestion)
  domain: string;             // domain the policy applies to
  dateBegin: Date;
  dateEnd: Date;
  policyP: string | null;     // "none" | "quarantine" | "reject"
  policySp: string | null;    // subdomain policy
  policyPct: number | null;
  policyAdkim: string | null; // DKIM alignment: "r" | "s"
  policyAspf: string | null;  // SPF alignment: "r" | "s"
  policyNp: string | null;    // policy for non-existent subdomains
  policyFo: string | null;    // failure-reporting options
  errors: string[];           // <error> entries the reporter included
}

interface DmarcRecord {
  sourceIp: string;
  count: number;
  disposition: string | null;  // applied: none | quarantine | reject
  dkimResult: string | null;   // DMARC-aligned, from policy_evaluated
  spfResult: string | null;    // DMARC-aligned, from policy_evaluated
  headerFrom: string | null;
  dkimDomain: string | null;   // primary (first) DKIM-authenticated domain
  spfDomain: string | null;    // primary (first) SPF-authenticated domain
  dkimAuth: DkimAuthResult[];  // all DKIM signatures: { domain, selector, result }
  spfAuth: SpfAuthResult[];    // all SPF results:     { domain, scope, result }
  reasons: DmarcReason[];      // policy_evaluated overrides: { type, comment }
}

interface DmarcSummary {
  total: number;               // total messages across all records
  passing: number;
  failing: number;
  passRate: number;            // 0-100, one decimal
  bySourceIp: { sourceIp: string; count: number; passing: number; passRate: number }[];
}

interface DmarcAggregate extends DmarcSummary {  // returned by aggregate(reports)
  reportCount: number;
  dateBegin: Date | null;      // earliest window start across reports
  dateEnd: Date | null;        // latest window end across reports
  domains: string[];           // distinct policy domains, sorted
}

A message passes DMARC when at least one aligned mechanism (DKIM or SPF) passes, i.e. dkimResult === 'pass' || spfResult === 'pass' (this is exactly what recordPassesDmarc and summarize use).

Notes

  • Safe by default. Decompressed payloads are capped (50 MB) to bound decompression-bomb attachments, and malformed input throws a typed DmarcParseError rather than returning garbage.
  • ESM only, Node ≥ 20. Three small dependencies (fast-xml-parser, fflate, mailparser).
  • Aggregate (RUA) reports only. Failure (RUF) reports are a different, rarer format.

License

MIT © Koduhai. Built and maintained alongside KoduhMail, an email API with first-class deliverability tooling.

About

Parse DMARC aggregate (RUA) reports — .xml, .gz, .zip, or raw .eml — into typed JSON. Zero-config CLI + tiny library.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages