mailauth 6.0.0 → 7.0.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 +12 -0
  3. package/README.md +111 -39
  4. package/bin/mailauth.js +14 -6
  5. package/cli.md +8 -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 +495 -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 +2 -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 +574 -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 +140 -47
  34. package/lib/dmarc/index.d.ts +29 -2
  35. package/lib/dmarc/verify.js +120 -44
  36. package/lib/mailauth.js +75 -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.0.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.0.0](https://github.com/postalsys/mailauth/compare/v6.0.0...v7.0.0) (2026-09-27)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * **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.
9
+
10
+ ### Features
11
+
12
+ * **dmarc:** apply np, t and the RFC 9989 rules for invalid policies ([c11740c](https://github.com/postalsys/mailauth/commit/c11740c0c9b18a9329e82c5eacfb63cfb3a3420a))
13
+ * 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))
14
+
3
15
  ## [6.0.0](https://github.com/postalsys/mailauth/compare/v5.0.3...v6.0.0) (2026-09-26)
4
16
 
5
17
 
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,8 +82,8 @@ 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.
87
88
  - **disableArc** (`boolean`): If `true`, skips ARC checks.
88
89
  - **disableDmarc** (`boolean`): If `true`, skips DMARC checks, also disabling dependent checks like BIMI.
@@ -94,6 +95,7 @@ await authenticate(message [, options])
94
95
  - **resolver** (`async function`): Custom DNS resolver function. Defaults to [`dns.promises.resolve`](https://nodejs.org/api/dns.html#dns_dnspromises_resolve_hostname_rrtype).
95
96
  - **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
97
  - **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).
98
+ - **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
99
 
98
100
  #### Example
99
101
 
@@ -141,6 +143,38 @@ You can see the full output, including structured data for DKIM, SPF, DMARC, and
141
143
 
142
144
  **Note:** The `receivedChain` property is an array of parsed representations of the `Received:` headers.
143
145
 
146
+ **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
+
148
+ **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.
149
+
150
+ #### Strict mode
151
+
152
+ 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.
153
+
154
+ 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.
155
+
156
+ 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
+
158
+ DKIM differences between the modes:
159
+
160
+ | Input | Default | `strict: true` |
161
+ | ------------------------------------------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------------- |
162
+ | rsa-sha1 signature (RFC 8301) | `pass`, warning `rsa-sha1` | `policy` (`policy.dkim-rules=weak-algorithm`) |
163
+ | Missing `v=` or `h=`, `v=` other than 1 | verified, warning `missing-v`/`missing-h`/`invalid-v` | `neutral` (`signature missing required tag`, `incompatible version`) |
164
+ | Duplicate tags, upper case tag names, malformed `t=`/`x=`/`l=` | verified, warning `tag-syntax` | `neutral` (`signature syntax error`) |
165
+ | `i=` domain outside `d=`, or a subdomain with a key that has `t=s` | verified, warning `identity-domain` | `neutral` (`domain mismatch`) |
166
+ | `x=` equal to `t=` | verified, warning `invalid-expiration` | `neutral` (`invalid expiration`) |
167
+ | Key record syntax errors, `v=` not first or not exactly `DKIM1` | used, warning `key-syntax`/`key-v-syntax` | key ignored |
168
+ | Ed25519 key record without `k=ed25519` | used, warning `key-type-inferred` | key ignored |
169
+ | Body hash mismatch | `neutral` | `fail` |
170
+ | Signature with an unknown algorithm or no `d=`/`s=` | left out of the results | `neutral` |
171
+ | `header.i` in Authentication-Results | always `@d` | the AUID (`i=` or `@d`), plus `header.d` |
172
+ | 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) |
173
+ | Header order | Received-SPF above Authentication-Results | Authentication-Results on top (RFC 8601 section 5) |
174
+ | Signing with rsa-sha1 or an RSA key under 1024 bits | signed, warning `rsa-sha1`/`weak-key` | error |
175
+
176
+ See the [DKIM result reference](docs/dkim.md) for every DKIM warning, and the SPF, DMARC, ARC and MTA-STS documentation for theirs.
177
+
144
178
  ### DKIM
