mailauth 5.0.3 → 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 (48) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +23 -0
  3. package/README.md +117 -41
  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 +82 -31
  10. package/docs/mta-sts.md +140 -42
  11. package/docs/spf.md +69 -4
  12. package/index.d.ts +499 -45
  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 +267 -53
  34. package/lib/dmarc/index.d.ts +29 -2
  35. package/lib/dmarc/index.js +2 -1
  36. package/lib/dmarc/verify.js +205 -53
  37. package/lib/mailauth.js +86 -40
  38. package/lib/mta-sts.d.ts +77 -11
  39. package/lib/mta-sts.js +497 -182
  40. package/lib/parse-dkim-headers.js +376 -191
  41. package/lib/parse-received.js +104 -32
  42. package/lib/spf/index.d.ts +37 -1
  43. package/lib/spf/index.js +260 -28
  44. package/lib/spf/macro.js +50 -13
  45. package/lib/spf/spf-verify.js +440 -204
  46. package/lib/spf/syntax.js +366 -0
  47. package/lib/tools.js +524 -118
  48. package/package.json +12 -12
package/docs/arc.md CHANGED
@@ -14,25 +14,30 @@ const { arc } = await authenticate(message, {
14
14
  });
15
15
  ```
16
16
 
17
+ With `strict: true` the validation and sealing follow RFC 8617 exactly, see [Strict Mode](#strict-mode). The default mode is lenient about tag syntax, and marks what it accepted in the `warnings` array of the result (see [Warnings](#warnings)).
18
+
17
19
  ## Validation Result Object Fields
18
20
 
19
- | Field | Type | Presence | Description |
20
- | ----------------------- | --------------- | ----------------- | ----------------------------------------------------------- |
21
- | `status` | `object` | Always | Validation status object (see below) |
22
- | `chain` | `array` | Non-enumerable | Array of ARC chain entries (hidden from JSON serialization) |
23
- | `i` | `number\|false` | Always | Last instance number in chain, or `false` if no chain |
24
- | `signature` | `object` | Chain exists | Verification result for last ARC-Message-Signature |
25
- | `authenticationResults` | `object` | Chain exists | Parsed last ARC-Authentication-Results |
26
- | `info` | `string` | Result not "none" | Formatted Authentication-Results header value |
21
+ | Field | Type | Presence | Description |
22
+ | ----------------------- | --------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
23
+ | `status` | `object` | Always | Validation status object (see below) |
24
+ | `chain` | `array` | Non-enumerable | Array of ARC chain entries (hidden from JSON serialization) |
25
+ | `i` | `number\|false` | Always | Last instance number in chain, or `false` if no chain |
26
+ | `signature` | `object` | Chain exists | Verification result for last ARC-Message-Signature |
27
+ | `authenticationResults` | `object` | Chain exists | Parsed last ARC-Authentication-Results |
28
+ | `info` | `string` | Result not "none" | Formatted Authentication-Results header value. In strict mode also for "none" |
29
+ | `warnings` | `string[]` | Default mode | What was accepted that strict mode would reject, see [Warnings](#warnings). Not set for a failing chain, never written into `info` |
30
+ | `sealErrors` | `array` | Sealing failed | Why the message was not sealed when `authenticate()` was asked to seal it (see [Sealing](#sealing)) |
27
31
 
28
32
  ## status Object
29
33
 
30
- | Field | Type | Presence | Description |
31
- | ------------ | --------- | ------------ | ------------------------------------------------------------- |
32
- | `result` | `string` | Always | ARC result code (see below) |
33
- | `comment` | `string` | On pass/fail | Description or error message |
34
- | `shouldSeal` | `boolean` | On fail | Whether to continue sealing despite failure |
35
- | `policy` | `object` | Policy issue | Policy violation details (e.g., `{"dkim-rules": "weak-key"}`) |
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) |
36
41
 
37
42
  ## authenticationResults Object
38
43
 
@@ -56,31 +61,109 @@ When ARC validation passes, the `authenticationResults` object contains parsed r
56
61
 
57
62
  ## Comment Values (on failure)
58
63
 
59
- | Comment Pattern | Description |
60
- | ------------------------------------------ | ------------------------------------------------- |
61
- | `"i={n} seal signature validation failed"` | ARC-Seal cryptographic verification failed |
62
- | `"i={n} no valid signature"` | ARC-Message-Signature verification failed |
63
- | `"i={n} multiple {header} values"` | Duplicate ARC headers for same instance |
64
- | `"chain-length={n}"` | Chain exceeds 50 instances |
65
- | `"i={n} expected={m}"` | Missing or out-of-order instance numbers |
66
- | `"i={n} no {header} set"` | Missing required ARC header |
67
- | `"i=1 cv={value}"` | First instance must have `cv=none` |
68
- | `"i={n} cv={value}"` | Non-first instance must have `cv=pass` |
69
- | `"i={n} invalid as c"` | Invalid canonicalization for ARC-Seal |
70
- | `"i={n} invalid ams h"` | ARC-Message-Signature signed arc-seal (forbidden) |
71
- | `"no key for {domain}"` | DNS key not found |
72
- | `"unknown key version for {domain}"` | Unsupported key version |
73
- | `"unknown key type for {domain}"` | Unsupported key type |
74
- | `"invalid public key for {domain}"` | Malformed public key |
75
- | `"weak key for {domain}"` | RSA key too short |
76
-
77
- ## Seal Result Object
78
-
79
- When sealing a message with `sealMessage()`, the result contains:
80
-
81
- | Field | Type | Description |
82
- | --------- | ---------- | ------------------------------------------------------------------------------------------------ |
83
- | `headers` | `string[]` | Array of ARC headers to prepend: `[ARC-Seal, ARC-Message-Signature, ARC-Authentication-Results]` |
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 |
86
+
87
+ 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
+ ## Strict Mode
90
+
91
+ These rules apply in both modes:
92
+
93
+ - `cv` values are compared case-insensitively (`cv=Pass` is `pass`), and every seal of the chain is validated.
94
+ - An Ed25519 seal signs the SHA-256 hash of the canonicalized data (RFC 8463).
95
+ - The seal's `a=` algorithm has to match the key type, and the key record's `v=`, `h=` and `s=` tags are honored.
96
+ - An ARC-Message-Signature without `c=` uses `simple/simple`, the DKIM-Signature default.
97
+
98
+ Differences between the modes:
99
+
100
+ | Input | Default | `strict: true` |
101
+ | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------- |
102
+ | Duplicate tags, empty tag-specs, invalid tag names in ARC-Seal or the newest ARC-Message-Signature | accepted, warning `arc-tag-syntax` | `fail` |
103
+ | Upper case tag names (`S=` for `s=`) | read as lower case, warning `arc-tag-syntax` | unknown tag, so usually `fail` for the missing tag |
104
+ | Missing required tags, malformed `t=`, `x=` or `l=` | warning `arc-tag-syntax` (a missing tag usually fails anyway) | `fail` |
105
+ | `i=` written as `0x1`, `+1`, `1e0` or `1.0` | read as a number, warning `arc-instance-syntax` | `fail` |
106
+ | ARC header field with an `i=` that is missing or not a number from 1 to 50 | ignored, warning `arc-instance-syntax` | `fail` |
107
+ | ARC-Authentication-Results whose `i=` is not first | accepted, warning `arc-instance-syntax` | `fail` |
108
+ | ARC-Message-Signature without `c=` that only verifies as `relaxed/relaxed` (the ARC drafts' default) | accepted, warning `ams-c-default` | `fail` |
109
+ | No ARC chain | no `arc=` entry in Authentication-Results | `arc=none` |
110
+ | Client IP | not reported | `smtp.remote-ip` |
111
+
112
+ The position of `i=` in an ARC-Seal or ARC-Message-Signature is not checked in either mode. The RFC 8617 ABNF puts it first, but a tag-list is unordered (RFC 6376 section 3.2) and the ARC test suite places it elsewhere in all of its valid chains.
113
+
114
+ ## Warnings
115
+
116
+ 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
+
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) |
124
+
125
+ ## Sealing
126
+
127
+ `sealMessage(message, seal)` returns the ARC headers to prepend as a Buffer, and an empty Buffer when no ARC set was created. `createSeal(message, { seal, strict })` returns the reason as well:
128
+
129
+ | Field | Type | Description |
130
+ | ---------- | ---------- | ------------------------------------------------------------------------------------------------- |
131
+ | `headers` | `string[]` | ARC headers to prepend: `[ARC-Seal, ARC-Message-Signature, ARC-Authentication-Results]`, or empty |
132
+ | `errors` | `array` | `{ err, type, selector, signingDomain }` entries saying why no set was created |
133
+ | `warnings` | `string[]` | What the default mode sealed that strict mode would have refused |
134
+
135
+ Seal options:
136
+
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 |
149
+
150
+ No ARC set is created (in either mode) when:
151
+
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
+
160
+ What only strict mode refuses (the default mode seals and adds the warning):
161
+
162
+ | Input | Warning |
163
+ | ------------------------------------------------------------ | ------------------ |
164
+ | An explicit `i` that leaves a gap after the highest instance | `arc-instance-gap` |
165
+ | `cv=none` for an instance above 1, or not `none` for `i=1` | `arc-cv-instance` |
166
+ | An RSA key shorter than 1024 bits | `weak-key` |
84
167
 
85
168
  ## Example Output
86
169
 
@@ -169,8 +252,8 @@ const sealHeaders = await sealMessage(message, {
169
252
  cv: 'pass'
170
253
  });
171
254
 
172
- // sealHeaders is a Buffer containing:
173
- // ARC-Seal: i=1; a=rsa-sha256; cv=pass; d=example.com; s=arc; ...
174
- // ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; ...
175
- // ARC-Authentication-Results: i=1; mx.example.com; spf=pass; dkim=pass
255
+ // sealHeaders is a Buffer containing, for a message that already had one ARC set:
256
+ // ARC-Seal: i=2; a=rsa-sha256; cv=pass; d=example.com; s=arc; ...
257
+ // ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=example.com; ...
258
+ // ARC-Authentication-Results: i=2; mx.example.com; spf=pass; dkim=pass
176
259
  ```
