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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +23 -0
- package/README.md +117 -41
- package/bin/mailauth.js +14 -6
- package/cli.md +8 -3
- package/docs/arc.md +126 -43
- package/docs/bimi.md +54 -37
- package/docs/dkim.md +79 -40
- package/docs/dmarc.md +82 -31
- package/docs/mta-sts.md +140 -42
- package/docs/spf.md +69 -4
- package/index.d.ts +499 -45
- 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 +2 -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 +574 -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 +267 -53
- package/lib/dmarc/index.d.ts +29 -2
- package/lib/dmarc/index.js +2 -1
- package/lib/dmarc/verify.js +205 -53
- package/lib/mailauth.js +86 -40
- 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 +524 -118
- package/package.json +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
15
|
+
## [6.0.0](https://github.com/postalsys/mailauth/compare/v5.0.3...v6.0.0) (2026-09-26)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### ⚠ BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* **dmarc:** DMARC verdicts can change. A subdomain whose parent publishes no record is now its own Organizational Domain, so a signature or SPF domain of the parent no longer aligns with it. Subdomains of names the Public Suffix List lists as suffixes but that publish a DMARC record (for example blogspot.com) now inherit that policy. The result's domain is the Tree Walk Organizational Domain, and is the author domain when no record is found.
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **dmarc:** find policies and organizational domains with the RFC 9989 DNS Tree Walk ([8ab1da2](https://github.com/postalsys/mailauth/commit/8ab1da24aa5f1be7939393bd79a9834802aa911d))
|
|
25
|
+
|
|
3
26
|
## [5.0.3](https://github.com/postalsys/mailauth/compare/v5.0.2...v5.0.3) (2026-09-03)
|
|
4
27
|
|
|
5
28
|
|
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()`.
|
|
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`):
|
|
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
|
-
|
|
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,20 +487,24 @@ 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
|
-
Fetches and parses the DMARC
|
|
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
|
|
|
438
502
|
###### Syntax
|
|
439
503
|
|
|
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
|
|
|
@@ -469,13 +534,17 @@ console.log(dmarcRecord);
|
|
|
469
534
|
"sp": "none",
|
|
470
535
|
"aspf": "r",
|
|
471
536
|
"rr": "v=DMARC1; p=none; pct=100; rua=mailto:re+joqy8fpatm3@dmarc.postmarkapp.com; sp=none; aspf=r;",
|
|
472
|
-
"isOrgRecord": false
|
|
537
|
+
"isOrgRecord": false,
|
|
538
|
+
"recordDomain": "ethereal.email",
|
|
539
|
+
"orgDomain": "ethereal.email"
|
|
473
540
|
}
|
|
474
541
|
```
|
|
475
542
|
|
|
543
|
+
`isOrgRecord` is `true` when the record was inherited from the Organizational Domain or a PSD, and `recordDomain` is where it was published. `orgDomain` is `null` when the record was found at the domain itself but the rest of the Tree Walk failed.
|
|
544
|
+
|
|
476
545
|
### BIMI
|
|
477
546
|
|
|
478
|
-
Brand Indicators for Message Identification (BIMI) support is based on [draft-
|
|
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`).
|
|
479
548
|
|
|
480
549
|
See [BIMI Result Reference](docs/bimi.md) for details on the result object structure.
|
|
481
550
|
|
|
@@ -502,7 +571,7 @@ if (bimi?.location) {
|
|
|
502
571
|
**Note:**
|
|
503
572
|
|
|
504
573
|
- The `BIMI-Location` header is ignored by mailauth.
|
|
505
|
-
- 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`.
|
|
506
575
|
|
|
507
576
|
#### Verified Mark Certificate (VMC)
|
|
508
577
|
|
|
@@ -526,7 +595,7 @@ Use the `getPolicy` function to fetch the MTA-STS policy for a domain.
|
|
|
526
595
|
```javascript
|
|
527
596
|
const { getPolicy } = require('mailauth/lib/mta-sts');
|
|
528
597
|
|
|
529
|
-
const { policy, status } = await getPolicy(domain [, knownPolicy]);
|
|
598
|
+
const { policy, status, warnings } = await getPolicy(domain [, knownPolicy [, options]]);
|
|
530
599
|
// Returns an object with the policy and status
|
|
531
600
|
```
|
|
532
601
|
|
|
@@ -534,33 +603,39 @@ See [MTA-STS Result Reference](docs/mta-sts.md) for details on the result object
|
|
|
534
603
|
|
|
535
604
|
##### Parameters
|
|
536
605
|
|
|
537
|
-
- **domain** (`string`): The domain to retrieve the policy for.
|
|
538
|
-
- **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.
|
|
539
613
|
|
|
540
614
|
##### Example
|
|
541
615
|
|
|
542
616
|
```javascript
|
|
543
617
|
const { getPolicy } = require('mailauth/lib/mta-sts');
|
|
544
618
|
|
|
545
|
-
const knownPolicy =
|
|
619
|
+
const knownPolicy = await cache.get('gmail.com'); // undefined if not cached
|
|
546
620
|
const { policy, status } = await getPolicy('gmail.com', knownPolicy);
|
|
547
621
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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);
|
|
551
625
|
|
|
552
626
|
if (policy.mode === 'enforce') {
|
|
553
|
-
|
|
627
|
+
// TLS must be used when sending to this domain
|
|
554
628
|
}
|
|
555
629
|
```
|
|
556
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
|
+
|
|
557
633
|
**Possible Status Values:**
|
|
558
634
|
|
|
559
|
-
- `"
|
|
560
|
-
- `"
|
|
561
|
-
- `"
|
|
562
|
-
- `"
|
|
563
|
-
- `"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"`.
|
|
564
639
|
|
|
565
640
|
#### MX Validation
|
|
566
641
|
|
|
@@ -571,7 +646,7 @@ Use the `validateMx` function to check if an MX hostname is valid according to t
|
|
|
571
646
|
```javascript
|
|
572
647
|
const { validateMx } = require('mailauth/lib/mta-sts');
|
|
573
648
|
|
|
574
|
-
const validation = validateMx(mx, policy);
|
|
649
|
+
const validation = validateMx(mx, policy [, options]);
|
|
575
650
|
// Returns an object indicating if the MX is valid
|
|
576
651
|
```
|
|
577
652
|
|
|
@@ -579,6 +654,9 @@ const validation = validateMx(mx, policy);
|
|
|
579
654
|
|
|
580
655
|
- **mx** (`string`): The resolved MX hostname.
|
|
581
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`.
|
|
582
660
|
|
|
583
661
|
##### Example
|
|
584
662
|
|
|
@@ -601,20 +679,18 @@ mailauth uses the following test suites:
|
|
|
601
679
|
|
|
602
680
|
### SPF Test Suite
|
|
603
681
|
|
|
604
|
-
Based on the [OpenSPF test suite](http://www.openspf.org/Test_Suite)
|
|
682
|
+
Based on the [OpenSPF test suite](http://www.openspf.org/Test_Suite). Every test is run in both modes:
|
|
605
683
|
|
|
606
|
-
-
|
|
607
|
-
-
|
|
608
|
-
- Some tests are skipped where the invalid component is after a matching part.
|
|
609
|
-
- 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.
|
|
610
686
|
|
|
611
687
|
### ARC Test Suite from ValiMail
|
|
612
688
|
|
|
613
|
-
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:
|
|
614
690
|
|
|
615
|
-
- mailauth
|
|
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.
|
|
616
693
|
- Signing test suite is used for input; mailauth validates signatures and checks for the same `cv=` output.
|
|
617
|
-
- All tests pass, aside from minor differences.
|
|
618
694
|
|
|
619
695
|
## License
|
|
620
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
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
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
|
|
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
|
|