145
179
 
146
180
  #### DKIM Signing
@@ -153,24 +187,29 @@ Use the `dkimSign` function to sign an email message with DKIM.
153
187
  const { dkimSign } = require('mailauth/lib/dkim/sign');
154
188
 
155
189
  const signResult = await dkimSign(message, options);
156
- // Returns: { signatures: String, errors: Array }
190
+ // Returns: { signatures: String, errors: Array, warnings: Array }
157
191
  ```
158
192
 
193
+ `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`).
194
+
159
195
  ##### Parameters
160
196
 
161
197
  - **message**: A `String`, `Buffer`, or `Readable` stream representing the email message.
162
198
  - **options**:
163
199
  - **canonicalization** (`string`): Canonicalization method. Defaults to `'relaxed/relaxed'`.
164
- - **algorithm** (`string`): Signing and hashing algorithm. Defaults to `'rsa-sha256'`.
200
+ - **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
201
  - **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.
202
+ - **expires** (`Date`): Signature expiration time (`x=` tag). Optional. It must be later than the signing time.
203
+ - **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.
204
+ - **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
205
  - **signatureData** (`Array`): Array of signature objects. Each object may contain:
169
- - **signingDomain** (`string`): DKIM key domain name.
206
+ - **signingDomain** (`string`): DKIM key domain name. An internationalized domain name is converted to A-labels.
170
207
  - **selector** (`string`): DKIM key selector.
171
208
  - **privateKey** (`string` or `Buffer`): Private key for signing (RSA or Ed25519).
172
209
  - **algorithm** (`string`, optional): Overrides parent `algorithm`.
173
210
  - **canonicalization** (`string`, optional): Overrides parent `canonicalization`.
211
+ - **headerList** (`Array` or `string`, optional): Overrides parent `headerList`.
212
+ - **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
213
  - **maxBodyLength** (`number`, optional): Maximum number of canonicalized body bytes to sign (`l=` tag). Not recommended for general use.
175
214
 
176
215
  ##### Example
@@ -239,6 +278,8 @@ const dkimSignStream = new DkimSignStream({
239
278
  process.stdin.pipe(dkimSignStream).pipe(process.stdout);
240
279
  ```
241
280
 
281
+ 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.
282
+
242
283
  #### DKIM Verification
243
284
 
244
285
  Use the `dkimVerify` function to verify DKIM signatures in an email message.
@@ -248,10 +289,19 @@ Use the `dkimVerify` function to verify DKIM signatures in an email message.
248
289
  ```javascript
249
290
  const { dkimVerify } = require('mailauth/lib/dkim/verify');
250
291
 
251
- const result = await dkimVerify(message);
292
+ const result = await dkimVerify(message [, options]);
252
293
  // Returns an object containing verification results
253
294
  ```
254
295
 