package/docs/bimi.md CHANGED
@@ -4,7 +4,15 @@ This document describes the result object returned by BIMI (Brand Indicators for
4
4
 
5
5
  ## Overview
6
6
 
7
- BIMI allows organizations to display brand logos in email clients. BIMI information is resolved during the authentication step, provided the message passes DMARC validation with a policy other than "none".
7
+ BIMI allows organizations to display brand logos in email clients. BIMI information is resolved during the authentication step, provided the message passes DMARC validation with an enforcing policy. Record discovery and the result keywords follow [draft-brand-indicators-for-message-identification-14](https://datatracker.ietf.org/doc/html/draft-brand-indicators-for-message-identification). Local-part selectors (`lps=`) are not supported.
8
+
9
+ Before looking up a BIMI record, mailauth checks the requirements of section 7.1 of the draft:
10
+
11
+ - The message has one From header field with one address. DMARC evaluates several addresses that share a domain, BIMI does not.
12
+ - DMARC passed, with an effective policy other than `none` and without `t=y`.
13
+ - Neither the DMARC record of the author domain nor the one of its Organizational Domain has `p=none` or `sp=none`, or `p=quarantine` with a `pct` other than 100. A record found at the author domain itself does not hide a lax record of the Organizational Domain. `dmarc()` passes both records to `bimi()`, for a DMARC result built elsewhere `bimi()` looks up the Organizational Domain's record itself.
14
+
15
+ The record is looked up at `<selector>._bimi.<author domain>` and then at `<selector>._bimi.<organizational domain>`, where the selector comes from the `BIMI-Selector` header and defaults to `default`. A custom selector does not fall back to `default`. TXT records that do not start with `v=BIMI1` are ignored, several BIMI records are a failure.
8
16
 
9
17
  ```javascript
10
18
  const { authenticate } = require('mailauth');
@@ -29,12 +37,12 @@ const { bimi } = await authenticate(message, {
29
37
 
30
38
  ## status Object
31
39
 
32
- | Field | Type | Presence | Description |
33
- | --------- | -------- | ------------ | ---------------------------- |
34
- | `result` | `string` | Always | BIMI result code (see below) |
35
- | `comment` | `string` | On skip/fail | Reason for skip or failure |
36
- | `header` | `object` | Always | Header information |
37
- | `policy` | `object` | VMC found | Authority policy details |
40
+ | Field | Type | Presence | Description |
41
+ | --------- | -------- | ---------------------- | ---------------------------- |
42
+ | `result` | `string` | Always | BIMI result code (see below) |
43
+ | `comment` | `string` | On skip/fail/temperror | Reason for skip or failure |
44
+ | `header` | `object` | Always | Header information |
45
+ | `policy` | `object` | VMC found | Authority policy details |
38
46
 
39
47
  ### status.header Object
40
48
 
@@ -52,39 +60,51 @@ const { bimi } = await authenticate(message, {
52
60
 
53
61
  ## Result Values
54
62
 
55
- | Result | Description |
56
- | --------- | -------------------------------------------- |
57
- | `pass` | BIMI record found and valid |
58
- | `skipped` | BIMI lookup skipped (see skip reasons below) |
59
- | `fail` | BIMI record found but invalid |
60
- | `none` | No BIMI record found |
61
- | `temperr` | Temporary error during DNS lookup |
63
+ | Result | Description |
64
+ | ----------- | -------------------------------------------------------------- |
65
+ | `pass` | BIMI record found and valid |
66
+ | `skipped` | BIMI lookup skipped (see skip reasons below) |
67
+ | `fail` | BIMI record found but invalid |
68
+ | `none` | No BIMI record found |
69
+ | `declined` | The domain published a Declination to Publish (`v=BIMI1; l=;`) |
70
+ | `temperror` | Temporary error during a DNS lookup |
71
+
72
+ Earlier versions reported a DNS error as `temperr`, a keyword the draft does not define. It is now `temperror`, the keyword of the draft and of the other methods in RFC 8601.
62
73
 
63
74
  ## Skip Reasons
64
75
 
65
76
  The `status.comment` field explains why BIMI was skipped:
66
77
 
67
- | Comment | Description |
68
- | ----------------------------------- | -------------------------------------------------------- |
69
- | `"DMARC not enabled"` | DMARC result was `none` |
70
- | `"message failed DMARC"` | DMARC result was not `pass` |
71
- | `"too lax DMARC policy"` | DMARC policy is `none` or `quarantine` with `pct < 100` |
72
- | `"Aligned DKIM signature required"` | `bimiWithAlignedDkim` option set but no aligned DKIM |
73
- | `"undersized DKIM signature"` | DKIM signature has unsigned body bytes (due to `l=` tag) |
74
- | `"could not determine domain"` | Unable to extract domain from headers |
78
+ | Comment | Description |
79
+ | ---------------------------------------------------- | -------------------------------------------------------- |
80
+ | `"DMARC not enabled"` | DMARC result was `none` |
81
+ | `"message failed DMARC"` | DMARC result was not `pass` |
82
+ | `"multiple From addresses"` | More than one From header field or address |
83
+ | `"too lax DMARC policy"` | DMARC policy is `none`, or the record has `t=y` |
84
+ | `"too lax DMARC subdomain policy"` | The author or Organizational Domain record has `sp=none` |
85
+ | `"DMARC policy applied to a percentage of messages"` | `p=quarantine` with a `pct` other than 100 |
86
+ | `"Aligned DKIM signature required"` | `bimiWithAlignedDkim` option set but no aligned DKIM |
87
+ | `"undersized DKIM signature"` | DKIM signature has unsigned body bytes (due to `l=` tag) |
88
+ | `"could not determine domain"` | Unable to extract domain from headers |
75
89
 
76
90
  ## Fail Reasons
77
91
 
78
- | Comment | Description |
79
- | ------------------------------------------- | ---------------------------------------------- |
80
- | `"multiple BIMI-Selector headers"` | Message has more than one BIMI-Selector header |
81
- | `"missing bimi version in selector header"` | BIMI-Selector header missing `v=BIMI1` |
82
- | `"missing bimi version in dns record"` | DNS record missing `v=BIMI1` |
83
- | `"missing location value in dns record"` | Record has neither `l=` nor `a=` tag |
84
- | `"invalid location value in dns record"` | `l=` value is not a valid HTTPS URL |
85
- | `"invalid authority value in dns record"` | `a=` value is not a valid HTTPS URL |
86
- | `"failed to resolve {domain}"` | DNS lookup error |
87
- | `"invalid BIMI response for {domain}"` | DNS response format invalid |
92
+ | Comment | Description |
93
+ | ------------------------------------------- | ------------------------------------------------------------------------ |
94
+ | `"multiple BIMI-Selector headers"` | Message has more than one BIMI-Selector header |
95
+ | `"missing bimi version in selector header"` | BIMI-Selector header missing `v=BIMI1` |
96
+ | `"missing bimi version in dns record"` | DNS record missing `v=BIMI1` |
97
+ | `"missing location value in dns record"` | Record has no `l=` value (and is not a declination), even if it has `a=` |
98
+ | `"invalid location value in dns record"` | `l=` value is not a valid HTTPS URL |
99
+ | `"invalid authority value in dns record"` | `a=` value is not a valid HTTPS URL |
100
+ | `"multiple BIMI records for {name}"` | More than one `v=BIMI1` record at the name |
101
+
102
+ ## Temperror Reasons
103
+
104
+ | Comment | Description |
105
+ | ------------------------------------------------------------------- | -------------------------------------------------------------------- |
106
+ | `"failed to resolve {name}"` | DNS lookup error for a BIMI record |
107
+ | `"failed to resolve the DMARC policy of the organizational domain"` | The DMARC record of the Organizational Domain could not be looked up |
88
108
 
89
109
  ## VMC Validation Result
90
110
 
@@ -210,12 +230,9 @@ These headers should be added to messages after successful BIMI validation. The
210
230
  {
211
231
  "status": {
212
232
  "result": "none",
213
- "header": {
214
- "selector": "default",
215
- "d": "example.com"
216
- }
233
+ "header": {}
217
234
  },
218
- "info": "bimi=none header.selector=default header.d=example.com"
235
+ "info": "bimi=none"
219
236
  }
220
237
  ```
221
238
 
package/docs/dkim.md CHANGED
@@ -9,17 +9,19 @@ The `dkimVerify` function verifies all DKIM-Signature headers in an email messag
9
9
  ```javascript
10
10
  const { dkimVerify } = require('mailauth/lib/dkim/verify');
11
11
 
12
- const result = await dkimVerify(message);
12
+ const result = await dkimVerify(message, { strict: false });
13
13
  // result.results is an array of signature verification results
14
14
  ```
15
15
 
16
+ With `strict: true` the verification follows RFC 6376, RFC 8301 and RFC 8463 exactly. The default mode is lenient about weak and non-conforming signatures, and marks what it accepted in `status.warnings` (see [Warnings](#warnings)).
17
+
16
18
  ## Top-Level Result Object
17
19
 
18
- | Field | Type | Description |
19
- | -------------- | --------------- | ------------------------------------------------------ |
20
- | `headerFrom` | `string[]` | Array of email addresses from the From header |
21
- | `envelopeFrom` | `string\|false` | Email address from Return-Path header or sender option |
22
- | `results` | `object[]` | Array of verification results for each DKIM signature |
20
+ | Field | Type | Description |
21
+ | -------------- | --------------- | ---------------------------------------------------------------------------------------------------- |
22
+ | `headerFrom` | `string[]` | Addresses from the From header, as full addr-specs. The mailboxes of a group (RFC 6854) are included |
23
+ | `envelopeFrom` | `string\|false` | Email address from Return-Path header or sender option |
24
+ | `results` | `object[]` | Array of verification results for each DKIM signature |
23
25
 
24
26
  ## Signature Result Object
25
27
 
@@ -61,51 +63,88 @@ Each entry in the `results` array has the following structure:
61
63
 
62
64
  ### status Object
63
65
 
64
- | Field | Type | Presence | Description |
65
- | ------------ | --------------- | --------------- | ------------------------------------ |
66
- | `result` | `string` | Always | Verification result code (see below) |
67
- | `comment` | `string` | On error/info | Human-readable explanation |
68
- | `aligned` | `string\|false` | DKIM signatures | DMARC-aligned domain, or false |
69
- | `header` | `object` | Always | Signature header info |
70
- | `policy` | `object` | Policy result | Policy violation details |
71
- | `underSized` | `number` | Body limited | Number of unsigned bytes |
66
+ | Field | Type | Presence | Description |
67
+ | ------------ | --------------- | --------------- | ----------------------------------------------------------------------------------------------------- |
68
+ | `result` | `string` | Always | Verification result code (see below) |
69
+ | `comment` | `string` | On error/info | Human-readable explanation |
70
+ | `aligned` | `string\|false` | DKIM signatures | DMARC-aligned domain, or false |
71
+ | `header` | `object` | Always | Signature header info |
72
+ | `policy` | `object` | Policy result | Policy violation details |
73
+ | `underSized` | `number` | Body limited | Number of unsigned bytes |
74
+ | `testing` | `boolean` | Key has `t=y` | The signing domain is testing DKIM (RFC 6376 section 3.6.1) |
75
+ | `warnings` | `string[]` | Default mode | What was accepted that strict mode would reject, see [Warnings](#warnings). Never written into `info` |
72
76
 
73
77
  #### status.header Object
74
78
 
75
- | Field | Type | Description |
76
- | ----- | --------------- | ----------------------------------------------------- |
77
- | `i` | `string\|false` | Signing domain with @ prefix (e.g., `"@example.com"`) |
78
- | `s` | `string` | DKIM selector |
79
- | `a` | `string` | Algorithm |
80
- | `b` | `string` | First 8 characters of signature value |
79
+ | Field | Type | Description |
80
+ | ----- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
81
+ | `d` | `string` | Strict mode only: the signing domain (`d=` tag) |
82
+ | `i` | `string\|false` | Signing domain with @ prefix (e.g., `"@example.com"`). In strict mode the AUID: the `i=` value, or `@` and the signing domain when there is no `i=` (RFC 8601 section 2.7.1) |
83
+ | `s` | `string` | DKIM selector |
84
+ | `a` | `string` | Algorithm |
85
+ | `b` | `string` | First 8 characters of signature value |
81
86
 
82
87
  ## Result Values
83
88
 
84
- | Result | Description |
85
- | ----------- | -------------------------------------------------------------------------- |
86
- | `pass` | Signature verified successfully |
87
- | `fail` | Signature verification failed (bad signature) |
88
- | `neutral` | Signature could not be verified (missing key, expired, body hash mismatch) |
89
- | `policy` | Signature failed policy check (e.g., weak key) |
90
- | `temperror` | Temporary error (DNS failure) |
91
- | `none` | Message not signed |
89
+ | Result | Description |
90
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
91
+ | `pass` | Signature verified successfully |
92
+ | `fail` | Signature verification failed (bad signature). In strict mode also a body hash mismatch |
93
+ | `neutral` | Signature could not be verified (missing key, expired, body hash mismatch in the default mode, invalid signature) |
94
+ | `policy` | Signature failed policy check (a key shorter than `minBitLength`, or rsa-sha1 in strict mode) |
95
+ | `temperror` | Temporary error (DNS failure) |
96
+ | `none` | Message not signed. In the default mode also a message whose only signatures use an unknown algorithm or canonicalization, or have no `d=` or `s=`. Strict mode reports those as `neutral` |
92
97
 
93
98
  ## Comment Values
94
99
 
95
100
  Common values for `status.comment`:
96
101
 
97
- | Comment | Description |
98
- | ---------------------------- | ------------------------------------------------ |
99
- | `"body hash did not verify"` | Calculated body hash does not match `bh=` tag |
100
- | `"bad signature"` | Cryptographic signature verification failed |
101
- | `"invalid expiration"` | Expiration timestamp is before signing timestamp |
102
- | `"expired"` | Signature has expired (past `x=` timestamp) |
103
- | `"no key"` | No DKIM key found in DNS |
104
- | `"unknown key version"` | Unsupported key version in DNS record |
105
- | `"unknown key type"` | Unsupported key type in DNS record |
106
- | `"invalid public key"` | Public key in DNS record is malformed |
107
- | `"DNS failure: {code}"` | DNS lookup failed with error code |
108
- | `"message not signed"` | No DKIM-Signature headers found |
102
+ | Comment | Description |
103
+ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
104
+ | `"body hash did not verify"` | Calculated body hash does not match `bh=` tag |
105
+ | `"bad signature"` | Cryptographic signature verification failed |
106
+ | `"invalid expiration"` | Expiration timestamp is before signing timestamp |
107
+ | `"expired"` | Signature has expired (past `x=` timestamp) |
108
+ | `"no key"` | No DKIM key found in DNS |
109
+ | `"unknown key version"` | Unsupported key version in DNS record |
110
+ | `"unknown key type"` | Unsupported key type in DNS record |
111
+ | `"invalid public key"` | Public key in DNS record is malformed, or revoked (empty `p=`) |
112
+ | `"inappropriate hash algorithm"` | The key record `h=` tag does not list the hash algorithm of the signature (both modes) |
113
+ | `"key not for email"` | The key record `s=` tag does not list `email` or `*` (both modes) |
114
+ | `"From field not signed"` | The `h=` tag does not include From (both modes, RFC 6376 section 6.1.1) |
115
+ | `"weak algorithm"` | Strict mode: rsa-sha1 signature (RFC 8301) |
116
+ | `"signature syntax error"` | Strict mode: the signature is not valid RFC 6376 syntax |
117
+ | `"signature missing required tag"` | Strict mode: `v=`, `a=`, `b=`, `bh=`, `d=`, `h=` or `s=` is missing |
118
+ | `"incompatible version"` | Strict mode: `v=` is not 1 |
119
+ | `"domain mismatch"` | Strict mode: the `i=` domain is not `d=` or its subdomain, or is a subdomain while the key has `t=s` |
120
+ | `"unsupported query method"` | Strict mode: `q=` does not list `dns/txt` |
121
+ | `"inappropriate key algorithm"` | Strict mode: the key type does not match the signature algorithm |
122
+ | `"unknown algorithm"`, `"unknown canonicalization"` | Strict mode: the signature can not be processed |
123
+ | `"DNS failure: {code}"` | DNS lookup failed with error code |
124
+ | `"message not signed"` | No DKIM-Signature headers found |
125
+
126
+ ## Warnings
127
+
128
+ In the default mode `status.warnings` lists what was accepted although strict mode would have rejected it. The array is only present when there is something to report, and it is never written into the Authentication-Results text.
129
+
130
+ | Warning | Meaning |
131
+ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
132
+ | `rsa-sha1` | The signature uses rsa-sha1, which RFC 8301 section 3.1 does not allow. A domain can refuse it with `h=sha256` in its key record, which applies in both modes |
133
+ | `tag-syntax` | Duplicate tags, upper case tag names, a malformed `t=`, `x=` or `l=` value, or a `d=` or `s=` that is not a domain name |
134
+ | `missing-v`, `missing-h` (and the other `missing-*`) | A required tag is missing. Without `h=` the default header list is used, which includes From |
135
+ | `invalid-v` | `v=` is not `1` |
136
+ | `identity-domain` | The `i=` domain is not `d=` or its subdomain, or it is a subdomain while the key record has `t=s` |
137
+ | `invalid-expiration` | `x=` is equal to `t=`. An `x=` before `t=` is `neutral (invalid expiration)` in both modes |
138
+ | `query-method` | `q=` does not list `dns/txt` |
139
+ | `key-syntax` | The key record is not valid tag-list syntax |
140
+ | `key-v-syntax` | The key record `v=` is not the first tag, or not exactly `DKIM1` |
141
+ | `key-type-inferred` | An Ed25519 key was recognized from its length, the key record has no `k=ed25519` |
142
+
143
+ A negative `l=` value is read as no limit in the default mode, so the whole body is covered and nothing is left unsigned. Strict mode reports it as a syntax error.
144
+
145
+ ## Tag parsing
146
+
147
+ DKIM-Signature headers and key records are tag-lists (RFC 6376 section 3.2): semicolons separate the tags, and quotes, parentheses and backslashes are ordinary value characters. A note such as `n=it's` or a copied header in `z=` with a parenthesis in it does not affect the other tags. When the signature is hashed, only the value of its own `b=` tag is emptied, even when another tag contains the text `b=`.
109
148
 
110
149
  ## Example Output
111
150