Files
skills/plugins/library-navigator/skills/sp-dmarc/SKILL.md
2026-05-29 08:20:56 +01:00

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 against policyPublished.domain or headerFrom

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

  • adkim and aspf default to "r" when absent from <policy_published> — the spec allows omission
  • fast-xml-parser coerces numeric text nodes to JS numbers, so str("1.0") returns "1" not "1.0"
  • Single <record>, <dkim>, <spf>, and <error> elements are forced to arrays by parseXml — never check Array.isArray conditionally
  • DmarcError does not extend @suaveplan/error — this package has zero @suaveplan/* runtime dependencies
  • authFailureReport on forensic reports is a raw MIME string — callers must parse it themselves
  • Empty or non-numeric date-range values coerce to 0 via the internal num() helper

Checklist

  1. Verify gzip auto-detection works for both Uint8Array inputs and explicit decompress: true
  2. Assert that dmarcParseTotal and dmarcParseDurationMs are updated on both success and error paths
  3. Confirm DMARC_CODES constants are used — never hand-type "DMARC.PARSE_ERROR" string literals
  4. Check that schema exports (./schemas/aggregate, ./schemas/forensic) are re-exported from the package root
  5. Run bunx turbo lint test typecheck build --filter=@suaveplan/dmarc before declaring done