296
+ - **options** (optional):
297
+ - **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).
298
+ - **minBitLength** (`number`): Minimum allowed bits for RSA public keys. Defaults to `1024`. A shorter key gives `dkim=policy` with `policy.dkim-rules=weak-key`.
299
+ - **sender** (`string`): Envelope sender. Defaults to the `Return-Path` header.
300
+ - **curTime** (`Date`): The time to check `t=` and `x=` against. Defaults to now.
301
+ - **strict** (`boolean`): If `true`, follows RFC 6376, RFC 8301 and RFC 8463 exactly. Defaults to `false`. See [Strict mode](#strict-mode).
302
+
303
+ 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`.
304
+
255
305
  See [DKIM Result Reference](docs/dkim.md) for details on the result object structure.
256
306
 
257
307
  ##### Example
@@ -300,6 +350,13 @@ See [SPF Result Reference](docs/spf.md) for details on the result object structu
300
350
  - **ip** (`string`): SMTP client IP.
301
351
  - **helo** (`string`): HELO/EHLO hostname.
302
352
  - **mta** (`string`): Hostname of the MTA performing the check.
353
+ - **resolver** (`function`): Custom DNS resolver, same signature as `dns.promises.resolve`.
354
+ - **maxResolveCount** (`number`): Maximum DNS lookups, defaults to `10`.
355
+ - **maxVoidCount** (`number`): Maximum void DNS lookups, defaults to `2`.
356
+ - **maxElapsedTime** (`number`): Time limit in milliseconds for the whole evaluation. If exceeded, the result is `temperror`. No limit by default.
357
+ - **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).
358
+
359
+ 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
360
 
304
361
  ##### Example
305
362
 
@@ -319,9 +376,8 @@ console.log(result.header);
319
376
  **Sample Output:**
320
377
 
321
378
  ```
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";
379
+ 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;
380
+ envelope-from="andris@wildduck.email"; helo=foo;
325
381
  ```
326
382
 
327
383
  ### ARC
@@ -344,7 +400,7 @@ const { arc } = await authenticate(message, {
344
400
  console.log(arc);
345
401
  ```
346
402
 
347
- See [ARC Result Reference](docs/arc.md) for details on the result object structure.
403
+ See [ARC Result Reference](docs/arc.md) for details on the result object structure, and for what `strict: true` changes.
348
404
 
349
405
  **Sample Output:**
350
406
 
@@ -387,6 +443,8 @@ process.stdout.write(headers); // Includes terminating line break
387
443
  process.stdout.write(message);
388
444
  ```
389
445
 
446
+ 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.
447
+
390
448
  ##### Sealing After Modifications
391
449
 
392
450
  If you need to modify the message before sealing, first authenticate it, modify as needed, then seal using the authentication results.
@@ -417,6 +475,8 @@ const sealHeaders = await sealMessage(message, {
417
475
  cv: arc.status.result,
418
476
  });
419
477
 
478
+ // sealHeaders is empty when no ARC set could be created, use createSeal() to get the errors
479
+
420
480
  // Output the sealed message
421
481
  process.stdout.write(sealHeaders); // ARC headers
422
482
  process.stdout.write(headers); // Authentication results
@@ -427,11 +487,15 @@ process.stdout.write(message);
427
487
 
428
488
  DMARC is verified during the authentication process. Although the `dmarc` handler is exported, it requires input from previous steps like SPF and DKIM.
429
489
 
490
+ 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`.
491
+
492
+ 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`.
493
+
430
494
  See [DMARC Result Reference](docs/dmarc.md) for details on the result object structure.
431
495
 
432
496
  #### DMARC Helpers
433
497
 
434
- ##### `getDmarcRecord(domain [, resolver])`
498
+ ##### `getDmarcRecord(domain [, resolver [, options]])`
435
499
 
436
500
  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
501
 
@@ -440,7 +504,7 @@ Fetches and parses the DMARC record that applies to a domain, found with the DNS
440
504
  ```javascript
441
505
  const getDmarcRecord = require('mailauth/lib/dmarc/get-dmarc-record');
442
506
 
443
- const dmarcRecord = await getDmarcRecord(domain [, resolver]);
507
+ const dmarcRecord = await getDmarcRecord(domain [, resolver [, { strict }]]);
444
508
  // Returns an object with DMARC record details or `false` if not found
445
509
  ```
446
510
 
@@ -448,6 +512,7 @@ const dmarcRecord = await getDmarcRecord(domain [, resolver]);
448
512
 
449
513
  - **domain** (`string`): The domain to check for a DMARC record.
450
514
  - **resolver** (`function`, optional): Custom DNS resolver function. Defaults to `dns.resolve`.
515
+ - **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
516
 
452
517
  ###### Example
453
518
 
@@ -479,7 +544,7 @@ console.log(dmarcRecord);
479
544
 
480
545
  ### BIMI
481
546
 
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".
547
+ 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
548
 
484
549
  See [BIMI Result Reference](docs/bimi.md) for details on the result object structure.
485
550
 
@@ -506,7 +571,7 @@ if (bimi?.location) {
506
571
  **Note:**
507
572
 
508
573
  - The `BIMI-Location` header is ignored by mailauth.
509
- - The `BIMI-Selector` header can be used for selector selection if available.
574
+ - 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
575
 
511
576
  #### Verified Mark Certificate (VMC)
512
577
 
@@ -530,7 +595,7 @@ Use the `getPolicy` function to fetch the MTA-STS policy for a domain.
530
595
  ```javascript
531
596
  const { getPolicy } = require('mailauth/lib/mta-sts');
532
597
 
533
- const { policy, status } = await getPolicy(domain [, knownPolicy]);
598
+ const { policy, status, warnings } = await getPolicy(domain [, knownPolicy [, options]]);
534
599
  // Returns an object with the policy and status
535
600
  ```
536
601
 
@@ -538,33 +603,39 @@ See [MTA-STS Result Reference](docs/mta-sts.md) for details on the result object
538
603
 
539
604
  ##### Parameters
540
605
 
541
- - **domain** (`string`): The domain to retrieve the policy for.
542
- - **knownPolicy** (`object`, optional): Previously cached policy for the domain.
606
+ - **domain** (`string`): The domain to retrieve the policy for. An email address is also accepted.
607
+ - **knownPolicy** (`object`, optional): Previously cached policy for the domain, the `policy` value of an earlier result.
608
+ - **options** (`object`, optional):
609
+ - **resolver** (`function`): Custom DNS resolver with the `dns.promises.resolve` signature.
610
+ - **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`.
611
+ - **timeout** (`number`, default `60000`): Overall time limit for the HTTPS policy request in milliseconds.
612
+ - **maxPolicySize** (`number`, default `65536`): Maximum size of the policy file in bytes.
543
613
 
544
614
  ##### Example
545
615
 
546
616
  ```javascript
547
617
  const { getPolicy } = require('mailauth/lib/mta-sts');
548
618
 
549
- const knownPolicy = /* Retrieve from your cache if available */;
619
+ const knownPolicy = await cache.get('gmail.com'); // undefined if not cached
550
620
  const { policy, status } = await getPolicy('gmail.com', knownPolicy);
551
621
 
552
- if (policy.id !== knownPolicy?.id) {
553
- // Update your cache with the new policy
554
- }
622
+ // Always store the returned policy. If no live policy can be found, getPolicy returns
623
+ // the still valid cached policy, so a DNS or HTTPS failure does not erase it.
624
+ await cache.set('gmail.com', policy);
555
625
 
556
626
  if (policy.mode === 'enforce') {
557
- // TLS must be used when sending to this domain
627
+ // TLS must be used when sending to this domain
558
628
  }
559
629
  ```
560
630
 
631
+ 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).
632
+
561
633
  **Possible Status Values:**
