mailauth 7.1.1 → 7.2.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.
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
+ }
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "7.1.1"
2
+ ".": "7.2.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.2.0](https://github.com/postalsys/mailauth/compare/v7.1.1...v7.2.0) (2026-10-09)
4
+
5
+
6
+ ### Features
7
+
8
+ * **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)
9
+ * **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)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **arc:** check a large authResults value without running out of stack ([624e8a3](https://github.com/postalsys/mailauth/commit/624e8a3bfd881d81f783f496a1ffc7c8a24bebba))
15
+ * **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)
16
+ * **arc:** fail a chain whose d= is not a host name ([2f0babe](https://github.com/postalsys/mailauth/commit/2f0babeac0fe41b17bde53c110d54c3f04a748b3))
17
+ * **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)
18
+ * **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)
19
+ * **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)
20
+ * **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)
21
+ * **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)
22
+ * **bimi:** count From addresses with the parser of the DMARC check ([0fb165e](https://github.com/postalsys/mailauth/commit/0fb165eb00a06b0fd372e7bb757e99528479f583))
23
+ * **bimi:** ignore an invalid BIMI-Selector header ([7f62164](https://github.com/postalsys/mailauth/commit/7f6216476b2eda34c91dde23f3c047e07ed67471)), closes [#167](https://github.com/postalsys/mailauth/issues/167)
24
+ * **bimi:** read the avp= avatar preference tag ([f577b50](https://github.com/postalsys/mailauth/commit/f577b5056476a5ffda325d44e7cfe0d7a8eb7547)), closes [#164](https://github.com/postalsys/mailauth/issues/164)
25
+ * **bimi:** report validateVMC() results in Authentication-Results ([6b03caa](https://github.com/postalsys/mailauth/commit/6b03caa9ffba2bc0e1d86177ad5a2b1f4f9d55d6)), closes [#168](https://github.com/postalsys/mailauth/issues/168)
26
+ * **bimi:** restrict logo and evidence downloads ([658d8c2](https://github.com/postalsys/mailauth/commit/658d8c22fb3087a37f5ccbd4f0b1e874d7f53fd2))
27
+ * **bimi:** validate logos against an SVG Tiny PS element allowlist ([0cf9679](https://github.com/postalsys/mailauth/commit/0cf9679c3fb185a63fd3541350a62cf68eda04c8))
28
+ * **bimi:** validate the logo from l= before building BIMI headers ([bdc7b90](https://github.com/postalsys/mailauth/commit/bdc7b907f329c1af1a983e7f845e901621aaee09))
29
+ * **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)
30
+ * **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)
31
+ * **dkim:** never align a signature when From has no single Author Domain ([ac882e2](https://github.com/postalsys/mailauth/commit/ac882e26bca1dd0c88ee7afc3cffecbb4d47db51))
32
+ * **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)
33
+ * **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)
34
+ * **dkim:** reject a d= that is not a host name and never align it ([387db0c](https://github.com/postalsys/mailauth/commit/387db0c639903fab54fa49d2674329664e7ffeca))
35
+ * **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)
36
+ * **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)
37
+ * **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)
38
+ * **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)
39
+ * **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)
40
+ * **dmarc:** discard record fragments without "=" ([0cdcf56](https://github.com/postalsys/mailauth/commit/0cdcf56e1ea9d55c4c614cd7214c74fc783729fb)), closes [#142](https://github.com/postalsys/mailauth/issues/142)
41
+ * **dmarc:** parse the From header by the RFC 5322 grammar ([08da841](https://github.com/postalsys/mailauth/commit/08da8419de7aa987cd04a366c3d04fe6b858d4d9))
42
+ * **dmarc:** reject a From local-part with a domain literal between its words ([ec0af9e](https://github.com/postalsys/mailauth/commit/ec0af9e7adc895170357e408c1598cbdb8f3fc03))
43
+ * **dmarc:** reject an Author Domain with control or format characters ([e1f1bb7](https://github.com/postalsys/mailauth/commit/e1f1bb7e79327e9cc92d665cb85c645cc36ab35f))
44
+ * **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)
45
+ * **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)
46
+ * **received:** do not guess the client IP when the HELO adds ";" or Exim comments ([49b9a45](https://github.com/postalsys/mailauth/commit/49b9a45422aa8973e4c8b0de7004bfd4ef705a20))
47
+ * **received:** take the client IP only from an unambiguous from clause ([be23ede](https://github.com/postalsys/mailauth/commit/be23edebe3115040367fc77f0c30323fa6726f70))
48
+ * **spf:** accept 0 for maxVoidCount and maxResolveCount ([e1826d7](https://github.com/postalsys/mailauth/commit/e1826d7a25d5f3121b109bed467b4e3834100ded)), closes [#158](https://github.com/postalsys/mailauth/issues/158)
49
+ * **spf:** accept a MAIL FROM or HELO domain with a single trailing dot ([11256ae](https://github.com/postalsys/mailauth/commit/11256ae23a8c653394e5998e7c39c58b5e0ff04d))
50
+ * **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)
51
+ * **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)
52
+ * **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)
53
+ * **spf:** split macro values on exactly the delimiters that are listed ([98c32c0](https://github.com/postalsys/mailauth/commit/98c32c09054a6651cacda089c07ddd22ff5fd470))
54
+ * **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)
55
+
3
56
  ## [7.1.1](https://github.com/postalsys/mailauth/compare/v7.1.0...v7.1.1) (2026-10-04)
4
57
 
5
58
 
package/README.md CHANGED
@@ -82,21 +82,21 @@ await authenticate(message [, options])
82
82
  - **sender** (`string`): Email address from the MAIL FROM command. Defaults to the `Return-Path` header if not set.
83
83
  - **ip** (`string`): IP address of the remote client that sent the message.
84
84
  - **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`.
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. 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
86
  - **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
87
  - **minBitLength** (`number`): Minimum allowed bits for RSA public keys. Defaults to `1024`. Keys with fewer bits will fail validation.
88
88
  - **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
89
  - **disableArc** (`boolean`): If `true`, skips ARC checks.
90
90
  - **disableDmarc** (`boolean`): If `true`, skips DMARC checks, also disabling dependent checks like BIMI.
91
91
  - **disableBimi** (`boolean`): If `true`, skips BIMI checks.
92
- - **seal** (`object`): Options for ARC sealing if the message doesn't have a broken ARC chain.
92
+ - **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
93
  - **signingDomain** (`string`): ARC key domain name.
94
94
  - **selector** (`string`): ARC key selector.
95
95
  - **privateKey** (`string` or `Buffer`): Private key for signing (RSA or Ed25519).
96
96
  - **resolver** (`async function`): Custom DNS resolver function. Defaults to [`dns.promises.resolve`](https://nodejs.org/api/dns.html#dns_dnspromises_resolve_hostname_rrtype).
97
97
  - **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
98
  - **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).
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. Defaults to `false`. See [Strict mode](#strict-mode).
100
100
 
101
101
  #### Example
102
102
 
@@ -144,7 +144,7 @@ You can see the full output, including structured data for DKIM, SPF, DMARC, and
144
144
 
145
145
  **Note:** The `receivedChain` property is an array of parsed representations of the `Received:` headers.
146
146
 
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.
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 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
148
 
149
149
  **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
150
 
@@ -154,27 +154,28 @@ By default mailauth is lenient where accepting non-conforming input costs little
154
154
 
155
155
  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
156
 
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.
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, 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
158
 
159
159
  DKIM differences between the modes:
160
160
 
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.
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/upper case tags, bad `c=`/`t=`/`x=`/`l=`, `l=` > body | 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
+ | Ed25519 key published as SubjectPublicKeyInfo | used, warning `key-ed25519-spki` | key ignored |
171
+ | Body hash mismatch | `neutral` | `fail` |
172
+ | Signature with an unknown algorithm or no `d=`/`s=` | left out of the results | `neutral` |
173
+ | `header.i` in Authentication-Results | always `@d` | the AUID (`i=` or `@d`), plus `header.d` |
174
+ | 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) |
175
+ | Header order | Received-SPF above Authentication-Results | Authentication-Results on top (RFC 8601 section 5) |
176
+ | Signing with rsa-sha1 or an RSA key under 1024 bits | signed, warning `rsa-sha1`/`weak-key` | error |
177
+
178
+ 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
179
 
179
180
  ### DKIM
180
181
 
@@ -191,26 +192,26 @@ const signResult = await dkimSign(message, options);
191
192
  // Returns: { signatures: String, errors: Array, warnings: Array }
192
193
  ```
193
194
 
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`).
195
+ `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
196
 
196
197
  ##### Parameters
197
198
 
198
199
  - **message**: A `String`, `Buffer`, or `Readable` stream representing the email message.
199
200
  - **options**:
200
- - **canonicalization** (`string`): Canonicalization method. Defaults to `'relaxed/relaxed'`.
201
+ - **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
202
  - **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
203
  - **signTime** (`Date`): Signing time. Defaults to current time.
203
204
  - **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.
205
+ - **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
206
  - **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:
207
+ - **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
208
  - **signingDomain** (`string`): DKIM key domain name. An internationalized domain name is converted to A-labels.
208
209
  - **selector** (`string`): DKIM key selector.
209
- - **privateKey** (`string` or `Buffer`): Private key for signing (RSA or Ed25519).
210
+ - **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
211
  - **algorithm** (`string`, optional): Overrides parent `algorithm`.
211
212
  - **canonicalization** (`string`, optional): Overrides parent `canonicalization`.
212
213
  - **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.
214
+ - **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
215
  - **maxBodyLength** (`number`, optional): Maximum number of canonicalized body bytes to sign (`l=` tag). Not recommended for general use.
215
216
 
216
217
  ##### Example
@@ -445,7 +446,7 @@ process.stdout.write(headers); // Includes terminating line break
445
446
  process.stdout.write(message);
446
447
  ```
447
448
 
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.
449
+ 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
450
 
450
451
  ##### Sealing After Modifications
451
452
 
@@ -573,11 +574,11 @@ if (bimi?.location) {
573
574
  **Note:**
574
575
 
575
576
  - 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`.
577
+ - 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
578
 
578
579
  #### Verified Mark Certificate (VMC)
579
580
 
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`.
581
+ 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
582
 
582
583
  **Example Authority Evidence Documents:**
583
584
 
package/cli.md CHANGED
@@ -185,7 +185,7 @@ mailauth seal [options] [email]
185
185
 
186
186
  By default, `seal` authenticates the message (SPF, DKIM, DMARC, ARC) and embeds the computed results in the `ARC-Authentication-Results` header. If the message was already authenticated elsewhere (for example at an edge MTA) and modified afterwards, you can instead provide the original `Authentication-Results` value yourself. When one of the following options is set, no authentication checks or DNS lookups are performed:
187
187
 
188
- - `--auth-results "authserv-id; spf=pass ..."`: `Authentication-Results` value to embed in the `ARC-Authentication-Results` header (the part after `i=N;`). Used as is.
188
+ - `--auth-results "authserv-id; spf=pass ..."`: `Authentication-Results` value to embed in the `ARC-Authentication-Results` header (the part after `i=N;`). Used as is, except that line breaks are written as CRLF. A multi-line value must be folded: every line after the first starts with a space or a tab. A value with any other line break is refused, as it would start a new header field.
189
189
  - `--auth-results-file /path/to/value.txt`: Same as `--auth-results`, but the value is read from a file. Useful for long or multi-line values. Cannot be combined with `--auth-results`.
190
190
  - `--cv status`: Chain validation status for the `ARC-Seal` header (`cv=` tag): `none`, `pass`, or `fail`. Defaults to `none`.
191
191
  - `--instance number`: ARC instance number (`i=` tag). Defaults to the next instance number based on the existing ARC chain of the message, or `1`.
package/docs/arc.md CHANGED
@@ -31,13 +31,13 @@ With `strict: true` the validation and sealing follow RFC 8617 exactly, see [Str
31
31
 
32
32
  ## status Object
33
33
 
34
- | Field | Type | Presence | Description |
35
- | ------------ | --------- | ------------ | ---------------------------------------------------------------- |
36
- | `result` | `string` | Always | ARC result code (see below) |
37
- | `comment` | `string` | On pass/fail | Description or error message |
38
- | `shouldSeal` | `boolean` | On fail | Whether to continue sealing despite failure |
39
- | `policy` | `object` | Policy issue | Policy violation details (e.g., `{"dkim-rules": "weak-key"}`) |
40
- | `smtp` | `object` | Strict mode | `{"remote-ip": "192.0.2.1"}`, the client IP (RFC 8617 section 6) |
34
+ | Field | Type | Presence | Description |
35
+ | ------------ | --------- | ------------ | -------------------------------------------------------------------------------------------------------- |
36
+ | `result` | `string` | Always | ARC result code (see below) |
37
+ | `comment` | `string` | On pass/fail | Description or error message |
38
+ | `shouldSeal` | `boolean` | On fail | Whether the failed chain is sealed with `cv=fail`, false when its newest ARC-Seal already says `cv=fail` |
39
+ | `policy` | `object` | Policy issue | Policy violation details (e.g., `{"dkim-rules": "weak-key"}`) |
40
+ | `smtp` | `object` | Strict mode | `{"remote-ip": "192.0.2.1"}`, the client IP (RFC 8617 section 6) |
41
41
 
42
42
  ## authenticationResults Object
43
43
 
@@ -61,28 +61,29 @@ When ARC validation passes, the `authenticationResults` object contains parsed r
61
61
 
62
62
  ## Comment Values (on failure)
63
63
 
64
- | Comment Pattern | Description |
65
- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
66
- | `"i={n} seal signature validation failed"` | ARC-Seal cryptographic verification failed |
67
- | `"i={n} no valid signature"` | ARC-Message-Signature verification failed |
68
- | `"i={n} multiple {header} values"` | Duplicate ARC headers for same instance |
69
- | `"chain-length={n}"` | Chain exceeds 50 instances |
70
- | `"i={n} expected={m}"` | Missing or out-of-order instance numbers |
71
- | `"i={n} no {header} set"` | Missing required ARC header |
72
- | `"i=1 cv={value}"` | First instance must have `cv=none` |
73
- | `"i={n} cv={value}"` | Non-first instance must have `cv=pass`, and a newest seal with `cv=fail` fails the chain |
74
- | `"i={n} unexpected as h"` | ARC-Seal has an `h=` tag |
75
- | `"i={n} invalid ams h"` | ARC-Message-Signature signed arc-seal (forbidden) |
76
- | `"i={n} as {problem}"`, `"i={n} ams {problem}"` | Strict mode: a tag-list syntax error, a missing required tag or a malformed `t=`/`x=`/`l=` |
77
- | `"invalid as instance"`, `"invalid ams instance"`, `"invalid aar instance"` | Strict mode: an ARC header field whose `i=` is not 1 to 50 written as 1*2DIGIT (first in ARC-Authentication-Results) |
78
- | `"no key for {domain}"` | DNS key not found |
79
- | `"unknown key version for {domain}"` | Key record `v=` is not `DKIM1` |
80
- | `"unknown key type for {domain}"` | Unsupported key type |
81
- | `"invalid public key for {domain}"` | Malformed public key |
82
- | `"inappropriate hash algorithm for {domain}"` | Key record `h=` does not list sha256 |
83
- | `"key not for email for {domain}"` | Key record `s=` does not list email |
84
- | `"inappropriate key algorithm for {domain}"` | The key type does not match the seal's `a=` algorithm |
85
- | `"weak key for {domain}"` | RSA key too short |
64
+ | Comment Pattern | Description |
65
+ | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
66
+ | `"i={n} seal signature validation failed"` | ARC-Seal cryptographic verification failed |
67
+ | `"i={n} no valid signature"` | ARC-Message-Signature verification failed |
68
+ | `"i={n} multiple {header} values"` | Duplicate ARC headers for same instance |
69
+ | `"chain-length={n}"` | Chain exceeds 50 instances |
70
+ | `"i={n} expected={m}"` | Missing or out-of-order instance numbers |
71
+ | `"i={n} no {header} set"` | Missing required ARC header |
72
+ | `"i=1 cv={value}"` | First instance must have `cv=none` |
73
+ | `"i={n} cv={value}"` | Non-first instance must have `cv=pass`, and a newest seal with `cv=fail` fails the chain |
74
+ | `"i={n} unexpected as h"` | ARC-Seal has an `h=` tag |
75
+ | `"i={n} invalid ams h"` | ARC-Message-Signature signed arc-seal (forbidden) |
76
+ | `"i={n} as {problem}"`, `"i={n} ams {problem}"` | Strict mode: a tag-list syntax error, a missing required tag or a malformed `t=`/`x=`/`l=` |
77
+ | `"i={n} as invalid d="`, `"i={n} ams invalid d="` | Both modes: an ARC-Seal or the newest ARC-Message-Signature has a `d=` with a character that can not be in a host name |
78
+ | `"invalid as instance"`, `"invalid ams instance"`, `"invalid aar instance"` | Strict mode: an ARC header field whose `i=` is not 1 to 50 written as 1*2DIGIT (first in ARC-Authentication-Results) |
79
+ | `"no key for {domain}"` | DNS key not found |
80
+ | `"unknown key version for {domain}"` | Key record `v=` is not `DKIM1` |
81
+ | `"unknown key type for {domain}"` | Unsupported key type |
82
+ | `"invalid public key for {domain}"` | Malformed public key |
83
+ | `"inappropriate hash algorithm for {domain}"` | Key record `h=` does not list sha256 |
84
+ | `"key not for email for {domain}"` | Key record `s=` does not list email |
85
+ | `"inappropriate key algorithm for {domain}"` | The key type does not match the seal's `a=` algorithm |
86
+ | `"weak key for {domain}"` | RSA key too short |
86
87
 
87
88
  An ARC-Message-Signature that does not sign the From header field is not valid (RFC 6376 section 6.1.1), which fails the chain with `"i={n} no valid signature"` in both modes. An ARC-Seal `c=` tag is not defined by RFC 8617, so it is ignored like any other unknown tag.
88
89
 
@@ -94,6 +95,7 @@ These rules apply in both modes:
94
95
  - An Ed25519 seal signs the SHA-256 hash of the canonicalized data (RFC 8463).
95
96
  - The seal's `a=` algorithm has to match the key type, and the key record's `v=`, `h=` and `s=` tags are honored.
96
97
  - An ARC-Message-Signature without `c=` uses `simple/simple`, the DKIM-Signature default.
98
+ - A `d=` with a character that can not be in a host name (such as `/`, `?`, `#`, `@` or `:`) in any ARC-Seal or in the newest ARC-Message-Signature fails the chain, and its key is not looked up.
97
99
 
98
100
  Differences between the modes:
99
101
 
@@ -115,12 +117,12 @@ The position of `i=` in an ARC-Seal or ARC-Message-Signature is not checked in e
115
117
 
116
118
  In the default mode `warnings` lists what was accepted although strict mode would have rejected it. It is only present when there is something to report and the chain did not fail, and it is never written into the Authentication-Results text.
117
119
 
118
- | Warning | Meaning |
119
- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
120
- | `arc-tag-syntax` | An ARC-Seal or the newest ARC-Message-Signature has a tag-list syntax error or an upper case tag name |
121
- | `arc-instance-syntax` | An `i=` value is not valid RFC 8617 syntax, or an ARC header field was ignored because of its `i=` |
122
- | `ams-c-default` | The ARC-Message-Signature has no `c=` and only verified with `relaxed/relaxed` |
123
- | `key-syntax`, `key-v-syntax`, `key-type-inferred` | Key record leniencies, see the [DKIM result reference](dkim.md#warnings) |
120
+ | Warning | Meaning |
121
+ | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
122
+ | `arc-tag-syntax` | An ARC-Seal or the newest ARC-Message-Signature has a tag-list syntax error or an upper case tag name |
123
+ | `arc-instance-syntax` | An `i=` value is not valid RFC 8617 syntax, or an ARC header field was ignored because of its `i=` |
124
+ | `ams-c-default` | The ARC-Message-Signature has no `c=` and only verified with `relaxed/relaxed` |
125
+ | `key-syntax`, `key-v-syntax`, `key-type-inferred`, `key-ed25519-spki` | Key record leniencies, see the [DKIM result reference](dkim.md#warnings) |
124
126
 
125
127
  ## Sealing
126
128
 
@@ -131,31 +133,37 @@ In the default mode `warnings` lists what was accepted although strict mode woul
131
133
  | `headers` | `string[]` | ARC headers to prepend: `[ARC-Seal, ARC-Message-Signature, ARC-Authentication-Results]`, or empty |
132
134
  | `errors` | `array` | `{ err, type, selector, signingDomain }` entries saying why no set was created |
133
135
  | `warnings` | `string[]` | What the default mode sealed that strict mode would have refused |
136
+ | `instance` | `number` | The instance (`i=`) of the created set, not set when no set was created |
134
137
 
135
138
  Seal options:
136
139
 
137
- | Option | Type | Description |
138
- | --------------- | ------------------ | ---------------------------------------------------------------------------------------------------------- |
139
- | `signingDomain` | `string` | `d=` value |
140
- | `selector` | `string` | `s=` value |
141
- | `privateKey` | `string\|Buffer` | RSA or Ed25519 private key |
142
- | `algorithm` | `string` | `rsa-sha256` or `ed25519-sha256`, follows the key type when not set |
143
- | `authResults` | `string` | The Authentication-Results payload for the ARC-Authentication-Results header |
144
- | `cv` | `string` | `none`, `pass` or `fail`. Defaults to `none` for the first set, required for later ones |
145
- | `i` | `number` | Instance, defaults to one more than the highest instance on the message |
146
- | `headerList` | `string\|string[]` | Header fields for the ARC-Message-Signature. ARC header fields and Authentication-Results are never signed |
147
- | `signTime` | `Date` | Signing time |
148
- | `strict` | `boolean` | Follow the RFCs exactly |
140
+ | Option | Type | Description |
141
+ | --------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------- |
142
+ | `signingDomain` | `string` | `d=` value |
143
+ | `selector` | `string` | `s=` value |
144
+ | `privateKey` | `string\|Buffer` | RSA or Ed25519 private key |
145
+ | `algorithm` | `string` | `rsa-sha256` or `ed25519-sha256`, follows the key type when not set |
146
+ | `authResults` | `string` | The Authentication-Results payload for the ARC-Authentication-Results header (required). Line breaks only as CRLF folding |
147
+ | `cv` | `string` | `none`, `pass` or `fail`. Defaults to `none` for the first set, required for later ones |
148
+ | `i` | `number` | Instance, defaults to one more than the highest instance on the message |
149
+ | `headerList` | `string\|string[]` | Header fields for the ARC-Message-Signature. ARC header fields and Authentication-Results are never signed |
150
+ | `signTime` | `Date` | Signing time |
151
+ | `strict` | `boolean` | Follow the RFCs exactly |
152
+
153
+ `authenticate()` always computes `authResults`, `cv` and `i` for the message it seals and ignores these values in its `seal` option. The seal options object is never modified, so one object can be shared by concurrent calls.
154
+
155
+ A name repeated in `headerList` is over-signed in the ARC-Message-Signature, as with `dkimSign`.
149
156
 
150
157
  No ARC set is created (in either mode) when:
151
158
 
152
- | Error code | Reason |
153
- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
154
- | `EARCCHAINFAILED` | The newest ARC-Seal on the message already has `cv=fail` (RFC 8617 section 5.1 step 2) |
155
- | `EINVALIDINSTANCE` | The instance is not 1 to 50, already exists on the message, or the chain already has 50 sets |
156
- | `EINVALIDCV` | `cv` is not `none`, `pass` or `fail`, is missing for an instance above 1, or is `pass` while the chain on the message could not be parsed |
157
- | `EINVALIDALGO` | The algorithm is not `rsa-sha256` or `ed25519-sha256` |
158
- | `EINVALIDTYPE` | The algorithm does not match the key type |
159
+ | Error code | Reason |
160
+ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
161
+ | `EARCCHAINFAILED` | The newest ARC-Seal on the message already has `cv=fail` (RFC 8617 section 5.1 step 2) |
162
+ | `EINVALIDINSTANCE` | The instance is not 1 to 50, already exists on the message, or the chain already has 50 sets |
163
+ | `EINVALIDCV` | `cv` is not `none`, `pass` or `fail`, is missing for an instance above 1, or is `pass` while the chain on the message could not be parsed |
164
+ | `EINVALIDALGO` | The algorithm is not `rsa-sha256` or `ed25519-sha256` |
165
+ | `EINVALIDAUTHRESULTS` | `authResults` is missing or empty, or has a line break that is not folding (CRLF followed by a space or tab and more text) |
166
+ | `EINVALIDTYPE` | The algorithm does not match the key type |
159
167
 
160
168
  What only strict mode refuses (the default mode seals and adds the warning):
161
169