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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +25 -0
- package/README.md +113 -39
- package/bin/mailauth.js +15 -6
- package/cli.md +9 -3
- package/docs/arc.md +126 -43
- package/docs/bimi.md +54 -37
- package/docs/dkim.md +79 -40
- package/docs/dmarc.md +80 -33
- package/docs/mta-sts.md +140 -42
- package/docs/spf.md +69 -4
- package/index.d.ts +508 -43
- package/lib/arc/index.d.ts +10 -8
- package/lib/arc/index.js +505 -124
- package/lib/bimi/index.js +162 -81
- package/lib/commands/report.js +3 -1
- package/lib/commands/seal.js +35 -5
- package/lib/commands/sign.js +8 -1
- package/lib/commands/spf.js +8 -0
- package/lib/dkim/body/relaxed.js +6 -5
- package/lib/dkim/body/simple.js +1 -3
- package/lib/dkim/dkim-signer.js +219 -33
- package/lib/dkim/dkim-verifier.js +577 -231
- package/lib/dkim/header/relaxed.js +6 -43
- package/lib/dkim/header/signature-opts.js +87 -0
- package/lib/dkim/header/simple.js +6 -43
- package/lib/dkim/message-parser.js +18 -4
- package/lib/dkim/sign.d.ts +13 -7
- package/lib/dkim/sign.js +18 -2
- package/lib/dkim/syntax.js +31 -0
- package/lib/dkim/verify.js +1 -0
- package/lib/dmarc/author-domain.js +181 -0
- package/lib/dmarc/get-dmarc-record.js +154 -47
- package/lib/dmarc/index.d.ts +29 -2
- package/lib/dmarc/verify.js +120 -44
- package/lib/mailauth.js +77 -36
- package/lib/mta-sts.d.ts +77 -11
- package/lib/mta-sts.js +497 -182
- package/lib/parse-dkim-headers.js +376 -191
- package/lib/parse-received.js +104 -32
- package/lib/spf/index.d.ts +37 -1
- package/lib/spf/index.js +260 -28
- package/lib/spf/macro.js +50 -13
- package/lib/spf/spf-verify.js +440 -204
- package/lib/spf/syntax.js +366 -0
- package/lib/tools.js +522 -116
- package/package.json +1 -1
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()`.
|
|
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`):
|
|
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
|
-
|
|
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-
|
|
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 =
|
|
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
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
-
- `"
|
|
564
|
-
- `"
|
|
565
|
-
- `"
|
|
566
|
-
- `"
|
|
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)
|
|
684
|
+
Based on the [OpenSPF test suite](http://www.openspf.org/Test_Suite). Every test is run in both modes:
|
|
609
685
|
|
|
610
|
-
-
|
|
611
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
|
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
|
|