mailauth 7.1.1 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.c8rc.json +11 -0
  2. package/.prettierignore +3 -0
  3. package/.release-please-manifest.json +1 -1
  4. package/CHANGELOG.md +68 -0
  5. package/README.md +131 -31
  6. package/bin/mailauth.js +62 -0
  7. package/cli.md +150 -8
  8. package/docs/arc.md +62 -54
  9. package/docs/bimi.md +153 -54
  10. package/docs/dkim.md +37 -36
  11. package/docs/dkim2.md +154 -0
  12. package/docs/dmarc.md +4 -1
  13. package/docs/mta-sts.md +12 -9
  14. package/docs/spf.md +14 -13
  15. package/index.d.ts +583 -42
  16. package/lib/arc/index.d.ts +6 -2
  17. package/lib/arc/index.js +45 -15
  18. package/lib/bimi/index.js +362 -102
  19. package/lib/bimi/validate-svg.js +493 -60
  20. package/lib/commands/dkim2-hash.js +25 -0
  21. package/lib/commands/dkim2-sign.js +77 -0
  22. package/lib/commands/dkim2-verify.js +59 -0
  23. package/lib/commands/report.js +10 -35
  24. package/lib/commands/resolver.js +51 -0
  25. package/lib/commands/seal.js +10 -38
  26. package/lib/commands/spf.js +4 -35
  27. package/lib/dkim/dkim-signer.js +50 -30
  28. package/lib/dkim/dkim-verifier.js +79 -27
  29. package/lib/dkim/header/simple.js +2 -2
  30. package/lib/dkim/sign.js +7 -2
  31. package/lib/dkim/syntax.js +11 -0
  32. package/lib/dkim/verify.js +1 -0
  33. package/lib/dkim2/dkim2-parser.js +52 -0
  34. package/lib/dkim2/dkim2-signer.js +334 -0
  35. package/lib/dkim2/dkim2-verifier.js +572 -0
  36. package/lib/dkim2/fields.js +570 -0
  37. package/lib/dkim2/hash.d.ts +3 -0
  38. package/lib/dkim2/hash.js +51 -0
  39. package/lib/dkim2/key.js +203 -0
  40. package/lib/dkim2/message-state.js +181 -0
  41. package/lib/dkim2/recipe.js +134 -0
  42. package/lib/dkim2/sign.d.ts +3 -0
  43. package/lib/dkim2/sign.js +38 -0
  44. package/lib/dkim2/verify.d.ts +3 -0
  45. package/lib/dkim2/verify.js +19 -0
  46. package/lib/dmarc/author-domain.js +270 -5
  47. package/lib/dmarc/get-dmarc-record.js +24 -11
  48. package/lib/dmarc/verify.js +7 -3
  49. package/lib/mailauth.js +57 -14
  50. package/lib/mta-sts.js +37 -11
  51. package/lib/parse-received.js +214 -25
  52. package/lib/spf/index.js +21 -4
  53. package/lib/spf/macro.js +5 -3
  54. package/lib/spf/spf-verify.js +14 -5
  55. package/lib/spf/syntax.js +5 -0
  56. package/lib/tools.js +160 -21
  57. package/package.json +15 -6
  58. package/stryker.config.json +16 -0
package/.c8rc.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "all": true,
3
+ "include": ["lib/**/*.js"],
4
+ "exclude": ["test/**", "bin/**"],
5
+ "reporter": ["text", "text-summary", "lcov"],
6
+ "reports-dir": "coverage",
7
+ "statements": 93.5,
8
+ "branches": 91.2,
9
+ "functions": 95.5,
10
+ "lines": 93.5
11
+ }
package/.prettierignore CHANGED
@@ -8,3 +8,6 @@ node_modules/
8
8
 
9
9
  # Test fixtures with embedded content
10
10
  test/fixtures/**/*.yml
