{"_id":"@domaincanary/dmarc-rua","_rev":"2-49c8cb8024058befd99f86e1f550cd05","name":"@domaincanary/dmarc-rua","dist-tags":{"latest":"0.4.0"},"versions":{"0.3.1":{"name":"@domaincanary/dmarc-rua","version":"0.3.1","keywords":["dmarc","dmarc-report","dmarc-parser","rua","aggregate-report","email-authentication","email-security","spf","dkim","xml","cloudflare-workers"],"license":"MIT","_id":"@domaincanary/dmarc-rua@0.3.1","maintainers":[{"name":"joshpurvis","email":"joshua.purvis@gmail.com"}],"homepage":"https://github.com/domaincanary/dmarc-rua#readme","bugs":{"url":"https://github.com/domaincanary/dmarc-rua/issues"},"dist":{"shasum":"e8dca15e6deaa8540c0994fd6d29c0eb17f23b21","tarball":"https://registry.npmjs.org/@domaincanary/dmarc-rua/-/dmarc-rua-0.3.1.tgz","fileCount":16,"integrity":"sha512-+Bmye6Jy9WTWpBlgM6U1g4WomEpDZEwOFE1iNLGBRyw8K1GLBDUwgNRbTIBVGAL4EshVHb35wXEdxtIiereXGQ==","signatures":[{"sig":"MEUCIBSOO+LddzJdNd30MvOJvBcDpFqRovDBeoLYPlaWlacNAiEAoadY7KoNeRDeYlGyCU+3uxnWSOhP8EBanJ9LrqWeApc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@domaincanary%2fdmarc-rua@0.3.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":68588},"module":"./esm/parser.js","engines":{"node":">=18"},"exports":{".":{"import":"./esm/parser.js"},"./email":{"import":"./esm/email.js"}},"gitHead":"022b4a3161c228f6ecea74d3f15d79944c5c2507","scripts":{},"_npmUser":{"name":"joshpurvis","email":"joshua.purvis@gmail.com"},"repository":{"url":"git+https://github.com/domaincanary/dmarc-rua.git","type":"git"},"_npmVersion":"11.17.0","description":"DMARC aggregate (RUA) report parser: XML, .xml.gz, and .zip reports into typed records, plus MIME attachment extraction for report email. Runs on Node.js, Deno, Bun, and Cloudflare Workers.","directories":{},"_nodeVersion":"24.19.0","dependencies":{"@zip.js/zip.js":"^2.8","@rgrove/parse-xml":"^4.2"},"_hasShrinkwrap":false,"devDependencies":{"@types/node":"^24"},"_npmOperationalInternal":{"tmp":"tmp/dmarc-rua_0.3.1_1787249135750_0.3412380798342989","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"_id":"@domaincanary/dmarc-rua@0.4.0","bugs":{"url":"https://github.com/domaincanary/dmarc-rua/issues"},"dist":{"shasum":"4af25e0d436a2f5045248ff799e806d4f40a6567","tarball":"https://registry.npmjs.org/@domaincanary/dmarc-rua/-/dmarc-rua-0.4.0.tgz","fileCount":16,"integrity":"sha512-fE8xUVcim+ERQxtTj/QFSox6p/iLMCIvv9DqKbFTTKMgWOE/4af7fp4yVT3HVvbokTeMpDQdRxJtAMe5bhlInw==","signatures":[{"sig":"MEUCIQDW0SZ2nRDCmX6UzNe6OsZLiNs1M9vfJjTO+/Yb0GecbwIgIjV80xk+TR/TAVHEbcZ8R4KYc5Z3bm8OhslH1kJc7z4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCADk3al0aoBsY5Z2PgqeTi7LcuvdgSCjRGJuFmEoUgfgIhANQqM+sJT5qDuQsFgIZUV5IHE2SDhFyE9zrx8lYvoQBK"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@domaincanary%2fdmarc-rua@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":76701},"name":"@domaincanary/dmarc-rua","module":"./esm/parser.js","engines":{"node":">=18"},"exports":{".":{"import":"./esm/parser.js"},"./email":{"import":"./esm/email.js"}},"gitHead":"bba691a1cd3659fd114c62b13b33eada5db34c80","license":"MIT","scripts":{},"version":"0.4.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:33ded32f-faf4-4d4e-a943-155e2b345d54"}},"homepage":"https://github.com/domaincanary/dmarc-rua#readme","keywords":["dmarc","dmarc-report","dmarc-parser","rua","aggregate-report","email-authentication","email-security","spf","dkim","xml","cloudflare-workers"],"repository":{"url":"git+https://github.com/domaincanary/dmarc-rua.git","type":"git"},"_npmVersion":"11.19.0","description":"DMARC aggregate (RUA) report parser: XML, .xml.gz, and .zip reports into typed records, plus MIME attachment extraction for report email. Runs on Node.js, Deno, Bun, and Cloudflare Workers.","directories":{},"maintainers":[{"name":"joshpurvis","email":"joshua.purvis@gmail.com"}],"_nodeVersion":"24.20.0","dependencies":{"@zip.js/zip.js":"^2.8","@rgrove/parse-xml":"^4.2"},"_hasShrinkwrap":false,"devDependencies":{"@types/node":"^24"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dmarc-rua_0.4.0_1790028714500_0.7992851444863367"}}},"time":{"created":"2026-08-20T18:05:35.276Z","modified":"2026-09-21T22:11:54.930Z","0.3.1":"2026-08-20T18:05:35.948Z","0.4.0":"2026-09-21T22:11:54.592Z"},"bugs":{"url":"https://github.com/domaincanary/dmarc-rua/issues"},"license":"MIT","homepage":"https://github.com/domaincanary/dmarc-rua#readme","keywords":["dmarc","dmarc-report","dmarc-parser","rua","aggregate-report","email-authentication","email-security","spf","dkim","xml","cloudflare-workers"],"repository":{"url":"git+https://github.com/domaincanary/dmarc-rua.git","type":"git"},"description":"DMARC aggregate (RUA) report parser: XML, .xml.gz, and .zip reports into typed records, plus MIME attachment extraction for report email. Runs on Node.js, Deno, Bun, and Cloudflare Workers.","maintainers":[{"name":"joshpurvis","email":"joshua.purvis@gmail.com"}],"readme":"# dmarc-rua\n\nParse DMARC aggregate (RUA) reports in TypeScript. `@domaincanary/dmarc-rua` is a DMARC RUA parser\nlibrary for JavaScript and TypeScript that turns the XML, gzipped XML, and zip report files mailbox\nproviders such as Google, Microsoft, and Yahoo send into typed records. It runs on Deno, Node.js,\nBun, and Cloudflare Workers.\n\nThe parser tolerates malformed records and applies shared decompression and record budgets, so\nhostile or oversized input cannot exhaust memory or CPU. The `@domaincanary/dmarc-rua/email` subpath\nextracts candidate report attachments from the small MIME subset used by DMARC report email, so raw\nRFC 5322 bytes can go in one end and parsed reports come out the other. It is not a general email\nparser.\n\nIf you want the reports read for you,\n[DomainCanary](https://domaincanary.com/?ref=dmarc-rua-readme-header) is the hosted DMARC monitoring\nservice built on this parser. Your first domain is free.\n\n- Parses DMARC aggregate report XML into typed `ParsedReport` and `ParsedRecord` objects\n- Accepts raw XML, `.xml.gz`, and `.zip` payloads, detecting the format from magic bytes\n- Surfaces SPF and DKIM authentication results, `adkim` and `aspf` alignment modes, and policy\n  override reasons\n- Bounded decompression and capped record counts guard against zip bombs\n- Runs in Cloudflare Email Workers: no Node-only dependencies apart from `node:net` `isIP`\n\n## What is a DMARC aggregate report?\n\nWhen a domain publishes a DMARC record with a `rua=` tag, receiving mail servers send periodic XML\nreports describing the mail they saw claiming to come from that domain: source IPs, message counts,\nSPF and DKIM results, and the policy they applied. These RUA reports arrive as email attachments,\nusually a gzipped or zipped XML file. This library parses those files into TypeScript objects, and\nthe email subpath pulls them out of the message first.\n\n## Install\n\nWith Deno:\n\n```sh\ndeno add jsr:@domaincanary/dmarc-rua\n```\n\nFor a Node.js or Bun project, straight from npm:\n\n```sh\nnpm install @domaincanary/dmarc-rua\n```\n\nEither way, the bare `@domaincanary/dmarc-rua` imports below work unchanged on every runtime. The\nnpm package is built from the same source, ships ESM with type declarations, and has two runtime\ndependencies, both with zero transitive dependencies and no install scripts.\n\n## Parse a DMARC report file\n\nPass the bytes of an `.xml`, `.xml.gz`, or `.zip` file to `parsePayload`. A zip can contain more\nthan one report, so the result is always an array.\n\n```ts\nimport { type ParsedReport, parsePayload } from \"@domaincanary/dmarc-rua\";\n\nconst bytes = await Deno.readFile(\"google.com!example.com!report.xml.gz\");\nconst reports: ParsedReport[] = await parsePayload(bytes, \"report.xml.gz\");\n\nfor (const report of reports) {\n  console.log(report.orgName, report.reportId, report.records.length);\n}\n```\n\n## Receive DMARC reports with a Cloudflare Email Worker\n\nCloudflare Email Workers do not provide Node mail libraries. The email subpath works directly on\n`message.raw`, and the package has no Node-only dependencies apart from `node:net` `isIP`. Workers\nsupport that API when the `nodejs_compat` compatibility flag is enabled.\n\n```ts\nimport { ParseBudget, type ParsedReport, ParseError, parsePayload } from \"@domaincanary/dmarc-rua\";\nimport { extractRecipient, parseEmailAttachments } from \"@domaincanary/dmarc-rua/email\";\n\nexport default {\n  async email(message: ForwardableEmailMessage): Promise<void> {\n    const raw = new Uint8Array(await new Response(message.raw).arrayBuffer());\n    const recipient = extractRecipient(raw);\n    const attachments = parseEmailAttachments(raw);\n    const budget = new ParseBudget();\n    const reports: ParsedReport[] = [];\n\n    for (const attachment of attachments) {\n      try {\n        reports.push(\n          ...await parsePayload(attachment.bytes, attachment.filename, budget),\n        );\n      } catch (e) {\n        // A candidate that is not a report, such as a logo image, throws ParseError. Skip it.\n        if (!(e instanceof ParseError)) throw e;\n      }\n    }\n\n    console.log({ recipient, reports });\n  },\n};\n```\n\nAdd the compatibility flag to `wrangler.jsonc`:\n\n```jsonc\n{\n  \"compatibility_flags\": [\"nodejs_compat\"]\n}\n```\n\n## Parse reports yourself, or have them read for you\n\nThis library gives you parsed records. You still have to receive the mail, store the records, work\nout which service sits behind each IP address, and tell someone when a sender starts failing.\n[DomainCanary](https://domaincanary.com/signup?from=readme.which) is the hosted service we built on\nthis parser, and it does those parts.\n\n|                   | dmarc-rua                                 | DomainCanary                                                                     |\n| ----------------- | ----------------------------------------- | -------------------------------------------------------------------------------- |\n| Who runs it       | You, in your own Worker, server or script | We do                                                                            |\n| Receiving reports | Your mailbox or Email Worker              | A reporting address for each domain                                              |\n| What you get      | Typed records from each report            | Every sending source matched to the service behind it, with pass and fail trends |\n| Alerts            | The ones you write                        | A Monday digest, and an email when your DMARC record changes                     |\n| History           | Whatever you store                        | 90 days of report detail on the free plan, 12 months on the Starter plan         |\n| Price             | Free, MIT                                 | Free for your first domain with no card, then from $19 a month for 3 domains     |\n\n[Start free with one domain](https://domaincanary.com/signup?from=readme.which).\n\n## Hardening against hostile input\n\nDMARC report addresses are published in public DNS, so anything can mail them anything. Gzip and\ndeflate data are decompressed as bounded streams. Every expanded byte is charged to a shared budget\nbefore it is retained, including nested gzip members inside zip archives. Zip entry counts and\nparsed record counts are capped to constrain CPU and memory use.\n\nMalformed records are skipped while usable sibling records are returned. `skippedRecords` reports\nhow many record elements were rejected or left unparsed, and `truncatedFields` reports bounded\nmetadata fields. These honesty counters let callers distinguish a complete report from a partially\nrecovered one.\n\n## API\n\n### `parsePayload(bytes, filename?, budget?)`\n\nParses raw XML, gzip, or zip bytes and returns `Promise<ParsedReport[]>`. Format detection uses\nmagic bytes first and the optional filename second. XML is decoded as UTF-8 unless the document\ndeclares another encoding or opens with a UTF-16 byte order mark. It throws `ParseError` when no\nusable report can be extracted, which includes every attachment that is not a report, so catch it\nper attachment when parsing email.\n\n### `ParseBudget`\n\nTracks the shared decompressed-byte and record budgets for one email or ingest operation. The\ndefaults are 64 MiB and 50,000 records. Pass custom limits to\n`new ParseBudget(decompressedBytes, records)`, and reuse the same instance across every attachment\nfrom one message. After parsing, `skippedPayloads`, `decompressionExceeded` and `recordLimitReached`\non the budget say whether anything was left unparsed, such as trailing zip members. A report whose\nXML has far more elements than the remaining record budget could account for is rejected before it\nis parsed, because the element tree costs much more memory than the bytes it came from.\n\n### `parseEmailAttachments(raw)`\n\nReturns `EmailAttachment[]` from raw RFC 5322 bytes. It handles the MIME forms needed for DMARC\nreports, including nested multipart bodies, reports forwarded as `message/rfc822` attachments,\nbase64, quoted-printable, RFC 2231 filenames, and bare XML or compressed bodies. Malformed input\nreturns whatever can be recovered and does not throw.\n\n### `extractRecipient(raw)`\n\nReturns the lowercased envelope recipient when available, preferring `X-Original-To`, then\n`Delivered-To`, then `To`. It returns `null` when no valid address can be recovered.\n\n### Types\n\nThe root export provides `ParsedReport`, `ParsedRecord`, `DkimAuthResult`, `PolicyReason`, and\n`ParseError`. `ParsedReport` includes report metadata, published policy (including the `domain` the\nreport is about and the `adkim` and `aspf` alignment modes), parsed records, and partial-recovery\ncounters. `ParsedRecord` contains the source IP, message count, evaluated DMARC results, identifiers\n(domains are lowercased), authentication results (the `mfrom` SPF result when a reporter lists\nseveral), the complete ordered DKIM auth results in `dkimAuthResults` (capped at\n`MAX_DKIM_AUTH_RESULTS_PER_RECORD`), and policy override reasons in `reasons` (capped at\n`MAX_POLICY_REASONS_PER_RECORD`). Entries dropped by either cap are counted in `truncatedFields`.\nThe email subpath exports `EmailAttachment`, whose fields are `bytes` and an optional `filename`.\n\nThis library powers\n[DMARC monitoring at DomainCanary](https://domaincanary.com/?ref=dmarc-rua-readme-footer), which\nalerts on authentication failures and DMARC record changes.\n","readmeFilename":"README.md"}