mailauth 7.0.0 → 7.1.1

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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "7.0.0"
2
+ ".": "7.1.1"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.1.1](https://github.com/postalsys/mailauth/compare/v7.1.0...v7.1.1) (2026-10-04)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **deps:** update libmime to 5.4.7 and nodemailer to 10.0.14 ([0c39b00](https://github.com/postalsys/mailauth/commit/0c39b007889bb58aaf55d39d1867fc7b0df73158))
9
+
10
+ ## [7.1.0](https://github.com/postalsys/mailauth/compare/v7.0.0...v7.1.0) (2026-09-27)
11
+
12
+
13
+ ### Features
14
+
15
+ * **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))
16
+ * **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))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **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))
22
+
3
23
  ## [7.0.0](https://github.com/postalsys/mailauth/compare/v6.0.0...v7.0.0) (2026-09-27)
4
24
 
5
25
 
package/README.md CHANGED
@@ -85,6 +85,7 @@ await authenticate(message [, options])
85
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
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.
87
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.
88
89
  - **disableArc** (`boolean`): If `true`, skips ARC checks.
89
90
  - **disableDmarc** (`boolean`): If `true`, skips DMARC checks, also disabling dependent checks like BIMI.
90
91
  - **disableBimi** (`boolean`): If `true`, skips BIMI checks.
@@ -151,7 +152,7 @@ You can see the full output, including structured data for DKIM, SPF, DMARC, and
151
152
 
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.
153
154
 
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
+ 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()`.
155
156
 
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.
157
158
 
@@ -296,6 +297,7 @@ const result = await dkimVerify(message [, options]);
296
297
  - **options** (optional):
297
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).
298
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.
299
301
  - **sender** (`string`): Envelope sender. Defaults to the `Return-Path` header.
300
302
  - **curTime** (`Date`): The time to check `t=` and `x=` against. Defaults to now.
301
303
  - **strict** (`boolean`): If `true`, follows RFC 6376, RFC 8301 and RFC 8463 exactly. Defaults to `false`. See [Strict mode](#strict-mode).
package/bin/mailauth.js CHANGED
@@ -104,6 +104,7 @@ program
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
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.')
107
108
  .action(runCommand(commandReport, 'Failed to generate report for the input message.'));
108
109
 
109
110
  program
package/cli.md CHANGED
@@ -87,6 +87,7 @@ mailauth report [options] [email]
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
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.
90
91
 
91
92
  #### Example
92
93
 
package/docs/dkim.md CHANGED
@@ -91,7 +91,7 @@ Each entry in the `results` array has the following structure:
91
91
  | `pass` | Signature verified successfully |
92
92
  | `fail` | Signature verification failed (bad signature). In strict mode also a body hash mismatch |
93
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) |
94
+ | `policy` | Signature failed policy check (a key shorter than `minBitLength`, or rsa-sha1 in strict mode or with `rejectRsaSha1`) |
95
95
  | `temperror` | Temporary error (DNS failure) |
96
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` |
97
97
 
@@ -112,7 +112,7 @@ Common values for `status.comment`:
112
112
  | `"inappropriate hash algorithm"` | The key record `h=` tag does not list the hash algorithm of the signature (both modes) |
113
113
  | `"key not for email"` | The key record `s=` tag does not list `email` or `*` (both modes) |
114
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) |
115
+ | `"weak algorithm"` | Strict mode or `rejectRsaSha1`: rsa-sha1 signature (RFC 8301) |
116
116
  | `"signature syntax error"` | Strict mode: the signature is not valid RFC 6376 syntax |
117
117
  | `"signature missing required tag"` | Strict mode: `v=`, `a=`, `b=`, `bh=`, `d=`, `h=` or `s=` is missing |
118
118
  | `"incompatible version"` | Strict mode: `v=` is not 1 |