562
634
 
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.
635
+ - `"found"`: A new or updated policy was fetched.
636
+ - `"renewed"`: The cached policy is still valid and its ID matches DNS; it is returned unchanged without fetching the policy file.
637
+ - `"not_found"`: No policy was found, and there is no valid cached policy.
638
+ - `"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
639
 
569
640
  #### MX Validation
570
641
 
@@ -575,7 +646,7 @@ Use the `validateMx` function to check if an MX hostname is valid according to t
575
646
  ```javascript
576
647
  const { validateMx } = require('mailauth/lib/mta-sts');
577
648
 
578
- const validation = validateMx(mx, policy);
649
+ const validation = validateMx(mx, policy [, options]);
579
650
  // Returns an object indicating if the MX is valid
580
651
  ```
581
652
 
@@ -583,6 +654,9 @@ const validation = validateMx(mx, policy);
583
654
 
584
655
  - **mx** (`string`): The resolved MX hostname.
585
656
  - **policy** (`object`): The MTA-STS policy object.
657
+ - **options** (`object`, optional): `{ strict }` is accepted for consistency; the matching rules are the same in both modes.
658
+
659
+ 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
660
 
587
661
  ##### Example
588
662
 
@@ -605,20 +679,18 @@ mailauth uses the following test suites:
605
679
 
606
680
  ### SPF Test Suite
607
681
 
608
- Based on the [OpenSPF test suite](http://www.openspf.org/Test_Suite), with some differences:
682
+ Based on the [OpenSPF test suite](http://www.openspf.org/Test_Suite). Every test is run in both modes:
609
683
 
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.
684
+ - With `strict: true`, all tests pass, including the explanation (`exp`) results.
685
+ - 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
686
 
615
687
  ### ARC Test Suite from ValiMail
616
688
 
617
- Based on ValiMail's [arc_test_suite](https://github.com/ValiMail/arc_test_suite):
689
+ Based on ValiMail's [arc_test_suite](https://github.com/ValiMail/arc_test_suite). Every test is run in both modes:
618
690
 
619
- - mailauth is less strict on header tags and casing.
691
+ - 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`).
692
+ - 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
693
  - 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
