mailauth 6.0.0 → 7.1.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 (47) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +113 -39
  4. package/bin/mailauth.js +15 -6
  5. package/cli.md +9 -3
  6. package/docs/arc.md +126 -43
  7. package/docs/bimi.md +54 -37
  8. package/docs/dkim.md +79 -40
  9. package/docs/dmarc.md +80 -33
  10. package/docs/mta-sts.md +140 -42
  11. package/docs/spf.md +69 -4
  12. package/index.d.ts +508 -43
  13. package/lib/arc/index.d.ts +10 -8
  14. package/lib/arc/index.js +505 -124
  15. package/lib/bimi/index.js +162 -81
  16. package/lib/commands/report.js +3 -1
  17. package/lib/commands/seal.js +35 -5
  18. package/lib/commands/sign.js +8 -1
  19. package/lib/commands/spf.js +8 -0
  20. package/lib/dkim/body/relaxed.js +6 -5
  21. package/lib/dkim/body/simple.js +1 -3
  22. package/lib/dkim/dkim-signer.js +219 -33
  23. package/lib/dkim/dkim-verifier.js +577 -231
  24. package/lib/dkim/header/relaxed.js +6 -43
  25. package/lib/dkim/header/signature-opts.js +87 -0
  26. package/lib/dkim/header/simple.js +6 -43
  27. package/lib/dkim/message-parser.js +18 -4
  28. package/lib/dkim/sign.d.ts +13 -7
  29. package/lib/dkim/sign.js +18 -2
  30. package/lib/dkim/syntax.js +31 -0
  31. package/lib/dkim/verify.js +1 -0
  32. package/lib/dmarc/author-domain.js +181 -0
  33. package/lib/dmarc/get-dmarc-record.js +154 -47
  34. package/lib/dmarc/index.d.ts +29 -2
  35. package/lib/dmarc/verify.js +120 -44
  36. package/lib/mailauth.js +77 -36
  37. package/lib/mta-sts.d.ts +77 -11
  38. package/lib/mta-sts.js +497 -182
  39. package/lib/parse-dkim-headers.js +376 -191
  40. package/lib/parse-received.js +104 -32
  41. package/lib/spf/index.d.ts +37 -1
  42. package/lib/spf/index.js +260 -28
  43. package/lib/spf/macro.js +50 -13
  44. package/lib/spf/spf-verify.js +440 -204
  45. package/lib/spf/syntax.js +366 -0
  46. package/lib/tools.js +522 -116
  47. package/package.json +1 -1
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "6.0.0"
2
+ ".": "7.1.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.1.0](https://github.com/postalsys/mailauth/compare/v7.0.0...v7.1.0) (2026-09-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * **cli:** add --reject-rsa-sha1 to the report command ([#140](https://github.com/postalsys/mailauth/issues/140)) ([fc8e965](https://github.com/postalsys/mailauth/commit/fc8e9651e304ff6d9f3249e2f3c1db7e481c7d8c))
9
+ * **dkim:** add rejectRsaSha1 to reject rsa-sha1 signatures without the strict mode ([#138](https://github.com/postalsys/mailauth/issues/138)) ([92d2c23](https://github.com/postalsys/mailauth/commit/92d2c23027d443dc62dc93a49b54d561beb3f2ed))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **dmarc:** count a subdomain with a dangling CNAME as existing for np ([#137](https://github.com/postalsys/mailauth/issues/137)) ([b38ecef](https://github.com/postalsys/mailauth/commit/b38ecef53b56e479863dd96cae1a6fe24c970353))
15
+
16
+ ## [7.0.0](https://github.com/postalsys/mailauth/compare/v6.0.0...v7.0.0) (2026-09-27)
17
+
18
+
19
+ ### ⚠ BREAKING CHANGES
20
+
21
+ * **dmarc:** the DMARC result no longer has pct, which is historic in RFC 9989, and BIMI no longer reads it. policy can differ from before for non-existent subdomains (np), for t=y records, and for records with an invalid p, sp or np.
22
+
23
+ ### Features
24
+
25
+ * **dmarc:** apply np, t and the RFC 9989 rules for invalid policies ([c11740c](https://github.com/postalsys/mailauth/commit/c11740c0c9b18a9329e82c5eacfb63cfb3a3420a))
26
+ * fix RFC compliance findings and add a strict mode ([#136](https://github.com/postalsys/mailauth/issues/136)) ([62e0dd1](https://github.com/postalsys/mailauth/commit/62e0dd16359b3964cb579230035a980bf054a06f))
27
+
3
28
  ## [6.0.0](https://github.com/postalsys/mailauth/compare/v5.0.3...v6.0.0) (2026-09-26)
4
29
 
5
30
 
package/README.md CHANGED
@@ -26,6 +26,7 @@ mailauth is a pure JavaScript implementation, requiring no external applications
26
26
  2. [Command-Line Usage](#command-line-usage)
27
27
  3. [Library Usage](#library-usage)
28
28
  - [Authentication](#authentication)
29
+ - [Strict mode](#strict-mode)
29
30
  - [DKIM](#dkim)
30
31
  - [Signing](#dkim-signing)
31
32
  - [Verification](#dkim-verification)
@@ -71,7 +72,7 @@ Use the `authenticate` function to validate DKIM signatures, SPF, DMARC, ARC, an
71
72
 
72
73
  ```javascript
73
74
  await authenticate(message [, options])
74
- // Returns: { dkim, spf, arc, dmarc, bimi, receivedChain, headers }
75
+ // Returns: { dkim, spf, arc, dmarc, bimi, receivedChain, headers [, dmarcSkipReason] }
75
76
  ```
76
77
 
77
78
  #### Parameters
@@ -81,9 +82,10 @@ await authenticate(message [, options])
81
82
  - **sender** (`string`): Email address from the MAIL FROM command. Defaults to the `Return-Path` header if not set.
82
83
  - **ip** (`string`): IP address of the remote client that sent the message.
83
84
  - **helo** (`string`): Hostname from the HELO/EHLO command.
84
- - **trustReceived** (`boolean`): If `true`, parses `ip` and `helo` from the latest `Received` header if not provided. Defaults to `false`.
85
- - **mta** (`string`): Hostname of the server performing the authentication. Defaults to `os.hostname()`. Included in Authentication headers.
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`.
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.
86
87
  - **minBitLength** (`number`): Minimum allowed bits for RSA public keys. Defaults to `1024`. Keys with fewer bits will fail validation.
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.
87
89
  - **disableArc** (`boolean`): If `true`, skips ARC checks.
88
90
  - **disableDmarc** (`boolean`): If `true`, skips DMARC checks, also disabling dependent checks like BIMI.
89
91
  - **disableBimi** (`boolean`): If `true`, skips BIMI checks.
@@ -94,6 +96,7 @@ await authenticate(message [, options])
94
96
  - **resolver** (`async function`): Custom DNS resolver function. Defaults to [`dns.promises.resolve`](https://nodejs.org/api/dns.html#dns_dnspromises_resolve_hostname_rrtype).
95
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).
96
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).
97
100
 
98
101
  #### Example
99
102
 
@@ -141,6 +144,38 @@ You can see the full output, including structured data for DKIM, SPF, DMARC, and
141
144
 
142
145
  **Note:** The `receivedChain` property is an array of parsed representations of the `Received:` headers.
143
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.
148
+
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
+
151
+ #### Strict mode
152
+
153
+ By default mailauth is lenient where accepting non-conforming input costs little security, mostly weak signatures that still bind the signing domain to the content. With `strict: true` every check follows its RFC exactly.
154
+
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
+
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.
158
+
159
+ DKIM differences between the modes:
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.
178
+
144
179
  ### DKIM
145
180
 
146
181
  #### DKIM Signing
@@ -153,24 +188,29 @@ Use the `dkimSign` function to sign an email message with DKIM.
153
188
  const { dkimSign } = require('mailauth/lib/dkim/sign');
154
189
 
155
190
  const signResult = await dkimSign(message, options);
156
- // Returns: { signatures: String, errors: Array }
191
+ // Returns: { signatures: String, errors: Array, warnings: Array }
157
192
  ```
158
193
 
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
+
159
196
  ##### Parameters
160
197
 
161
198
  - **message**: A `String`, `Buffer`, or `Readable` stream representing the email message.
162
199
  - **options**:
163
200
  - **canonicalization** (`string`): Canonicalization method. Defaults to `'relaxed/relaxed'`.
164
- - **algorithm** (`string`): Signing and hashing algorithm. Defaults to `'rsa-sha256'`.
201
+ - **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.
165
202
  - **signTime** (`Date`): Signing time. Defaults to current time.
166
- - **expires** (`Date`): Signature expiration time (`x=` tag). Optional.
167
- - **headerList** (`Array`): List of header field names to sign. Optional; uses default set if not specified.
203
+ - **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
+ - **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.
168
206
  - **signatureData** (`Array`): Array of signature objects. Each object may contain:
169
- - **signingDomain** (`string`): DKIM key domain name.
207
+ - **signingDomain** (`string`): DKIM key domain name. An internationalized domain name is converted to A-labels.
170
208
  - **selector** (`string`): DKIM key selector.
171
209
  - **privateKey** (`string` or `Buffer`): Private key for signing (RSA or Ed25519).
172
210
  - **algorithm** (`string`, optional): Overrides parent `algorithm`.
173
211
  - **canonicalization** (`string`, optional): Overrides parent `canonicalization`.
212
+ - **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.
174
214
  - **maxBodyLength** (`number`, optional): Maximum number of canonicalized body bytes to sign (`l=` tag). Not recommended for general use.
175
215
 
176
216
  ##### Example
@@ -239,6 +279,8 @@ const dkimSignStream = new DkimSignStream({
239
279
  process.stdin.pipe(dkimSignStream).pipe(process.stdout);
240
280
  ```
241
281
 
282
+ When no signature can be created the message is passed through unchanged. Once the stream has ended, `dkimSignStream.errors` and `dkimSignStream.warnings` hold the same values as the `dkimSign` result.
283
+
242
284
  #### DKIM Verification
243
285
 
244
286
  Use the `dkimVerify` function to verify DKIM signatures in an email message.
@@ -248,10 +290,20 @@ Use the `dkimVerify` function to verify DKIM signatures in an email message.
248
290
  ```javascript
249
291
  const { dkimVerify } = require('mailauth/lib/dkim/verify');
250
292
 
251
- const result = await dkimVerify(message);
293
+ const result = await dkimVerify(message [, options]);
252
294
  // Returns an object containing verification results
253
295
  ```
254
296
 
297
+ - **options** (optional):
298
+ - **resolver** (`async function`): Custom DNS resolver function. Defaults to `dns.promises.resolve`. A U-label signing domain is converted to A-labels before it is looked up (RFC 8616).
299
+ - **minBitLength** (`number`): Minimum allowed bits for RSA public keys. Defaults to `1024`. A shorter key gives `dkim=policy` with `policy.dkim-rules=weak-key`.
300
+ - **rejectRsaSha1** (`boolean`): If `true`, an rsa-sha1 signature gives `dkim=policy` with `policy.dkim-rules=weak-algorithm`, as in strict mode, while every other check keeps the lenient default. Signing is not affected. Defaults to `false`, and `strict` implies it.
301
+ - **sender** (`string`): Envelope sender. Defaults to the `Return-Path` header.
302
+ - **curTime** (`Date`): The time to check `t=` and `x=` against. Defaults to now.
303
+ - **strict** (`boolean`): If `true`, follows RFC 6376, RFC 8301 and RFC 8463 exactly. Defaults to `false`. See [Strict mode](#strict-mode).
304
+
305
+ Whatever the mode, a signature that does not sign the From header is reported as `dkim=neutral (From field not signed)`, and a key record whose `h=` does not list the hash algorithm of the signature, or whose `s=` does not include `email`, is not used. A key record with `t=y` (testing) sets `status.testing`.
306
+
255
307
  See [DKIM Result Reference](docs/dkim.md) for details on the result object structure.
256
308
 
257
309
  ##### Example
@@ -300,6 +352,13 @@ See [SPF Result Reference](docs/spf.md) for details on the result object structu
300
352
  - **ip** (`string`): SMTP client IP.
301
353
  - **helo** (`string`): HELO/EHLO hostname.
302
354
  - **mta** (`string`): Hostname of the MTA performing the check.
355
+ - **resolver** (`function`): Custom DNS resolver, same signature as `dns.promises.resolve`.
356
+ - **maxResolveCount** (`number`): Maximum DNS lookups, defaults to `10`.
357
+ - **maxVoidCount** (`number`): Maximum void DNS lookups, defaults to `2`.
358
+ - **maxElapsedTime** (`number`): Time limit in milliseconds for the whole evaluation. If exceeded, the result is `temperror`. No limit by default.
359
+ - **strict** (`boolean`): If `true`, follow RFC 7208 exactly: the whole record is validated before evaluation, void lookups are counted as the RFC does, and Authentication-Results reports only the checked identity. Defaults to `false`. See [Strict Mode](docs/spf.md#strict-mode).
360
+
361
+ In the default mode, input that strict mode would reject is listed in the `warnings` array of the result. For a `fail` result, the `explanation` property holds the text published with the `exp` modifier, if any.
303
362
 
304
363
  ##### Example
305
364
 
@@ -319,9 +378,8 @@ console.log(result.header);
319
378
  **Sample Output:**
320
379
 
321
380
  ```
322
- Received-SPF: pass (mx.myhost.com: domain of andris@wildduck.email
323
- designates 217.146.76.20 as permitted sender) client-ip=217.146.76.20;
324
- envelope-from="andris@wildduck.email";
381
+ Received-SPF: pass (mx.myhost.com: domain of andris@wildduck.email designates 217.146.76.20 as permitted sender) client-ip=217.146.76.20;
382
+ envelope-from="andris@wildduck.email"; helo=foo;
325
383
  ```
326
384
 
327
385
  ### ARC
@@ -344,7 +402,7 @@ const { arc } = await authenticate(message, {
344
402
  console.log(arc);
345
403
  ```
346
404
 
347
- See [ARC Result Reference](docs/arc.md) for details on the result object structure.
405
+ See [ARC Result Reference](docs/arc.md) for details on the result object structure, and for what `strict: true` changes.
348
406
 
349
407
  **Sample Output:**
350
408
 
@@ -387,6 +445,8 @@ process.stdout.write(headers); // Includes terminating line break
387
445
  process.stdout.write(message);
388
446
  ```
389
447
 
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
+
390
450
  ##### Sealing After Modifications
391
451
 
392
452
  If you need to modify the message before sealing, first authenticate it, modify as needed, then seal using the authentication results.
@@ -417,6 +477,8 @@ const sealHeaders = await sealMessage(message, {
417
477
  cv: arc.status.result,
418
478
  });
419
479
 
480
+ // sealHeaders is empty when no ARC set could be created, use createSeal() to get the errors
481
+
420
482
  // Output the sealed message
421
483
  process.stdout.write(sealHeaders); // ARC headers
422
484
  process.stdout.write(headers); // Authentication results
@@ -427,11 +489,15 @@ process.stdout.write(message);
427
489
 
428
490
  DMARC is verified during the authentication process. Although the `dmarc` handler is exported, it requires input from previous steps like SPF and DKIM.
429
491
 
492
+ The author domain is the domain of the From address, also when the local-part is quoted and contains "@" or the address has an obsolete source route. Several From addresses are evaluated if they share one domain. With no domain or several domains, DMARC validation is not possible and the result is `false`.
493
+
494
+ The Authentication-Results entry includes `policy.dmarc` with the evaluated policy on `pass` and `fail`. With `strict: true`, records follow the RFC 6376 tag-list rules (case sensitive tag names, a duplicated tag invalidates the record) and the unregistered `header.d` is left out. The default mode keeps `header.d` and case-folds tag names, letting the last duplicate win, and lists such records in `warnings`.
495
+
430
496
  See [DMARC Result Reference](docs/dmarc.md) for details on the result object structure.
431
497
 
432
498
  #### DMARC Helpers
433
499
 
434
- ##### `getDmarcRecord(domain [, resolver])`
500
+ ##### `getDmarcRecord(domain [, resolver [, options]])`
435
501
 
436
502
  Fetches and parses the DMARC record that applies to a domain, found with the DNS Tree Walk of RFC 9989 section 4.10. That is the domain's own record, or else the record of its Organizational Domain, or else a record published with `psd=y` above it. Returns `false` if no record applies.
437
503
 
@@ -440,7 +506,7 @@ Fetches and parses the DMARC record that applies to a domain, found with the DNS
440
506
  ```javascript
441
507
  const getDmarcRecord = require('mailauth/lib/dmarc/get-dmarc-record');
442
508
 
443
- const dmarcRecord = await getDmarcRecord(domain [, resolver]);
509
+ const dmarcRecord = await getDmarcRecord(domain [, resolver [, { strict }]]);
444
510
  // Returns an object with DMARC record details or `false` if not found
445
511
  ```
446
512
 
@@ -448,6 +514,7 @@ const dmarcRecord = await getDmarcRecord(domain [, resolver]);
448
514
 
449
515
  - **domain** (`string`): The domain to check for a DMARC record.
450
516
  - **resolver** (`function`, optional): Custom DNS resolver function. Defaults to `dns.resolve`.
517
+ - **options.strict** (`boolean`, optional): Parse records by the RFC 6376 tag-list rules, so a record with a duplicated tag is ignored. Defaults to `false`.
451
518
 
452
519
  ###### Example
453
520
 
@@ -479,7 +546,7 @@ console.log(dmarcRecord);
479
546
 
480
547
  ### BIMI
481
548
 
482
- Brand Indicators for Message Identification (BIMI) support is based on [draft-blank-ietf-bimi-02](https://tools.ietf.org/html/draft-blank-ietf-bimi-02). BIMI information is resolved during the authentication step, provided the message passes DMARC validation with a policy other than "none".
549
+ Brand Indicators for Message Identification (BIMI) support is based on [draft-brand-indicators-for-message-identification-14](https://datatracker.ietf.org/doc/html/draft-brand-indicators-for-message-identification). BIMI information is resolved during the authentication step, provided the message has a single From address and passes DMARC validation with an enforcing policy: neither the author domain's DMARC record nor the one of its Organizational Domain may have `p=none` or `sp=none`. A declination record (`v=BIMI1; l=;`) gives `bimi=declined`, and a DNS error gives `bimi=temperror` (earlier versions used `temperr`).
483
550
 
484
551
  See [BIMI Result Reference](docs/bimi.md) for details on the result object structure.
485
552
 
@@ -506,7 +573,7 @@ if (bimi?.location) {
506
573
  **Note:**
507
574
 
508
575
  - The `BIMI-Location` header is ignored by mailauth.
509
- - The `BIMI-Selector` header can be used for selector selection if available.
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`.
510
577
 
511
578
  #### Verified Mark Certificate (VMC)
512
579
 
@@ -530,7 +597,7 @@ Use the `getPolicy` function to fetch the MTA-STS policy for a domain.
530
597
  ```javascript
531
598
  const { getPolicy } = require('mailauth/lib/mta-sts');
532
599
 
533
- const { policy, status } = await getPolicy(domain [, knownPolicy]);
600
+ const { policy, status, warnings } = await getPolicy(domain [, knownPolicy [, options]]);
534
601
  // Returns an object with the policy and status
535
602
  ```
536
603
 
@@ -538,33 +605,39 @@ See [MTA-STS Result Reference](docs/mta-sts.md) for details on the result object
538
605
 
539
606
  ##### Parameters
540
607
 
541
- - **domain** (`string`): The domain to retrieve the policy for.
542
- - **knownPolicy** (`object`, optional): Previously cached policy for the domain.
608
+ - **domain** (`string`): The domain to retrieve the policy for. An email address is also accepted.
609
+ - **knownPolicy** (`object`, optional): Previously cached policy for the domain, the `policy` value of an earlier result.
610
+ - **options** (`object`, optional):
611
+ - **resolver** (`function`): Custom DNS resolver with the `dns.promises.resolve` signature.
612
+ - **strict** (`boolean`, default `false`): Apply the RFC 8461 rules exactly (TXT record and policy syntax, HTTP 200 only, `text/plain` only, no CN fallback or partial wildcards in the policy host certificate, expired cached policies are not applied). In the default lax mode these are accepted and listed in `warnings`.
613
+ - **timeout** (`number`, default `60000`): Overall time limit for the HTTPS policy request in milliseconds.
614
+ - **maxPolicySize** (`number`, default `65536`): Maximum size of the policy file in bytes.
543
615
 
544
616
  ##### Example
545
617
 
546
618
  ```javascript
547
619
  const { getPolicy } = require('mailauth/lib/mta-sts');
548
620
 
549
- const knownPolicy = /* Retrieve from your cache if available */;
621
+ const knownPolicy = await cache.get('gmail.com'); // undefined if not cached
550
622
  const { policy, status } = await getPolicy('gmail.com', knownPolicy);
551
623
 
552
- if (policy.id !== knownPolicy?.id) {
553
- // Update your cache with the new policy
554
- }
624
+ // Always store the returned policy. If no live policy can be found, getPolicy returns
625
+ // the still valid cached policy, so a DNS or HTTPS failure does not erase it.
626
+ await cache.set('gmail.com', policy);
555
627
 
556
628
  if (policy.mode === 'enforce') {
557
- // TLS must be used when sending to this domain
629
+ // TLS must be used when sending to this domain
558
630
  }
559
631
  ```
560
632
 
633
+ Keep cache entries at least until `policy.expires`. A cached policy that has not expired is used as long as the policy ID in DNS stays the same, and it is kept when the TXT record or the policy file can not be retrieved (RFC 8461 3.3 and 5.1).
634
+
561
635
  **Possible Status Values:**
562
636
 
563
- - `"not_found"`: No policy was found.
564
- - `"cached"`: Existing policy is still valid.
565
- - `"found"`: New or updated policy found.
566
- - `"renew"`: Existing policy is valid; renew cache.
567
- - `"errored"`: Policy discovery failed due to a temporary error.
637
+ - `"found"`: A new or updated policy was fetched.
638
+ - `"renewed"`: The cached policy is still valid and its ID matches DNS; it is returned unchanged without fetching the policy file.
639
+ - `"not_found"`: No policy was found, and there is no valid cached policy.
640
+ - `"errored"`: No live policy could be discovered or fetched; `policy.error` holds the reason. `policy` is the valid cached policy if there is one, otherwise a policy with mode `"none"`.
568
641
 
569
642
  #### MX Validation
570
643
 
@@ -575,7 +648,7 @@ Use the `validateMx` function to check if an MX hostname is valid according to t
575
648
  ```javascript
576
649
  const { validateMx } = require('mailauth/lib/mta-sts');
577
650
 
578
- const validation = validateMx(mx, policy);
651
+ const validation = validateMx(mx, policy [, options]);
579
652
  // Returns an object indicating if the MX is valid
580
653
  ```
581
654
 
@@ -583,6 +656,9 @@ const validation = validateMx(mx, policy);
583
656
 
584
657
  - **mx** (`string`): The resolved MX hostname.
585
658
  - **policy** (`object`): The MTA-STS policy object.
659
+ - **options** (`object`, optional): `{ strict }` is accepted for consistency; the matching rules are the same in both modes.
660
+
661
+ A wildcard pattern such as `*.example.com` matches exactly one left-most label (`mail.example.com`, but not `example.com` or `foo.bar.example.com`). `validateMx` does not check `policy.expires`.
586
662
 
587
663
  ##### Example
588
664
 
@@ -605,20 +681,18 @@ mailauth uses the following test suites:
605
681
 
606
682
  ### SPF Test Suite
607
683
 
608
- Based on the [OpenSPF test suite](http://www.openspf.org/Test_Suite), with some differences:
684
+ Based on the [OpenSPF test suite](http://www.openspf.org/Test_Suite). Every test is run in both modes:
609
685
 
610
- - Less strict whitespace checks.
611
- - Some macro tests are skipped.
612
- - Some tests are skipped where the invalid component is after a matching part.
613
- - All other tests pass.
686
+ - With `strict: true`, all tests pass, including the explanation (`exp`) results.
687
+ - In the default mode, 7 tests give a different result: syntax errors after the matching term are not detected, the `c`, `r` and `t` macros are accepted outside of explanation text, and a name that is invalid after macro expansion gives `permerror` instead of not matching.
614
688
 
615
689
  ### ARC Test Suite from ValiMail
616
690
 
617
- Based on ValiMail's [arc_test_suite](https://github.com/ValiMail/arc_test_suite):
691
+ Based on ValiMail's [arc_test_suite](https://github.com/ValiMail/arc_test_suite). Every test is run in both modes:
618
692
 
619
- - mailauth is less strict on header tags and casing.
693
+ - With `strict: true`, all validation tests pass except two where the suite, written against the ARC drafts, disagrees with RFC 8617: an ARC-Message-Signature with an empty `h=` (RFC 6376 requires a non-empty `h=` that includes From, so mailauth fails it in both modes), and one without `c=` that was signed `relaxed/relaxed` (the RFC default is `simple/simple`).
694
+ - In the default mode, the four ARC-Seal tag syntax tests (duplicate tags, tag name case, an invalid tag name and an empty tag-spec) are accepted with a warning instead of failing, and the ARC-Message-Signature without `c=` passes through a `relaxed/relaxed` fallback.
620
695
  - Signing test suite is used for input; mailauth validates signatures and checks for the same `cv=` output.
621
- - All tests pass, aside from minor differences.
622
696
 
623
697
  ## License
624
698
 
package/bin/mailauth.js CHANGED
@@ -103,6 +103,8 @@ program
103
103
  )
104
104
  .option('-x, --max-lookups <number>', 'Maximum allowed DNS lookups during SPF checks. Defaults to 10.', numberArg, 10)
105
105
  .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
+ .option('--strict', 'Follow the RFCs exactly instead of the lenient defaults (for example, reject rsa-sha1 DKIM signatures).')
107
+ .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.')
106
108
  .action(runCommand(commandReport, 'Failed to generate report for the input message.'));
107
109
 
108
110
  program
@@ -113,7 +115,9 @@ program
113
115
  .requiredOption('-k, --private-key <file>', 'Path to the private key file used for signing.')
114
116
  .requiredOption('-d, --domain <domain>', 'Domain name to use in the DKIM signature (d= tag).')
115
117
  .requiredOption('-s, --selector <selector>', 'Selector to use in the DKIM signature (s= tag).')
116
- .option('-a, --algo <algorithm>', 'Signing algorithm. Defaults to "rsa-sha256" or "ed25519-sha256" depending on the private key type.', 'rsa-sha256')
118
+ // no default value: without one the algorithm follows the key type, so an ed25519 key
119
+ // signs with ed25519-sha256 as the description says
120
+ .option('-a, --algo <algorithm>', 'Signing algorithm. Defaults to "rsa-sha256" or "ed25519-sha256" depending on the private key type.')
117
121
  .option('-c, --canonicalization <method>', 'Canonicalization method (c= tag). Defaults to "relaxed/relaxed".', 'relaxed/relaxed')
118
122
  .option('-t, --time <timestamp>', 'Signing time as a UNIX timestamp (t= tag). Defaults to the current time.', numberArg)
119
123
  .option(
@@ -123,6 +127,7 @@ program
123
127
  )
124
128
  .option('-h, --header-fields <fields>', 'Colon-separated list of header field names to include in the signature (h= tag).')
125
129
  .option('-o, --headers-only', 'If set, outputs only the DKIM signature headers without the message body.')
130
+ .option('--strict', 'Follow the RFCs exactly (for example, refuse to sign with rsa-sha1 or with an RSA key shorter than 1024 bits).')
126
131
  .action(runCommand(commandSign, 'Failed to sign the input message.'));
127
132
 
128
133
  program
@@ -133,11 +138,8 @@ program
133
138
  .requiredOption('-k, --private-key <file>', 'Path to the private key file used for sealing.')
134
139
  .requiredOption('-d, --domain <domain>', 'Domain name to use in the ARC seal (d= tag).')
135
140
  .requiredOption('-s, --selector <selector>', 'Selector to use in the ARC seal (s= tag).')
136
- .option(
137
- '-a, --algo <algorithm>',
138
- 'Sealing algorithm. Defaults to "rsa-sha256" or "ed25519-sha256" depending on the private key type. Note: RFC8617 only allows "rsa-sha256" (a= tag).',
139
- 'rsa-sha256'
140
- )
141
+ // no default value: without one the algorithm follows the key type
142
+ .option('-a, --algo <algorithm>', 'Sealing algorithm. Defaults to "rsa-sha256" or "ed25519-sha256" depending on the private key type.')
141
143
  .option('-c, --canonicalization <method>', 'Canonicalization method. Note: RFC8617 only allows "relaxed/relaxed" (c= tag).', 'relaxed/relaxed')
142
144
  .option('-t, --time <timestamp>', 'Sealing time as a UNIX timestamp (t= tag). Defaults to the current time.', numberArg)
143
145
  .option('-h, --header-fields <fields>', 'Colon-separated list of header field names to include in the seal (h= tag).')
@@ -154,6 +156,7 @@ program
154
156
  'Path to a JSON file with cached DNS responses. When provided, DNS queries use these cached responses instead of performing actual DNS lookups.'
155
157
  )
156
158
  .option('-o, --headers-only', 'If set, outputs only the ARC seal headers without the message body.')
159
+ .option('--strict', 'Follow the RFCs exactly instead of the lenient defaults.')
157
160
  .addOption(
158
161
  rejectRepeated(
159
162
  new Option(
@@ -231,6 +234,12 @@ program
231
234
  .option('-o, --headers-only', 'If set, outputs only the SPF authentication header.')
232
235
  .option('-x, --max-lookups <number>', 'Maximum allowed DNS lookups during SPF checks. Defaults to 10.', numberArg, 10)
233
236
  .option('-z, --max-void-lookups <number>', 'Maximum allowed DNS lookups that return no data (void lookups) during SPF checks. Defaults to 2.', numberArg, 2)
237
+ .option(
238
+ '--max-elapsed-time <ms>',
239
+ 'Maximum time in milliseconds for the whole SPF evaluation, after which the result is temperror. Not limited by default.',
240
+ numberArg
241
+ )
242
+ .option('--strict', 'Follow RFC 7208 exactly instead of the lenient defaults.')
234
243
  .action(runCommand(commandSpf, 'Failed to verify SPF for the email address.'));
235
244
 
236
245
  program
package/cli.md CHANGED
@@ -78,7 +78,7 @@ mailauth report [options] [email]
78
78
 
79
79
  #### Options
80
80
 
81
- - `--client-ip x.x.x.x`, `-i x.x.x.x`: IP address of the remote client that sent the email. If not provided, it's parsed from the latest `Received` header.
81
+ - `--client-ip x.x.x.x`, `-i x.x.x.x`: IP address of the remote client that sent the email. If not provided, it's parsed from the latest `Received` header: the connecting address in the TCP-info comment of its `from` clause (RFC 5321 section 4.4), never an address the client gave in its HELO.
82
82
  - `--sender user@example.com`, `-f user@example.com`: Email address from the MAIL FROM command. If not provided, it's parsed from the latest `Return-Path` header.
83
83
  - `--helo hostname`, `-e hostname`: Hostname from the HELO/EHLO command. Used in some SPF validations.
84
84
  - `--mta hostname`, `-m hostname`: Hostname of the server performing validations. Defaults to the local hostname.
@@ -86,6 +86,8 @@ mailauth report [options] [email]
86
86
  - `--verbose`, `-v`: Enables verbose output, displaying debugging information.
87
87
  - `--max-lookups number`, `-x number`: Sets the maximum number of DNS lookups for SPF checks. Defaults to `10`.
88
88
  - `--max-void-lookups number`, `-z number`: Sets the maximum number of void DNS lookups for SPF checks. Defaults to `2`.
89
+ - `--strict`: Follows the RFCs exactly instead of the lenient defaults, for example an `rsa-sha1` DKIM signature is reported as `dkim=policy`. See [Strict mode](README.md#strict-mode).
90
+ - `--reject-rsa-sha1`: Reports an `rsa-sha1` DKIM signature as `dkim=policy` and does not count it for DMARC, as `--strict` does, while every other check keeps the lenient default.
89
91
 
90
92
  #### Example
91
93
 
@@ -125,12 +127,13 @@ mailauth sign [options] [email]
125
127
  - `--private-key /path/to/private.key`, `-k /path/to/private.key`: Path to the private key used for signing.
126
128
  - `--domain example.com`, `-d example.com`: Domain name for the DKIM signature (`d=` tag).
127
129
  - `--selector selector`, `-s selector`: Selector for the DKIM key (`s=` tag).
128
- - `--algo algorithm`, `-a algorithm`: Signing algorithm (e.g., `rsa-sha256`). Defaults based on the private key type.
130
+ - `--algo algorithm`, `-a algorithm`: Signing algorithm (e.g., `rsa-sha256`). Defaults to `rsa-sha256` for an RSA key and `ed25519-sha256` for an Ed25519 key.
129
131
  - `--canonicalization method`, `-c method`: Canonicalization method (e.g., `relaxed/relaxed`). Defaults to `relaxed/relaxed`.
130
132
  - `--time timestamp`, `-t timestamp`: Signing time as a Unix timestamp (`t=` tag).
131
- - `--header-fields "field1:field2"`, `-h "field1:field2"`: Colon-separated list of header fields to include in the signature (`h=` tag).
133
+ - `--header-fields "field1:field2"`, `-h "field1:field2"`: Colon-separated list of header fields to include in the signature (`h=` tag). It must include `From`.
132
134
  - `--body-length length`, `-l length`: Maximum length of the body to include in the signature (`l=` tag).
133
135
  - `--headers-only`, `-o`: Outputs only the DKIM signature headers without the entire message.
136
+ - `--strict`: Refuses to sign with `rsa-sha1` or with an RSA key shorter than 1024 bits (RFC 8301), and with a domain or selector that is not valid RFC 6376 syntax. Without it these are signed, and `--verbose` prints a warning.
134
137
 
135
138
  #### Example
136
139
 
@@ -176,6 +179,7 @@ mailauth seal [options] [email]
176
179
  - `--time timestamp`, `-t timestamp`: Sealing time as a Unix timestamp (`t=` tag).
177
180
  - `--header-fields "field1:field2"`, `-h "field1:field2"`: Colon-separated list of header fields to include in the seal (`h=` tag).
178
181
  - `--headers-only`, `-o`: Outputs only the ARC seal headers without the entire message.
182
+ - `--strict`: Follows the RFCs exactly instead of the lenient defaults.
179
183
 
180
184
  **Seal-Only Options:**
181
185
 
@@ -244,6 +248,8 @@ mailauth spf [options]
244
248
  - `--headers-only`, `-o`: Outputs only the SPF authentication header.
245
249
  - `--max-lookups number`, `-x number`: Sets the maximum number of DNS lookups. Defaults to `10`.
246
250
  - `--max-void-lookups number`, `-z number`: Sets the maximum number of void DNS lookups. Defaults to `2`.
251
+ - `--max-elapsed-time ms`: Maximum time in milliseconds for the whole SPF evaluation, after which the result is `temperror`. Not limited by default.
252
+ - `--strict`: Follows RFC 7208 exactly instead of the lenient defaults.
247
253
 
248
254
  #### Example
249
255