package/docs/dmarc.md CHANGED
@@ -61,7 +61,7 @@ On a `temperror` that happens after a record was found, `policy`, `p`, `sp`, `rr
61
61
 
62
62
  `policyDomain` is the domain an RFC 9990 aggregate report lists as the domain of the published policy. By default it is also in `status.header.d`. `domain` is the Organizational Domain, which is a different domain when the policy came from the author domain's own record below it, or from a PSD above it.
63
63
 
64
- `policy` is `p` for the author domain's own record. For a record inherited from the Organizational Domain or a PSD it is `np` when the author domain does not exist (NXDOMAIN, RFC 9989 A.4), otherwise `sp`, both falling back to `p`. The existence query is only made when the record has `np`. When it fails and `np` differs from the other policy, a message that passes still passes, `policy` is the weaker of the two and `policy.dmarc` is left out, and a message that does not pass gets `temperror`. With `t=y` the policy is one level below that, so `reject` becomes `quarantine` and `quarantine` becomes `none`, while `p`, `sp` and `np` stay as published.
64
+ `policy` is `p` for the author domain's own record. For a record inherited from the Organizational Domain or a PSD it is `np` when the author domain does not exist (NXDOMAIN, RFC 9989 A.4), otherwise `sp`, both falling back to `p`. The existence query is only made when the record has `np`. It asks for A, and after an NXDOMAIN for the name's own CNAME, because a name that is a CNAME exists even when its target does not. When it fails and `np` differs from the other policy, a message that passes still passes, `policy` is the weaker of the two and `policy.dmarc` is left out, and a message that does not pass gets `temperror`. With `t=y` the policy is one level below that, so `reject` becomes `quarantine` and `quarantine` becomes `none`, while `p`, `sp` and `np` stay as published.
65
65
 
66
66
  A record without a valid `p`, or with an invalid `sp` or `np`, is applied as `p=none` when its `rua` lists a syntactically valid URI (RFC 3986, so `mailto:` counts and a bare address does not), and otherwise gets no DMARC processing (result `none`). `pct` is historic in RFC 9989 and is ignored.
67
67
 
package/index.d.ts CHANGED
@@ -55,6 +55,13 @@ export interface AuthenticateOptions {
55
55
  */
56
56
  minBitLength?: number;
57
57
 
58
+ /**
59
+ * If true, rsa-sha1 DKIM signatures get `policy` (weak-algorithm) as in strict mode and are
60
+ * not counted for DMARC, while every other check keeps the lenient default. Verification
61
+ * only (default: false)
62
+ */
63
+ rejectRsaSha1?: boolean;
64
+
58
65
  /**
59
66
  * Custom DNS resolver function
60
67
  */
@@ -1216,6 +1223,12 @@ export interface DKIMVerifyOptions {
1216
1223
  */
1217
1224
  minBitLength?: number;
1218
1225
 
1226
+ /**
1227
+ * If true, rsa-sha1 signatures get `policy` (weak-algorithm) as in strict mode, while every
1228
+ * other check keeps the lenient default (default: false)
1229
+ */
1230
+ rejectRsaSha1?: boolean;
1231
+
1219
1232
  /**
1220
1233
  * Current time for signature expiration checks
1221
1234
  */
@@ -26,7 +26,8 @@ const cmd = async argv => {
26
26
 
27
27
  const opts = {
28
28
  trustReceived: true,
29
- strict: !!argv.strict
29
+ strict: !!argv.strict,
30
+ rejectRsaSha1: !!argv.rejectRsaSha1
30
31
  };
31
32
 
32
33
  if (argv.clientIp) {
@@ -61,6 +61,9 @@ class DkimVerifier extends MessageParser {
61
61
  // follow RFC 6376, RFC 8301 and RFC 8463 exactly instead of the lenient defaults
62
62
  this.strict = !!this.options.strict;
63
63
 
64
+ // RFC 8301 section 3.1 on its own, without the rest of the strict mode
65
+ this.rejectRsaSha1 = this.strict || !!this.options.rejectRsaSha1;
66
+
64
67
  this.curTime = getCurTime(this.options.curTime);
65
68
 
66
69
  this.results = [];
@@ -378,8 +381,8 @@ class DkimVerifier extends MessageParser {
378
381
 
379
382
  if (signatureHeader.hashAlgo === 'sha1') {
380
383
  // RFC 8301 section 3.1: rsa-sha1 MUST NOT be used for verifying. Only the
381
- // strict mode rejects it, the lenient mode lets the key owner opt out of it
382
- // with the h= tag of the key record
384
+ // strict mode and rejectRsaSha1 reject it, the lenient mode lets the key owner
385
+ // opt out of it with the h= tag of the key record
383
386
  warn('rsa-sha1');
384
387
  }
385
388
  }
@@ -510,7 +513,7 @@ class DkimVerifier extends MessageParser {
510
513
  // The lenient mode keeps reporting neutral, as it always has
511
514
  status.result = this.strict ? 'fail' : 'neutral';
512
515
  status.comment = `body hash did not verify`;
513
- } else if (this.strict && signatureHeader.type === 'DKIM' && signatureHeader.hashAlgo === 'sha1') {
516
+ } else if (this.rejectRsaSha1 && signatureHeader.type === 'DKIM' && signatureHeader.hashAlgo === 'sha1') {
514
517
  // RFC 8301 section 3.1, "rsa-sha1 MUST NOT be used for signing or verifying"
515
518
  status.result = 'policy';
516
519
  status.comment = 'weak algorithm';
@@ -54,18 +54,32 @@ const getPolicyTags = record => {
54
54
 
55
55
  // Domain existence test (RFC 9989 A.4): any RR makes a name exist, only NXDOMAIN means it
56
56
  // does not. A query for A tells the two apart, NODATA is an existing name without A records.
57
+ // A query for A follows a CNAME and gets the target's NXDOMAIN when the target is gone (RFC 6604 3),
58
+ // so after an NXDOMAIN the name's own CNAME, which is not followed, is looked up as well.
57
59
  const domainExists = async (domain, resolver) => {
58
60
  if (!isValidName(domain)) {
59
61
  return false;
60
62
  }
63
+ resolver = resolver || dns.resolve;
61
64
  try {
62
- await (resolver || dns.resolve)(domain, 'A');
65
+ await resolver(domain, 'A');
63
66
  return true;
64
67
  } catch (err) {
65
68
  if (err.code === 'ENODATA') {
66
69
  return true;
67
70
  }
68
- if (err.code === 'ENOTFOUND' || err.code === 'EBADNAME') {
71
+ if (err.code === 'EBADNAME') {
72
+ return false;
73
+ }
74
+ if (err.code !== 'ENOTFOUND') {
75
+ throw err;
76
+ }
77
+ }
78
+ try {
79
+ return ((await resolver(domain, 'CNAME')) || []).length > 0;
80
+ } catch (err) {
81
+ if (NO_RECORD_CODES.has(err.code)) {
82
+ // no CNAME, so the NXDOMAIN for A was the name's own
69
83
  return false;
70
84
  }
71
85
  throw err;
package/lib/mailauth.js CHANGED
@@ -24,6 +24,7 @@ const { isIP } = require('net');
24
24
  * @param {String} [opts.helo] Hostname from EHLO/HELO
25
25
  * @param {String} [opts.mta] MTA/MX hostname (defaults to os.hostname)
26
26
  * @param {Number} [opts.minBitLength=1024] Minimal allowed length of public keys. If DKIM/ARC key is smaller, then verification fails
27
+ * @param {Boolean} [opts.rejectRsaSha1=false] If true then rsa-sha1 DKIM signatures get dkim=policy (weak-algorithm) as in strict mode (RFC 8301), the other checks stay lenient
27
28
  * @param {Object} [opts.seal] ARC sealing options
28
29
  * @param {String} [opts.seal.signingDomain] ARC key domain name
29
30
  * @param {String} [opts.seal.selector] ARC key selector
@@ -49,6 +50,7 @@ const authenticate = async (input, opts) => {
49
50
  sender: opts.sender, // defaults to Return-Path header
50
51
  seal: opts.seal,
51
52
  minBitLength: opts.minBitLength,
53
+ rejectRsaSha1: opts.rejectRsaSha1,
52
54
  strict: opts.strict
53
55
  });
54
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailauth",
3
- "version": "7.0.0",
3
+ "version": "7.1.1",
4
4
  "description": "Email authentication library for Node.js",
5
5
  "main": "lib/mailauth.js",
6
6
  "types": "index.d.ts",
@@ -35,26 +35,26 @@
35
35
  "homepage": "https://github.com/postalsys/mailauth",
36
36
  "devDependencies": {
37
37
  "chai": "4.4.1",
38
- "eslint": "10.11.0",
38
+ "eslint": "10.12.0",
39
39
  "eslint-config-nodemailer": "1.2.0",
40
40
  "eslint-config-prettier": "10.1.8",
41
41
  "js-yaml": "5.4.2",
42
42
  "license-report": "6.8.5",
43
43
  "mbox-reader": "1.2.0",
44
- "mocha": "12.0.2",
44
+ "mocha": "12.0.3",
45
45
  "prettier": "3.9.9",
46
46
  "resedit": "3.1.0"
47
47
  },
48
48
  "dependencies": {
49
49
  "@postalsys/vmc": "1.1.5",
50
50
  "commander": "15.0.0",
51
- "fast-xml-parser": "5.11.1",
51
+ "fast-xml-parser": "5.11.2",
52
52
  "ipaddr.js": "2.5.0",
53
53
  "joi": "18.2.9",
54
- "libmime": "5.4.4",
55
- "nodemailer": "10.0.10",
54
+ "libmime": "5.4.7",
55
+ "nodemailer": "10.0.14",
56
56
  "punycode.js": "2.3.1",
57
- "tldts": "7.4.15",
57
+ "tldts": "7.4.16",
58
58
  "undici": "8.11.2"
59
59
  },
60
60
  "engines": {