11
+ test/fixtures/dkim2tests/vectors.json
12
+ test/fixtures/dkim2-stalwart/messages.json
13
+ test/fixtures/dkim2-croessner/public-golden.json
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "7.1.1"
2
+ ".": "7.3.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,73 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.3.0](https://github.com/postalsys/mailauth/compare/v7.2.0...v7.3.0) (2026-10-10)
4
+
5
+
6
+ ### Features
7
+
8
+ * **dkim2:** add dkim2-verify and dkim2-hash CLI commands ([abc5867](https://github.com/postalsys/mailauth/commit/abc586767ac60a37aacb5ae4ef2cc5957b14d5cd))
9
+ * **dkim2:** add experimental DKIM2 signing and verification ([767ce96](https://github.com/postalsys/mailauth/commit/767ce96a0128f3054a4634d7f2e29ccb734c1b8b))
10
+ * **dkim2:** reject timestamps more than 5 minutes in the future by default ([85ec5b8](https://github.com/postalsys/mailauth/commit/85ec5b8a9ed6f3fe9126ae04d12fca6a6d4c003e))
11
+ * **dkim2:** warn when the SMTP envelope is not checked ([6c0f570](https://github.com/postalsys/mailauth/commit/6c0f570e1b9630b0a383876b6f9a603514731efc))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **dkim2:** match the draft-06 vectors of croessner/dkim2 ([968d0b3](https://github.com/postalsys/mailauth/commit/968d0b303be75580ea76d2207394cf26edafc2bc))
17
+
18
+ ## [7.2.0](https://github.com/postalsys/mailauth/compare/v7.1.1...v7.2.0) (2026-10-09)
19
+
20
+
21
+ ### Features
22
+
23
+ * **bimi:** apply the exact Assertion Record syntax in strict mode ([85c4363](https://github.com/postalsys/mailauth/commit/85c4363f3238ce0ba80cbece81abf5af92bb0840)), closes [#166](https://github.com/postalsys/mailauth/issues/166)
24
+ * **dkim:** over-sign header field names repeated in headerList ([021218f](https://github.com/postalsys/mailauth/commit/021218fd733ccb86f941e8aa9a80e2add7967b0e)), closes [#153](https://github.com/postalsys/mailauth/issues/153)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * **arc:** check a large authResults value without running out of stack ([624e8a3](https://github.com/postalsys/mailauth/commit/624e8a3bfd881d81f783f496a1ffc7c8a24bebba))
30
+ * **arc:** do not modify the caller's seal options ([5c57a9d](https://github.com/postalsys/mailauth/commit/5c57a9dd1be2b24dbb32ee78b04f2ca41f47c26d)), closes [#159](https://github.com/postalsys/mailauth/issues/159)
31
+ * **arc:** fail a chain whose d= is not a host name ([2f0babe](https://github.com/postalsys/mailauth/commit/2f0babeac0fe41b17bde53c110d54c3f04a748b3))
32
+ * **arc:** refuse an ARC-Authentication-Results value that breaks the header ([43713b4](https://github.com/postalsys/mailauth/commit/43713b4aa9cd7e3efff5bc35e11385d0c4be758a)), closes [#161](https://github.com/postalsys/mailauth/issues/161)
33
+ * **arc:** seal a malformed chain with cv=fail ([c9a4e85](https://github.com/postalsys/mailauth/commit/c9a4e8555d7e1f525d67d855a3a6737d69124dbe)), closes [#162](https://github.com/postalsys/mailauth/issues/162)
34
+ * **arc:** seal with the computed chain status and AAR in authenticate() ([d6f7efc](https://github.com/postalsys/mailauth/commit/d6f7efcd35fa14489c3adc089b430818e5bab806)), closes [#160](https://github.com/postalsys/mailauth/issues/160)
35
+ * **auth-results:** write a U-label domain unquoted in strict mode ([a24ffd6](https://github.com/postalsys/mailauth/commit/a24ffd631bdae835edcbe65a28e357d33f60be00)), closes [#169](https://github.com/postalsys/mailauth/issues/169)
36
+ * **bimi:** check the SVG Tiny PS document rules, add strict mode ([4cf9bfc](https://github.com/postalsys/mailauth/commit/4cf9bfc31ece9b8f2a477db841808c038da65b49)), closes [#165](https://github.com/postalsys/mailauth/issues/165)
37
+ * **bimi:** count From addresses with the parser of the DMARC check ([0fb165e](https://github.com/postalsys/mailauth/commit/0fb165eb00a06b0fd372e7bb757e99528479f583))
38
+ * **bimi:** ignore an invalid BIMI-Selector header ([7f62164](https://github.com/postalsys/mailauth/commit/7f6216476b2eda34c91dde23f3c047e07ed67471)), closes [#167](https://github.com/postalsys/mailauth/issues/167)
39
+ * **bimi:** read the avp= avatar preference tag ([f577b50](https://github.com/postalsys/mailauth/commit/f577b5056476a5ffda325d44e7cfe0d7a8eb7547)), closes [#164](https://github.com/postalsys/mailauth/issues/164)
40
+ * **bimi:** report validateVMC() results in Authentication-Results ([6b03caa](https://github.com/postalsys/mailauth/commit/6b03caa9ffba2bc0e1d86177ad5a2b1f4f9d55d6)), closes [#168](https://github.com/postalsys/mailauth/issues/168)
41
+ * **bimi:** restrict logo and evidence downloads ([658d8c2](https://github.com/postalsys/mailauth/commit/658d8c22fb3087a37f5ccbd4f0b1e874d7f53fd2))
42
+ * **bimi:** validate logos against an SVG Tiny PS element allowlist ([0cf9679](https://github.com/postalsys/mailauth/commit/0cf9679c3fb185a63fd3541350a62cf68eda04c8))
43
+ * **bimi:** validate the logo from l= before building BIMI headers ([bdc7b90](https://github.com/postalsys/mailauth/commit/bdc7b907f329c1af1a983e7f845e901621aaee09))
44
+ * **dkim:** check the key type against a= in both modes ([4b9ef52](https://github.com/postalsys/mailauth/commit/4b9ef52f446486611b622e2f1e2fb82656e85223)), closes [#146](https://github.com/postalsys/mailauth/issues/146)
45
+ * **dkim:** encode i= as dkim-quoted-printable and check its syntax ([59f63e1](https://github.com/postalsys/mailauth/commit/59f63e1334565605979d4e9d8b8d364cd1f9c365)), closes [#150](https://github.com/postalsys/mailauth/issues/150)
46
+ * **dkim:** never align a signature when From has no single Author Domain ([ac882e2](https://github.com/postalsys/mailauth/commit/ac882e26bca1dd0c88ee7afc3cffecbb4d47db51))
47
+ * **dkim:** parse key records without removing whitespace inside values ([4b40376](https://github.com/postalsys/mailauth/commit/4b4037603d5b45e628f962ae2e986221b4eb4fdb)), closes [#149](https://github.com/postalsys/mailauth/issues/149)
48
+ * **dkim:** read the canonicalization option like a c= value ([1383056](https://github.com/postalsys/mailauth/commit/1383056d50220a0f61698964f35493a381335143)), closes [#152](https://github.com/postalsys/mailauth/issues/152)
49
+ * **dkim:** reject a d= that is not a host name and never align it ([387db0c](https://github.com/postalsys/mailauth/commit/387db0c639903fab54fa49d2674329664e7ffeca))
50
+ * **dkim:** reject an l= larger than the canonicalized body in strict mode ([5139833](https://github.com/postalsys/mailauth/commit/5139833901adeef37443d53fd3ad8960188dabae)), closes [#147](https://github.com/postalsys/mailauth/issues/147)
51
+ * **dkim:** report a key record name that can not exist as no key ([d4f60ca](https://github.com/postalsys/mailauth/commit/d4f60cace9e8a555964a140476aa616aa2de005c)), closes [#145](https://github.com/postalsys/mailauth/issues/145)
52
+ * **dkim:** report signatures that are skipped for a missing key ([bda5402](https://github.com/postalsys/mailauth/commit/bda540242cc93e6cefe73c5b75efbb8f41544163)), closes [#151](https://github.com/postalsys/mailauth/issues/151)
53
+ * **dkim:** validate the c= syntax and canonicalize with the parsed value ([5da00a5](https://github.com/postalsys/mailauth/commit/5da00a553f45482e6ef5209d6894c196401a0fbc)), closes [#148](https://github.com/postalsys/mailauth/issues/148)
54
+ * **dmarc:** discard a policy record with whitespace before v=DMARC1 in strict mode ([d84decf](https://github.com/postalsys/mailauth/commit/d84decf7db25efd3385d0102410d0e2aa1f458d7)), closes [#143](https://github.com/postalsys/mailauth/issues/143)
55
+ * **dmarc:** discard record fragments without "=" ([0cdcf56](https://github.com/postalsys/mailauth/commit/0cdcf56e1ea9d55c4c614cd7214c74fc783729fb)), closes [#142](https://github.com/postalsys/mailauth/issues/142)
56
+ * **dmarc:** parse the From header by the RFC 5322 grammar ([08da841](https://github.com/postalsys/mailauth/commit/08da8419de7aa987cd04a366c3d04fe6b858d4d9))
57
+ * **dmarc:** reject a From local-part with a domain literal between its words ([ec0af9e](https://github.com/postalsys/mailauth/commit/ec0af9e7adc895170357e408c1598cbdb8f3fc03))
58
+ * **dmarc:** reject an Author Domain with control or format characters ([e1f1bb7](https://github.com/postalsys/mailauth/commit/e1f1bb7e79327e9cc92d665cb85c645cc36ab35f))
59
+ * **mailauth:** use the HELO name from the Received header with trustReceived ([f7e2869](https://github.com/postalsys/mailauth/commit/f7e2869f1b6009863945698b337527be289d3fab)), closes [#156](https://github.com/postalsys/mailauth/issues/156)
60
+ * **mta-sts:** limit retries of a new policy ID while a cached policy is used ([599b988](https://github.com/postalsys/mailauth/commit/599b988b992be4a5b4ddda6b69cfb32565c95924)), closes [#170](https://github.com/postalsys/mailauth/issues/170)
61
+ * **received:** do not guess the client IP when the HELO adds ";" or Exim comments ([49b9a45](https://github.com/postalsys/mailauth/commit/49b9a45422aa8973e4c8b0de7004bfd4ef705a20))
62
+ * **received:** take the client IP only from an unambiguous from clause ([be23ede](https://github.com/postalsys/mailauth/commit/be23edebe3115040367fc77f0c30323fa6726f70))
63
+ * **spf:** accept 0 for maxVoidCount and maxResolveCount ([e1826d7](https://github.com/postalsys/mailauth/commit/e1826d7a25d5f3121b109bed467b4e3834100ded)), closes [#158](https://github.com/postalsys/mailauth/issues/158)
64
+ * **spf:** accept a MAIL FROM or HELO domain with a single trailing dot ([11256ae](https://github.com/postalsys/mailauth/commit/11256ae23a8c653394e5998e7c39c58b5e0ff04d))
65
+ * **spf:** check the expanded explanation string for US-ASCII ([b4e844e](https://github.com/postalsys/mailauth/commit/b4e844e814b368769466dff063246972271a8f36)), closes [#155](https://github.com/postalsys/mailauth/issues/155)
66
+ * **spf:** do not count the %{p} PTR query as a void lookup ([46f3298](https://github.com/postalsys/mailauth/commit/46f3298fcb672a5897d94e6b792219a68091bb9f)), closes [#154](https://github.com/postalsys/mailauth/issues/154)
67
+ * **spf:** report the reason of macro syntax errors in the permerror comment ([d3fe2bf](https://github.com/postalsys/mailauth/commit/d3fe2bfbce43550a27b8dac5a27931e55297203f)), closes [#157](https://github.com/postalsys/mailauth/issues/157)
68
+ * **spf:** split macro values on exactly the delimiters that are listed ([98c32c0](https://github.com/postalsys/mailauth/commit/98c32c09054a6651cacda089c07ddd22ff5fd470))
69
+ * **types:** mark DMARCResult policy fields optional and cite RFC 9989 9.1 ([10dd7fe](https://github.com/postalsys/mailauth/commit/10dd7fee7e7fa8994649ffd7409d97a3a722c316)), closes [#144](https://github.com/postalsys/mailauth/issues/144)
70
+
3
71
  ## [7.1.1](https://github.com/postalsys/mailauth/compare/v7.1.0...v7.1.1) (2026-10-04)
4
72
 
5
73
 
package/README.md CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  - **SPF** verification
10
10
  - **DKIM** signing and verification
11
+ - **DKIM2** signing and verification (experimental, built against the IETF drafts, see [DKIM2](#dkim2))
11
12
  - **DMARC** verification
12
13
  - **ARC** verification and sealing
13
14
  - Sealing during authentication
@@ -30,6 +31,9 @@ mailauth is a pure JavaScript implementation, requiring no external applications
30
31
  - [DKIM](#dkim)
31
32
  - [Signing](#dkim-signing)
32
33
  - [Verification](#dkim-verification)
34
+ - [DKIM2](#dkim2)
35
+ - [Signing](#dkim2-signing)
36
+ - [Verification](#dkim2-verification)
33
37
  - [SPF](#spf)
34
38
  - [Verification](#spf-verification)
35
39
  - [ARC](#arc)
@@ -82,21 +86,21 @@ await authenticate(message [, options])
82
86
  - **sender** (`string`): Email address from the MAIL FROM command. Defaults to the `Return-Path` header if not set.
83
87
  - **ip** (`string`): IP address of the remote client that sent the message.
84
88
  - **helo** (`string`): Hostname from the HELO/EHLO command.
85
- - **trustReceived** (`boolean`): If `true`, parses `ip` and `helo` from the latest `Received` header if not provided. The IP address is the connecting address from the TCP-info comment of the `from` clause (RFC 5321 section 4.4), never an address literal the client sent in its HELO. Postfix, Sendmail, Exim (including its `from [ip] (helo=...)` form) and similar formats are understood. Defaults to `false`.
89
+ - **trustReceived** (`boolean`): If `true`, parses `ip` and `helo` from the latest `Received` header if not provided. The IP address is the connecting address from the TCP-info comment of the `from` clause (RFC 5321 section 4.4), never an address literal the client sent in its HELO. The HELO name is the `from` value, or the `helo=` value that Exim writes into the comment, and is only taken when neither `ip` nor `helo` was provided. Postfix, Sendmail, Exim (including its `from [ip] (helo=...)` form) and similar formats are understood. If the header can not be read without guessing, for example when the HELO name holds parentheses, quotes, a `;`, an address literal in a comment or a `by` keyword that make the `from` clause ambiguous, no IP address is taken from it. Defaults to `false`.
86
90
  - **mta** (`string`): Hostname of the server performing the authentication. Defaults to `os.hostname()`. Used as the authserv-id of the Authentication headers, so it should be a host name: an internationalized name is converted to A-labels, and a value that is not a valid token (for example one with a space or a semicolon) is written as a quoted string. With `strict` such a value throws an error with the code `EINVALIDAUTHSERVID` instead.
87
91
  - **minBitLength** (`number`): Minimum allowed bits for RSA public keys. Defaults to `1024`. Keys with fewer bits will fail validation.
88
92
  - **rejectRsaSha1** (`boolean`): If `true`, an rsa-sha1 DKIM signature gives `dkim=policy` with `policy.dkim-rules=weak-algorithm`, as in strict mode, and is not counted for DMARC. Every other check keeps the lenient default. It only affects verification. Defaults to `false`, and `strict` implies it.
89
93
  - **disableArc** (`boolean`): If `true`, skips ARC checks.
90
94
  - **disableDmarc** (`boolean`): If `true`, skips DMARC checks, also disabling dependent checks like BIMI.
91
95
  - **disableBimi** (`boolean`): If `true`, skips BIMI checks.
92
- - **seal** (`object`): Options for ARC sealing if the message doesn't have a broken ARC chain.
96
+ - **seal** (`object`): Options for ARC sealing. A message without an ARC chain or with a valid one is sealed with `cv=none` or `cv=pass`, a message with a failed chain is sealed with `cv=fail`, unless its newest ARC-Seal already says `cv=fail` (RFC 8617 section 5.1).
93
97
  - **signingDomain** (`string`): ARC key domain name.
94
98
  - **selector** (`string`): ARC key selector.
95
99
  - **privateKey** (`string` or `Buffer`): Private key for signing (RSA or Ed25519).
96
100
  - **resolver** (`async function`): Custom DNS resolver function. Defaults to [`dns.promises.resolve`](https://nodejs.org/api/dns.html#dns_dnspromises_resolve_hostname_rrtype).
97
101
  - **maxResolveCount** (`number`): DNS lookup limit for SPF. Defaults to `10` as per [RFC7208](https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4).
98
102
  - **maxVoidCount** (`number`): DNS lookup limit for SPF producing empty results. Defaults to `2` as per [RFC7208](https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4).
99
- - **strict** (`boolean`): If `true`, every check follows its RFC exactly instead of the lenient defaults. Passed on to DKIM, SPF, ARC, DMARC, BIMI and ARC sealing (BIMI only uses it for the format of its Authentication-Results entry). Defaults to `false`. See [Strict mode](#strict-mode).
103
+ - **strict** (`boolean`): If `true`, every check follows its RFC exactly instead of the lenient defaults. Passed on to DKIM, SPF, ARC, DMARC, BIMI and ARC sealing. Defaults to `false`. See [Strict mode](#strict-mode).
100
104
 
101
105
  #### Example
102
106
 
@@ -144,7 +148,7 @@ You can see the full output, including structured data for DKIM, SPF, DMARC, and
144
148
 
145
149
  **Note:** The `receivedChain` property is an array of parsed representations of the `Received:` headers.
146
150
 
147
- **Note:** `dmarc` is `false` when DMARC can not be evaluated because the From header has no domain or more than one (RFC 9989 section 5.3.1). `dmarcSkipReason` then says why: `no-author-domain`, `multiple-author-domains`, `invalid-author-domain` (a From mailbox has no usable domain) or `multiple-from-fields` (the message has more than one From header field, which RFC 5322 does not allow). RFC 9989 section 11.5 suggests treating such a message as suspicious.
151
+ **Note:** `dmarc` is `false` when DMARC can not be evaluated because the From header has no domain or more than one (RFC 9989 section 5.3.1). `dmarcSkipReason` then says why: `no-author-domain`, `multiple-author-domains`, `invalid-author-domain` (a From mailbox has no usable domain, or the From header is malformed) or `multiple-from-fields` (the message has more than one From header field, which RFC 5322 does not allow). RFC 9989 section 11.5 suggests treating such a message as suspicious.
148
152
 
149
153
  **Note:** An ARC set is only added while the chain has fewer than 50 sets (RFC 8617 section 4.2.1). Otherwise the message is not sealed and `arc.sealErrors` says why.
150
154
 
@@ -154,27 +158,28 @@ By default mailauth is lenient where accepting non-conforming input costs little
154
158
 
155
159
  Whatever the default mode accepts that strict mode would reject is marked in a `warnings` array on the affected result (for example `dkim.results[0].status.warnings` is `['rsa-sha1']`), so that you can apply your own policy without switching modes. The markers are never written into the generated headers. For rsa-sha1 verification the `rejectRsaSha1` option applies the strict rule on its own, which also keeps such a signature out of DMARC in `authenticate()`.
156
160
 
157
- Some rules apply in both modes, because breaking them is a defect and not leniency. For DKIM these are: a signature must sign the From header, the key record's `h=` (hash algorithms) and `s=` (service types) restrictions are honored, `ed25519-sha1` is not an algorithm, and signing refuses a domain, selector or identity that would break out of its tag.
161
+ Some rules apply in both modes, because breaking them is a defect and not leniency. For DKIM these are: a signature must sign the From header, the key record's `h=` (hash algorithms) and `s=` (service types) restrictions are honored, `ed25519-sha1` is not an algorithm, the key type must match the signature algorithm, a `d=` with a character that can not be in a host name (such as `/`, `?` or `#`) makes the signature invalid, and signing refuses a domain, selector or identity that would break out of its tag. The same `d=` rule fails an ARC chain whose ARC-Seal or newest ARC-Message-Signature has such a `d=`.
158
162
 
159
163
  DKIM differences between the modes:
160
164
 
161
- | Input | Default | `strict: true` |
162
- | ------------------------------------------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------------- |
163
- | rsa-sha1 signature (RFC 8301) | `pass`, warning `rsa-sha1` | `policy` (`policy.dkim-rules=weak-algorithm`) |
164
- | Missing `v=` or `h=`, `v=` other than 1 | verified, warning `missing-v`/`missing-h`/`invalid-v` | `neutral` (`signature missing required tag`, `incompatible version`) |
165
- | Duplicate tags, upper case tag names, malformed `t=`/`x=`/`l=` | verified, warning `tag-syntax` | `neutral` (`signature syntax error`) |
166
- | `i=` domain outside `d=`, or a subdomain with a key that has `t=s` | verified, warning `identity-domain` | `neutral` (`domain mismatch`) |
167
- | `x=` equal to `t=` | verified, warning `invalid-expiration` | `neutral` (`invalid expiration`) |
168
- | Key record syntax errors, `v=` not first or not exactly `DKIM1` | used, warning `key-syntax`/`key-v-syntax` | key ignored |
169
- | Ed25519 key record without `k=ed25519` | used, warning `key-type-inferred` | key ignored |
170
- | Body hash mismatch | `neutral` | `fail` |
171
- | Signature with an unknown algorithm or no `d=`/`s=` | left out of the results | `neutral` |
172
- | `header.i` in Authentication-Results | always `@d` | the AUID (`i=` or `@d`), plus `header.d` |
173
- | Email identities in Authentication-Results | a value that needs quoting is quoted as a whole | `local-part@domain` with only the local-part quoted (RFC 8601 section 2.2) |
174
- | Header order | Received-SPF above Authentication-Results | Authentication-Results on top (RFC 8601 section 5) |
175
- | Signing with rsa-sha1 or an RSA key under 1024 bits | signed, warning `rsa-sha1`/`weak-key` | error |
176
-
177
- See the [DKIM result reference](docs/dkim.md) for every DKIM warning, and the SPF, DMARC, ARC and MTA-STS documentation for theirs.
165
+ | Input | Default | `strict: true` |
166
+ | ------------------------------------------------------------------ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
167
+ | rsa-sha1 signature (RFC 8301) | `pass`, warning `rsa-sha1` | `policy` (`policy.dkim-rules=weak-algorithm`) |
168
+ | Missing `v=` or `h=`, `v=` other than 1 | verified, warning `missing-v`/`missing-h`/`invalid-v` | `neutral` (`signature missing required tag`, `incompatible version`) |
169
+ | Duplicate/upper case tags, bad `c=`/`t=`/`x=`/`l=`, `l=` > body | verified, warning `tag-syntax` | `neutral` (`signature syntax error`) |
170
+ | `i=` domain outside `d=`, or a subdomain with a key that has `t=s` | verified, warning `identity-domain` | `neutral` (`domain mismatch`) |
171
+ | `x=` equal to `t=` | verified, warning `invalid-expiration` | `neutral` (`invalid expiration`) |
172
+ | Key record syntax errors, `v=` not first or not exactly `DKIM1` | used, warning `key-syntax`/`key-v-syntax` | key ignored |
173
+ | Ed25519 key record without `k=ed25519` | used, warning `key-type-inferred` | key ignored |
174
+ | Ed25519 key published as SubjectPublicKeyInfo | used, warning `key-ed25519-spki` | key ignored |
175
+ | Body hash mismatch | `neutral` | `fail` |
176
+ | Signature with an unknown algorithm or no `d=`/`s=` | left out of the results | `neutral` |
177
+ | `header.i` in Authentication-Results | always `@d` | the AUID (`i=` or `@d`), plus `header.d` |
178
+ | Email identities in Authentication-Results | a value that needs quoting is quoted as a whole | `local-part@domain` with only the local-part quoted, U-label domains unquoted (RFC 8601 sections 1.5.2 and 2.2) |
179
+ | Header order | Received-SPF above Authentication-Results | Authentication-Results on top (RFC 8601 section 5) |
180
+ | Signing with rsa-sha1 or an RSA key under 1024 bits | signed, warning `rsa-sha1`/`weak-key` | error |
181
+
182
+ See the [DKIM result reference](docs/dkim.md) for every DKIM warning, and the SPF, DMARC, ARC, BIMI and MTA-STS documentation for theirs.
178
183
 
179
184
  ### DKIM
180
185
 
@@ -191,26 +196,26 @@ const signResult = await dkimSign(message, options);
191
196
  // Returns: { signatures: String, errors: Array, warnings: Array }
192
197
  ```
193
198
 
194
- `signatures` holds the `DKIM-Signature` header lines, each ending with a line break. It is an empty string when no signature could be created, in which case `errors` says why. Each entry of `errors` is an object with an `err` property (an `Error` with a `code`, such as `ENOFROM`, `EINVALIDALGO`, `ESHORTKEY` or `EINVALIDDOMAIN`) and the `signingDomain` and `selector` it applies to. `warnings` lists what was signed although `strict` mode would have refused it (`rsa-sha1`, `weak-key`, `invalid-expiration`, `invalid-signtime`, `d-syntax`, `s-syntax`, `identity-domain`).
199
+ `signatures` holds the `DKIM-Signature` header lines, each ending with a line break. It is an empty string when no signature could be created, in which case `errors` says why. Each entry of `errors` is an object with an `err` property (an `Error` with a `code`, such as `ENOFROM`, `ENOKEY`, `EINVALIDALGO`, `ESHORTKEY` or `EINVALIDDOMAIN`) and the `signingDomain` and `selector` it applies to. When no signature is configured at all, the only entry has the code `ENOSIGNATURE`. `warnings` lists what was signed although `strict` mode would have refused it (`rsa-sha1`, `weak-key`, `invalid-expiration`, `invalid-signtime`, `d-syntax`, `s-syntax`, `identity-syntax`, `identity-domain`).
195
200
 
196
201
  ##### Parameters
197
202
 
198
203
  - **message**: A `String`, `Buffer`, or `Readable` stream representing the email message.
199
204
  - **options**:
200
- - **canonicalization** (`string`): Canonicalization method. Defaults to `'relaxed/relaxed'`.
205
+ - **canonicalization** (`string`): Canonicalization method, as `header/body`. Defaults to `'relaxed/relaxed'`. Read like a `c=` value: case-insensitive, and a value without a body part, such as `'relaxed'`, uses `simple` for the body. A signature with an unknown method is not created and gets an `EINVALIDCANON` error.
201
206
  - **algorithm** (`string`): Signing and hashing algorithm. Defaults to `'rsa-sha256'` for an RSA key and `'ed25519-sha256'` for an Ed25519 key. `'rsa-sha1'` is historic (RFC 8301) and adds a warning.
202
207
  - **signTime** (`Date`): Signing time. Defaults to current time.
203
208
  - **expires** (`Date`): Signature expiration time (`x=` tag). Optional. It must be later than the signing time.
204
- - **headerList** (`Array` or `string`): Header field names to sign, as an array or a colon separated string. Optional; uses default set if not specified. `From` must be included.
209
+ - **headerList** (`Array` or `string`): Header field names to sign, as an array or a colon separated string. Optional; uses default set if not specified. `From` must be included. Every field of a listed name is signed, and a name the message does not have is left out of `h=`. To over-sign a field (RFC 6376 section 8.15), so that adding another instance of it in transit breaks the signature, list its name more than once, such as `'From:From:Subject:Subject'`: a repeated name appears in `h=` as many times as it is listed, or as many times as the message has that field, whichever is more.
205
210
  - **strict** (`boolean`): If `true`, refuses to sign with `rsa-sha1`, with an RSA key shorter than 1024 bits, with an expiration that is not after the signing time, and with a domain, selector or identity that is not valid RFC 6376 syntax. Defaults to `false`, which signs these and lists them in `warnings`. A value that would break out of its tag (a semicolon, whitespace or a line break in the domain, selector or identity) is refused in both modes.
206
- - **signatureData** (`Array`): Array of signature objects. Each object may contain:
211
+ - **signatureData** (`Array`): Array of signature objects, one for each signature to create. The domain, selector and key are only read from here, not from the top level of the options. Each object may contain:
207
212
  - **signingDomain** (`string`): DKIM key domain name. An internationalized domain name is converted to A-labels.
208
213
  - **selector** (`string`): DKIM key selector.
209
- - **privateKey** (`string` or `Buffer`): Private key for signing (RSA or Ed25519).
214
+ - **privateKey** (`string` or `Buffer`): Private key for signing (RSA or Ed25519). An entry without a key is not signed and gets an `ENOKEY` error.
210
215
  - **algorithm** (`string`, optional): Overrides parent `algorithm`.
211
216
  - **canonicalization** (`string`, optional): Overrides parent `canonicalization`.
212
217
  - **headerList** (`Array` or `string`, optional): Overrides parent `headerList`.
213
- - **identity** (`string`, optional): Agent or User Identifier for the `i=` tag, such as `user@mail.example.com`. Its domain must be the signing domain or one of its subdomains.
218
+ - **identity** (`string`, optional): Agent or User Identifier for the `i=` tag, such as `user@mail.example.com`. Its domain must be the signing domain or one of its subdomains. Pass the address as it is, not encoded: it is written in dkim-quoted-printable, so `jõgi@example.com` becomes `i=j=C3=B5gi@example.com` and a `=` becomes `=3D`.
214
219
  - **maxBodyLength** (`number`, optional): Maximum number of canonicalized body bytes to sign (`l=` tag). Not recommended for general use.
215
220
 
216
221
  ##### Example
@@ -328,6 +333,101 @@ dkim=pass header.i=@tahvel.info header.s=test.rsa header.b="BrEgDN4A"
328
333
  dkim=policy policy.dkim-rules=weak-key header.i=@tahvel.info header.s=test.small header.b="d0jjgPun"
329
334
  ```
330
335
 
336
+ ### DKIM2
337
+
338
+ > [!WARNING]
339
+ > DKIM2 is not a published RFC yet. This implementation is experimental and follows these Internet-Drafts, which can still change in ways that break compatibility:
340
+ >
341
+ > - [draft-ietf-dkim-dkim2-spec-06](https://www.ietf.org/archive/id/draft-ietf-dkim-dkim2-spec-06.txt), "DomainKeys Identified Mail Signatures v2 (DKIM2)", 28 August 2026
342
+ > - [draft-ietf-dkim-dkim2-dns-00](https://www.ietf.org/archive/id/draft-ietf-dkim-dkim2-dns-00.txt), "Domain Name Specification for DKIM2", 20 July 2026
343
+ > - [draft-ietf-dkim-dkim2-bcp-01](https://www.ietf.org/archive/id/draft-ietf-dkim-dkim2-bcp-01.txt), "DKIM2 Best Practices", 9 September 2026
344
+ >
345
+ > The `dkim2` Authentication-Results method follows the individual draft [draft-gondwana-dkim2-authres-00](https://datatracker.ietf.org/doc/html/draft-gondwana-dkim2-authres-00), as no method name is registered yet.
346
+
347
+ DKIM2 adds a `Message-Instance` header field with hashes of the message, and a `DKIM2-Signature` header field per hop that signs the DKIM2 header fields and records the SMTP envelope (MAIL FROM and RCPT TO) of that hop. Forwarders that change a message describe their changes with Recipes, so that a verifier can recreate every earlier instance of the message and check every signature. DKIM2 uses the same DNS key records as DKIM1 (`selector._domainkey.domain`, `v=DKIM1`), so existing RSA and Ed25519 keys work as they are.
348
+
349
+ DKIM1 and DKIM2 do not sign each other's header fields, so a message can carry both, added in any order. The best practices draft recommends signing with both while DKIM2 is being deployed.
350
+
351
+ DKIM2 is opt-in in `authenticate()` with the `dkim2` option. Its result is reported in `result.dkim2` and in Authentication-Results, and it is not used for DMARC (whether DMARC will use DKIM2 is still an open question of the working group). Delivery Status Notification handling (section 12 of the specification) is left to the MTA.
352
+
353
+ #### DKIM2 Signing
354
+
355
+ ```javascript
356
+ const { dkim2Sign } = require('mailauth');
357
+
358
+ const { signatures } = await dkim2Sign(message, {
359
+ signingDomain: 'example.com',
360
+ // one key per algorithm is recommended, at most two keys per algorithm
361
+ signatureData: [
362
+ { selector: 'rsa2026', privateKey: rsaPrivateKeyPem },
363
+ { selector: 'ed2026', privateKey: ed25519PrivateKeyPem }
364
+ ],
365
+ // the SMTP envelope the message is sent with, required (section 9.2)
366
+ mailFrom: 'bounces@example.com',
367
+ rcptTo: ['user@example.net']
368
+ });
369
+
370
+ // prepend the DKIM2-Signature and Message-Instance header fields to the message
371
+ const signedMessage = signatures + message;
372
+ ```
373
+
374
+ - **signingDomain** (`string`): The signing domain (`d=`). It has to be the MAIL FROM domain or a parent of it.
375
+ - **signatureData** (`array`): Keys to sign with, `{ selector, privateKey, algorithm }`. The algorithm (`rsa-sha256` or `ed25519-sha256`) follows the key type. RSA keys need at least 1024 bits and the public exponent 65537. For a single key, `selector` and `privateKey` can be given directly instead.
376
+ - **mailFrom** (`string`): MAIL FROM address (`mf=`), `''` or `'<>'` for the null sender.
377
+ - **rcptTo** (`string|string[]`): RCPT TO addresses (`rt=`).
378
+ - **nextDomain** (`string`): For a hop across a trust boundary (section 9.3), the domain of the next signature (`nd=`), used instead of `mailFrom` and `rcptTo`.
379
+ - **flags** (`string[]`): Flags (`f=`): `donotmodify`, `donotexplode`, `exploded`, `feedback`, `feedhere`.
380
+ - **nonce** (`string`): A value for your own use (`n=`), at most 64 characters.
381
+ - **hashAlgorithms** (`string[]`): `sha256` (default) and/or `sha512`.
382
+ - **recipe** (`object`): When the message was changed since it was last signed, a Recipe that recreates the previous instance (section 5). The signer checks that the Recipe really recreates it before signing.
383
+ - **signTime** (`Date`): The signing time (`t=`). Defaults to now.
384
+
385
+ `dkim2Sign` resolves with `{ signatures, signature, messageInstance, i, m }` and rejects for invalid options, an invalid DKIM2 chain on the message, or a Recipe that does not fit. A new `Message-Instance` is only added when the message has no instance yet or has changed since the last one. `Dkim2SignStream` is a Transform stream that outputs the signed message, like `DkimSignStream`.
386
+
387
+ A forwarder that changes the message passes a Recipe. This mailing list replaced the `Subject` header field, added a `List-Id` header field, and added a footer after the first 3 body lines:
388
+
389
+ ```javascript
390
+ const { signatures } = await dkim2Sign(revisedMessage, {
391
+ signingDomain: 'list.example.org',
392
+ signatureData: [{ selector: 'list', privateKey }],
393
+ mailFrom: 'bounce@list.example.org',
394
+ rcptTo: 'member@example.net',
395
+ recipe: {
396
+ h: { subject: [{ d: ['Original subject'] }], 'list-id': [] },
397
+ b: [{ c: [1, 3] }]
398
+ }
399
+ });
400
+ ```
401
+
402
+ #### DKIM2 Verification
403
+
404
+ ```javascript
405
+ const { dkim2Verify } = require('mailauth');
406
+
407
+ const result = await dkim2Verify(message, {
408
+ // the SMTP envelope of the delivery, checked against the highest DKIM2-Signature (section 11.4)
409
+ mailFrom: 'bounce@list.example.org',
410
+ rcptTo: ['member@example.net']
411
+ });
412
+
413
+ console.log(result.status.result); // "pass", "fail", "permerror", "temperror" or "none"
414
+ console.log(result.info); // dkim2=pass (i=1 example.com pass, i=2 list.example.org pass) header.d=example.com
415
+ ```
416
+
417
+ - **resolver** (`async function`): Custom DNS resolver function.
418
+ - **mailFrom** / **rcptTo**: The SMTP envelope. The chain of custody check against the envelope, which is what stops DKIM2 replay, is only done for what is set here, as a library can not know the envelope otherwise. A missing part is reported in `status.warnings` as `mail-from-not-checked` or `rcpt-to-not-checked`, without changing the result. In `authenticate()` these come from the `sender` and `rcptTo` options.
419
+ - **maxSignatureAge** (`number|false`): Seconds after which a signature expires (section 11.3). Defaults to 14 days, `false` disables the check.
420
+ - **maxFutureTime** (`number|false`): Seconds a signature timestamp may be ahead of the current time, a later one is a PERMERROR (section 8.4). Defaults to `300` (5 minutes, for clock skew), `false` disables the check.
421
+ - **curTime** (`Date`): The time to check against. Defaults to now.
422
+ - **maxInstances** (`number`): The most `Message-Instance`, and the most `DKIM2-Signature`, header fields processed for one message. Defaults to `20`, more is a PERMERROR (BCP section 7.5).
423
+ - **checkReplay** (`async function`): Called with `{ key, exploded }`, where `key` identifies the message by its `m=1` hashes. Return `true` when a message with the same key was seen before, and the result is FAIL unless a signature has the `exploded` flag (section 11.9). Storing the keys is up to you.
424
+
425
+ The verifier recreates every instance of the message with the Recipes, checks every hash and signature, the chain of custody between the hops, the timestamps, and the `donotmodify` and `donotexplode` requests. Failures use the human-readable strings of section 11 of the specification. See [DKIM2 Result Reference](docs/dkim2.md) for the result object and for how ambiguities of the drafts are handled.
426
+
427
+ `dkim2Hash(message, { algorithms })` computes the header and body hashes of a message as they go into the `h=` tag of a `Message-Instance` header field, which helps when writing or checking a Recipe. It resolves with `{ headers, hashes }`, the names of the hashed header fields and `[{ algorithm, headerHash, bodyHash }]`.
428
+
429
+ With the CLI, `mailauth dkim2-sign` signs a message, `mailauth dkim2-verify` verifies it, `mailauth dkim2-hash` prints the hashes of its `Message-Instance`, and `mailauth report --dkim2 --sender <address> --rcpt-to <address>` includes DKIM2 in the report. See the [command-line documentation](cli.md#dkim2-sign).
430
+
331
431
  ### SPF
332
432
 
333
433
  #### SPF Verification
@@ -445,7 +545,7 @@ process.stdout.write(headers); // Includes terminating line break
445
545
  process.stdout.write(message);
446
546
  ```
447
547
 
448
- The sealing algorithm follows the key type (`rsa-sha256` or `ed25519-sha256`). A message whose newest ARC-Seal already says `cv=fail`, or whose chain already has 50 sets, is not sealed (RFC 8617 sections 5.1 and 4.2.1), and `arc.sealErrors` says why a requested seal was not added.
548
+ The sealing algorithm follows the key type (`rsa-sha256` or `ed25519-sha256`). A message with a failed ARC chain, including one that is malformed, is sealed with `cv=fail`. A message whose newest ARC-Seal already says `cv=fail` is not sealed again (RFC 8617 section 5.1 step 2). When a requested seal could not be added, for example because the chain already has 50 sets (RFC 8617 section 4.2.1), `arc.sealErrors` says why.
449
549
 
450
550
  ##### Sealing After Modifications
451
551
 
@@ -573,11 +673,11 @@ if (bimi?.location) {
573
673
  **Note:**
574
674
 
575
675
  - The `BIMI-Location` header is ignored by mailauth.
576
- - The `BIMI-Selector` header can be used for selector selection if available. A selector that is not found at the author domain is looked up at the Organizational Domain, it does not fall back to `default`.
676
+ - The `BIMI-Selector` header can be used for selector selection if available. A header without `v=BIMI1` or a valid selector is ignored. A selector that is not found at the author domain is looked up at the Organizational Domain, it does not fall back to `default`.
577
677
 
578
678
  #### Verified Mark Certificate (VMC)
579
679
 
580
- If an Authority Evidence Document is specified in the BIMI record, its location is available in `bimi.authority`. mailauth exposes the certificate type (`"VMC"` or `"CMC"`) in `bimi.authority.vmc.type`.
680
+ If an Authority Evidence Document is specified in the BIMI record, its location is available in `bimi.authority`. Use `validateVMC()` from `mailauth/lib/bimi` to download and validate the logo and the evidence document. Its result includes the BIMI headers for the message and an updated Authentication-Results entry in `info`, as `bimi.info` only covers record discovery. mailauth exposes the certificate type (`"VMC"` or `"CMC"`) in `bimi.authority.vmc.type`.
581
681
 
582
682
  **Example Authority Evidence Documents:**
583
683
 
package/bin/mailauth.js CHANGED
@@ -11,6 +11,9 @@ const commandSeal = require('../lib/commands/seal');
11
11
  const commandSpf = require('../lib/commands/spf');
12
12
  const commandVmc = require('../lib/commands/vmc');
13
13
  const commandBodyhash = require('../lib/commands/bodyhash');
14
+ const commandDkim2Sign = require('../lib/commands/dkim2-sign');
15
+ const commandDkim2Verify = require('../lib/commands/dkim2-verify');
16
+ const commandDkim2Hash = require('../lib/commands/dkim2-hash');
14
17
 
15
18
  const fs = require('node:fs');
16
19
  const pathlib = require('node:path');
@@ -70,6 +73,9 @@ const runCommand = (fn, failMessage) =>
70
73
  });
71
74
  };
72
75
 
76
+ // Collects the values of an option that can be repeated
77
+ const collect = (value, previous) => (previous || []).concat(value);
78
+
73
79
  const emailArgDescription = 'Path to the email message file in EML format. If not specified, the content is read from standard input.';
74
80
 
75
81
  const program = new Command();
@@ -105,6 +111,8 @@ program
105
111
  .option('-z, --max-void-lookups <number>', 'Maximum allowed DNS lookups that return no data (void lookups) during SPF checks. Defaults to 2.', numberArg, 2)
106
112
  .option('--strict', 'Follow the RFCs exactly instead of the lenient defaults (for example, reject rsa-sha1 DKIM signatures).')
107
113
  .option('--reject-rsa-sha1', 'Report rsa-sha1 DKIM signatures as dkim=policy and do not count them for DMARC, while keeping the other lenient defaults.')
114
+ .option('--dkim2', 'Verify DKIM2 header fields as well (experimental, built against draft-ietf-dkim-dkim2-spec-06).')
115
+ .option('-r, --rcpt-to <address>', 'Email address from a RCPT TO command, checked against the DKIM2 chain of custody. Can be repeated.', collect)
108
116
  .action(runCommand(commandReport, 'Failed to generate report for the input message.'));
109
117
 
110
118
  program
@@ -130,6 +138,60 @@ program
130
138
  .option('--strict', 'Follow the RFCs exactly (for example, refuse to sign with rsa-sha1 or with an RSA key shorter than 1024 bits).')
131
139
  .action(runCommand(commandSign, 'Failed to sign the input message.'));
132
140
 
141
+ program
142
+ .command('dkim2-sign')
143
+ .description('Sign an email with DKIM2 (experimental, built against draft-ietf-dkim-dkim2-spec-06)')
144
+ .argument('[email]', emailArgDescription)
145
+ .helpOption('--help', 'Show help.')
146
+ .requiredOption('-k, --private-key <file>', 'Path to a private key file. Repeat it together with --selector to sign with several keys.', collect)
147
+ .requiredOption('-s, --selector <selector>', 'Selector of the private key with the same position. Can be repeated.', collect)
148
+ .requiredOption('-d, --domain <domain>', 'Signing domain (d= tag).')
149
+ .option('-f, --mail-from <address>', 'MAIL FROM address the message is sent with (mf= tag). Use "<>" for the null sender.')
150
+ .option('-r, --rcpt-to <address>', 'RCPT TO address the message is sent to (rt= tag). Can be repeated.', collect)
151
+ .option('--next-domain <domain>', 'Domain of the next DKIM2 signature, instead of --mail-from and --rcpt-to (nd= tag).')
152
+ .option('--nonce <value>', 'Nonce value (n= tag).')
153
+ .option('--flag <flag>', 'Flag to set, such as "donotmodify" or "exploded" (f= tag). Can be repeated.', collect)
154
+ .option('--hash <algorithm>', 'Hash algorithm for the Message-Instance, "sha256" or "sha512". Can be repeated. Defaults to "sha256".', collect)
155
+ .option('--recipe <file>', 'Path to a JSON file with the Recipe that recreates the previous instance of a changed message.')
156
+ .option('-t, --time <timestamp>', 'Signing time as a UNIX timestamp (t= tag). Defaults to the current time.', numberArg)
157
+ .option('-o, --headers-only', 'If set, outputs only the DKIM2 header fields without the message.')
158
+ .hook('preAction', command => {
159
+ let opts = command.opts();
160
+ if (opts.nextDomain ? opts.mailFrom || opts.rcptTo : !opts.mailFrom || !opts.rcptTo) {
161
+ command.error('error: use either --mail-from and --rcpt-to, or --next-domain');
162
+ }
163
+ })
164
+ .action(runCommand(commandDkim2Sign, 'Failed to sign the input message with DKIM2.'));
165
+
166
+ program
167
+ .command('dkim2-verify')
168
+ .description('Verify the DKIM2 signatures of an email and return a JSON report (experimental, built against draft-ietf-dkim-dkim2-spec-06)')
169
+ .argument('[email]', emailArgDescription)
170
+ .helpOption('--help', 'Show help.')
171
+ .option(
172
+ '-f, --mail-from <address>',
173
+ 'MAIL FROM address the message was delivered with, "<>" for the null sender. Checked against the highest DKIM2-Signature.'
174
+ )
175
+ .option('-r, --rcpt-to <address>', 'RCPT TO address the message was delivered to, checked against the highest DKIM2-Signature. Can be repeated.', collect)
176
+ .option(
177
+ '-n, --dns-cache <file>',
178
+ 'Path to a JSON file with cached DNS responses. When provided, DNS queries use these cached responses instead of performing actual DNS lookups.'
179
+ )
180
+ .option('-t, --time <timestamp>', 'Time to verify against as a UNIX timestamp. Defaults to the current time.', numberArg)
181
+ .option('--max-age <seconds>', 'Seconds after which a signature expires, 0 to not check. Defaults to 14 days.', numberArg)
182
+ .option('--max-future <seconds>', 'Seconds a signature timestamp may be ahead of the current time, 0 to not check. Defaults to 300.', numberArg)
183
+ .option('--max-instances <number>', 'Most Message-Instance, and most DKIM2-Signature, header fields to process. Defaults to 20.', numberArg)
184
+ .option('-o, --headers-only', 'If set, outputs only the Authentication-Results entry (dkim2=...) instead of the JSON report.')
185
+ .action(runCommand(commandDkim2Verify, 'Failed to verify DKIM2 for the input message.'));
186
+
187
+ program
188
+ .command('dkim2-hash')
189
+ .description('Generate the DKIM2 header and body hashes of an email (the h= tag of a Message-Instance)')
190
+ .argument('[email]', emailArgDescription)
191
+ .helpOption('--help', 'Show help.')
192
+ .option('-a, --algo <algorithm>', 'Hash algorithm, "sha256" or "sha512". Can be repeated. Defaults to "sha256".', collect)
193
+ .action(runCommand(commandDkim2Hash, 'Failed to calculate the DKIM2 hashes for the input message.'));
194
+
133
195
  program
134
196
  .command('seal')
135
197
  .description('Authenticate and seal an email with an ARC digital signature')