5.3 KiB
5.3 KiB
name, description
| name | description |
|---|---|
| sp-dmarc | Parses DMARC aggregate (RUA) and forensic (RUF) XML reports into validated, strongly-typed structures with gzip auto-detection. |
DMARC Report Parser for Suaveplan
Parses DMARC aggregate (RUA) and forensic (RUF) XML reports into validated, strongly-typed structures. Accepts raw XML strings or gzip-compressed binary blobs, maps snake_case XML fields to camelCase, validates against Zod schemas, and emits OpenTelemetry metrics on every parse call.
Architecture
| Component | Responsibility |
|---|---|
parseDmarcAggregate |
Entry point for RUA reports — decompresses, parses XML, maps fields, validates with DmarcAggregateReportSchema, emits OTel metrics |
parseDmarcForensic |
Entry point for RUF reports — same pipeline as aggregate but targets DmarcForensicReportSchema and surfaces optional authFailureReport |
decodeInput |
Unified input decoder — passes through strings, auto-decompresses gzip via magic-byte probe, UTF-8 decodes plain bytes |
parseXml |
Wraps fast-xml-parser with fixed config forcing <record>, <dkim>, <spf>, and <error> to always be arrays |
DmarcError |
Lightweight typed error class with readonly code: DmarcCode — no @suaveplan/error runtime dependency |
recordParse |
OTel convenience wrapper that increments dmarcParseTotal counter and records dmarcParseDurationMs histogram |
DmarcAggregateReportSchema / DmarcForensicReportSchema |
Zod schemas defining the validated output shapes for aggregate and forensic reports |
File Structure
communication/emails/dmarc/
src/parsers/ # `parseDmarcAggregate` and `parseDmarcForensic` entry points
src/decoders/ # `decodeInput`, `decompressGzip`, `isGzip`, and `parseXml`
src/schemas/ # Zod schemas and TypeScript types for aggregate, forensic, and shared fields
src/errors/ # `DmarcError` class and `DMARC_CODES` registry
src/otel/ # OpenTelemetry counter/histogram and `recordParse` helper
Sibling packages:
@suaveplan/email-processing— wraps this package for DKIM/SPF/DMARC verification inside MIME parsing pipelines@suaveplan/emailer— universal email service with pluggable transports; may generate DMARC-reportable messages@suaveplan/delivery-tracking— channel-agnostic delivery event tracking that consumes parsed DMARC results@suaveplan/domain-normalizer— canonicalises domains before comparing againstpolicyPublished.domainorheaderFrom
Patterns
Quick Start
import { parseDmarcAggregate, parseDmarcForensic } from "@suaveplan/dmarc";
// Parse an aggregate report from an XML string
const aggregate = parseDmarcAggregate(xmlString);
console.log(aggregate.reportMetadata.orgName);
console.log(aggregate.record[0].row.sourceIp);
// Parse from gzip-compressed bytes (auto-detected)
const { gzipSync, strToU8 } = await import("fflate");
const compressed = gzipSync(strToU8(xmlString));
const report = parseDmarcAggregate(compressed);
// Parse a forensic report
const forensic = parseDmarcForensic(forensicXml);
console.log(forensic.feedback.policyPublished.domain);
Error Discrimination
import { DmarcError, DMARC_CODES } from "@suaveplan/dmarc";
try {
parseDmarcAggregate(input);
} catch (err) {
if (err instanceof DmarcError) {
switch (err.code) {
case DMARC_CODES.PARSE_ERROR: // XML could not be parsed
case DMARC_CODES.INVALID_REPORT: // Schema validation failed
case DMARC_CODES.UNSUPPORTED_ENCODING: // Gzip decompression failed
}
}
}
Parsing with Explicit Decompression
import { parseDmarcAggregate } from "@suaveplan/dmarc/parsers/aggregate";
import { gzipSync, strToU8 } from "fflate";
const compressed = gzipSync(strToU8(xmlString));
const report = parseDmarcAggregate(compressed, { decompress: true });
Accessing Low-Level Decoders
import { decodeInput, isGzip } from "@suaveplan/dmarc/decoders/gzip";
import { parseXml } from "@suaveplan/dmarc/decoders/xml";
const xml = decodeInput(maybeCompressedBytes);
const raw = parseXml(xml);
Constraints
adkimandaspfdefault to"r"when absent from<policy_published>— the spec allows omissionfast-xml-parsercoerces numeric text nodes to JS numbers, sostr("1.0")returns"1"not"1.0"- Single
<record>,<dkim>,<spf>, and<error>elements are forced to arrays byparseXml— never checkArray.isArrayconditionally DmarcErrordoes not extend@suaveplan/error— this package has zero@suaveplan/*runtime dependenciesauthFailureReporton forensic reports is a raw MIME string — callers must parse it themselves- Empty or non-numeric date-range values coerce to
0via the internalnum()helper
Checklist
- Verify gzip auto-detection works for both
Uint8Arrayinputs and explicitdecompress: true - Assert that
dmarcParseTotalanddmarcParseDurationMsare updated on both success and error paths - Confirm
DMARC_CODESconstants are used — never hand-type"DMARC.PARSE_ERROR"string literals - Check that schema exports (
./schemas/aggregate,./schemas/forensic) are re-exported from the package root - Run
bunx turbo lint test typecheck build --filter=@suaveplan/dmarcbefore declaring done