694
 
623
695
  ## License
624
696
 
package/bin/mailauth.js CHANGED
@@ -103,6 +103,7 @@ 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).')
106
107
  .action(runCommand(commandReport, 'Failed to generate report for the input message.'));
107
108
 
108
109
  program
@@ -113,7 +114,9 @@ program
113
114
  .requiredOption('-k, --private-key <file>', 'Path to the private key file used for signing.')
114
115
  .requiredOption('-d, --domain <domain>', 'Domain name to use in the DKIM signature (d= tag).')
115
116
  .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')
117
+ // no default value: without one the algorithm follows the key type, so an ed25519 key
118
+ // signs with ed25519-sha256 as the description says
119
+ .option('-a, --algo <algorithm>', 'Signing algorithm. Defaults to "rsa-sha256" or "ed25519-sha256" depending on the private key type.')
117
120
  .option('-c, --canonicalization <method>', 'Canonicalization method (c= tag). Defaults to "relaxed/relaxed".', 'relaxed/relaxed')
118
121
  .option('-t, --time <timestamp>', 'Signing time as a UNIX timestamp (t= tag). Defaults to the current time.', numberArg)
119
122
  .option(
@@ -123,6 +126,7 @@ program
123
126
  )
124
127
  .option('-h, --header-fields <fields>', 'Colon-separated list of header field names to include in the signature (h= tag).')
125
128
  .option('-o, --headers-only', 'If set, outputs only the DKIM signature headers without the message body.')
129
+ .option('--strict', 'Follow the RFCs exactly (for example, refuse to sign with rsa-sha1 or with an RSA key shorter than 1024 bits).')
126
130
  .action(runCommand(commandSign, 'Failed to sign the input message.'));
127
131
 
128
132
  program
@@ -133,11 +137,8 @@ program
133
137
  .requiredOption('-k, --private-key <file>', 'Path to the private key file used for sealing.')
134
138
  .requiredOption('-d, --domain <domain>', 'Domain name to use in the ARC seal (d= tag).')
135
139
  .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
- )
140
+ // no default value: without one the algorithm follows the key type
141
+ .option('-a, --algo <algorithm>', 'Sealing algorithm. Defaults to "rsa-sha256" or "ed25519-sha256" depending on the private key type.')
141
142
  .option('-c, --canonicalization <method>', 'Canonicalization method. Note: RFC8617 only allows "relaxed/relaxed" (c= tag).', 'relaxed/relaxed')
142
143
  .option('-t, --time <timestamp>', 'Sealing time as a UNIX timestamp (t= tag). Defaults to the current time.', numberArg)
143
144
  .option('-h, --header-fields <fields>', 'Colon-separated list of header field names to include in the seal (h= tag).')
@@ -154,6 +155,7 @@ program
154
155
  'Path to a JSON file with cached DNS responses. When provided, DNS queries use these cached responses instead of performing actual DNS lookups.'
155
156
  )
156
157
  .option('-o, --headers-only', 'If set, outputs only the ARC seal headers without the message body.')
158
+ .option('--strict', 'Follow the RFCs exactly instead of the lenient defaults.')
157
159
  .addOption(
158
160
  rejectRepeated(
159
161
  new Option(
@@ -231,6 +233,12 @@ program
231
233
  .option('-o, --headers-only', 'If set, outputs only the SPF authentication header.')
232
234
  .option('-x, --max-lookups <number>', 'Maximum allowed DNS lookups during SPF checks. Defaults to 10.', numberArg, 10)
233
235
  .option('-z, --max-void-lookups <number>', 'Maximum allowed DNS lookups that return no data (void lookups) during SPF checks. Defaults to 2.', numberArg, 2)
236
+ .option(
237
+ '--max-elapsed-time <ms>',
238
+ 'Maximum time in milliseconds for the whole SPF evaluation, after which the result is temperror. Not limited by default.',
239
+ numberArg
240
+ )
241
+ .option('--strict', 'Follow RFC 7208 exactly instead of the lenient defaults.')
234
242
  .action(runCommand(commandSpf, 'Failed to verify SPF for the email address.'));
235
243
 
236
244
  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,7 @@ 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).
89
90
 
90
91
  #### Example
91
92
 
@@ -125,12 +126,13 @@ mailauth sign [options] [email]
125
126
  - `--private-key /path/to/private.key`, `-k /path/to/private.key`: Path to the private key used for signing.
126
127
  - `--domain example.com`, `-d example.com`: Domain name for the DKIM signature (`d=` tag).
127
128
  - `--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.
129
+ - `--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
130
  - `--canonicalization method`, `-c method`: Canonicalization method (e.g., `relaxed/relaxed`). Defaults to `relaxed/relaxed`.
130
131
  - `--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).
132
+ - `--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
133
  - `--body-length length`, `-l length`: Maximum length of the body to include in the signature (`l=` tag).
133
134
  - `--headers-only`, `-o`: Outputs only the DKIM signature headers without the entire message.
135
+ - `--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
136
 
135
137
  #### Example
136
138
 
@@ -176,6 +178,7 @@ mailauth seal [options] [email]
176
178
  - `--time timestamp`, `-t timestamp`: Sealing time as a Unix timestamp (`t=` tag).
177
179
  - `--header-fields "field1:field2"`, `-h "field1:field2"`: Colon-separated list of header fields to include in the seal (`h=` tag).
178
180
  - `--headers-only`, `-o`: Outputs only the ARC seal headers without the entire message.
181
+ - `--strict`: Follows the RFCs exactly instead of the lenient defaults.
179
182
 
180
183
  **Seal-Only Options:**
181
184
 
@@ -244,6 +247,8 @@ mailauth spf [options]
244
247
  - `--headers-only`, `-o`: Outputs only the SPF authentication header.
245
248
  - `--max-lookups number`, `-x number`: Sets the maximum number of DNS lookups. Defaults to `10`.
246
249
  - `--max-void-lookups number`, `-z number`: Sets the maximum number of void DNS lookups. Defaults to `2`.
250
+ - `--max-elapsed-time ms`: Maximum time in milliseconds for the whole SPF evaluation, after which the result is `temperror`. Not limited by default.
251
+ - `--strict`: Follows RFC 7208 exactly instead of the lenient defaults.
247
252
 
248
253
  #### Example
249
254