mailauth 4.9.4 → 4.10.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/.ncurc.js CHANGED
@@ -4,10 +4,6 @@ module.exports = {
4
4
  // only works as ESM
5
5
  'chai',
6
6
  'fast-xml-parser',
7
- 'yargs',
8
-
9
- // fix later
10
- 'eslint',
11
- 'eslint-config-prettier'
7
+ 'yargs'
12
8
  ]
13
9
  };
@@ -0,0 +1,10 @@
1
+ # Auto-generated files
2
+ CHANGELOG.md
3
+ licenses.txt
4
+
5
+ # Build output
6
+ ee-dist/
7
+ node_modules/
8
+
9
+ # Test fixtures with embedded content
10
+ test/fixtures/**/*.yml
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "4.10.0"
3
+ }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.10.0](https://github.com/postalsys/mailauth/compare/mailauth-v4.9.5...mailauth-v4.10.0) (2025-10-31)
4
+
5
+
6
+ ### Features
7
+
8
+ * added `forwardemail.net` to ARC trusted list ([#86](https://github.com/postalsys/mailauth/issues/86)) ([8cb577b](https://github.com/postalsys/mailauth/commit/8cb577b5cceaf0a61f02744811ad2f9533550032))
9
+ * **cert-type:** BIMI authority information includes the type of the cert ('VMC' or 'CMC') ([0dd8db8](https://github.com/postalsys/mailauth/commit/0dd8db81b2ffc8b9d84d1a4396c65bfa9a347088))
10
+ * **deploy:** Set up automatic publishing ([f9b9c32](https://github.com/postalsys/mailauth/commit/f9b9c325e4dbac060114aa12c5887ea8c92c0bf8))
11
+ * **dkim-sign:** Added new Transfor stream class DkimSignStream to sign emails in a stream processing pipeline ([130a1a3](https://github.com/postalsys/mailauth/commit/130a1a3812fac2ad710f244510ca60887c2d33a9))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **ARC:** ensure that instance value is 1 if ARC chain does not exist yet ([ab4c5e9](https://github.com/postalsys/mailauth/commit/ab4c5e9ae0158e196b10f346321ca55b8f06c679))
17
+ * **ARC:** Updated built-in trust list for ARC ([ea9fc8c](https://github.com/postalsys/mailauth/commit/ea9fc8c6f8c5609b66053f1ffe95891c0b4efcb7))
18
+ * **bimi:** Bumped VMC module to add support for GLobalSign VMC root ([d0e9ecf](https://github.com/postalsys/mailauth/commit/d0e9ecf89b699aae8ad9953445f052b558250f5a))
19
+ * **bimi:** skip bimi with oversized DKIM signatures ([d666d74](https://github.com/postalsys/mailauth/commit/d666d7476cbcae8b3161c78a7e737559ad112fd9))
20
+ * **BodyHashStream:** Skip header ([3da03d2](https://github.com/postalsys/mailauth/commit/3da03d23baa90acb119c7946c2cd740a72ba069d))
21
+ * bumped 2022 in copyright notices to 2024 ([cc89823](https://github.com/postalsys/mailauth/commit/cc8982349d14b42a28581ebc52aa6de2e11b5be8))
22
+ * bumped deps ([006475e](https://github.com/postalsys/mailauth/commit/006475ee7bbf61a8c7c00de793f4007f66dba61a))
23
+ * **cli:** Updated help strings for the cli script ([8a86e51](https://github.com/postalsys/mailauth/commit/8a86e51bff0300a7daea26062481ac56904202a8))
24
+ * **deps:** Bumped deps to clear out security warnings ([4ca35fe](https://github.com/postalsys/mailauth/commit/4ca35fef37e37ae715c420b8a52c7cb202e4b360))
25
+ * **deps:** Bumped deps to get updated vmc root store ([5ad7464](https://github.com/postalsys/mailauth/commit/5ad746450f97d348217607802e83445e08737faf))
26
+ * **deps:** Removed uuid dependency in favor of crypto.randomUUID() ([0b5d8f5](https://github.com/postalsys/mailauth/commit/0b5d8f5328d0b82f75daea7fdbd74e1e76e8b642))
27
+ * **dkim-relaxed:** Faster DKIM hash calculation for relaxed body if the body contains extremely long lines ([fd8c89e](https://github.com/postalsys/mailauth/commit/fd8c89edd87a114464f99ebf79a1e903a8287876))
28
+ * **dkim-verify:** Show the length of the source body in DKIM results ([d28663b](https://github.com/postalsys/mailauth/commit/d28663b30b0bfaf07d395e9d3eaea044c9085657))
29
+ * **dkim:** Added new output property mimeStructureStart ([8f25353](https://github.com/postalsys/mailauth/commit/8f25353fa6a67ba3e1f0c5091325007b2434a29d))
30
+ * **dkim:** New class BodyHashStream ([88d2fad](https://github.com/postalsys/mailauth/commit/88d2fad329a9a6fc8ebc1da4efc1c4844ae49507))
31
+ * **dkim:** Store byteLength in BodyHashStream ([081f823](https://github.com/postalsys/mailauth/commit/081f82340505d4beb88f12728919d851d35b6576))
32
+ * **dmarc-alignment:** Fixed tldts usage to allow private domains ([cc7dfa8](https://github.com/postalsys/mailauth/commit/cc7dfa8d820c1a4112602340192010354d51cd52))
33
+ * downgraded yargs because of ESM ([215c71a](https://github.com/postalsys/mailauth/commit/215c71aaa108744970533f346408c41b38590500))
34
+ * **ed25519:** Fixed ed25519 signing and verification ([40f1245](https://github.com/postalsys/mailauth/commit/40f12457d8f49f0ea21015fe4203b4de746ab7b8))
35
+ * expose verifyASChain ([#89](https://github.com/postalsys/mailauth/issues/89)) ([cd11d85](https://github.com/postalsys/mailauth/commit/cd11d851f3c8cea125209676f3ba26676c700c5b))
36
+ * protect against prototype pollution ([3b7515d](https://github.com/postalsys/mailauth/commit/3b7515df768ce1d2e4e02858fdfca8efca6243fb))
37
+ * **psl:** Replaced psl module with tldts for up to date public suffix list ([cab894b](https://github.com/postalsys/mailauth/commit/cab894b54a3544b33a641f377783db67a43bec0e))
38
+ * **spf:** expand macros in mx mechanism ([d8c05f9](https://github.com/postalsys/mailauth/commit/d8c05f90589e3fb5a56ecb4498e6dcb795dcc047))
39
+ * **spf:** optimize dual-stack A/AAAA void lookup counting ([3069e5a](https://github.com/postalsys/mailauth/commit/3069e5afa946589e54fe8aec8ffe186d90eca810))
40
+ * use minLength option for rsa keys ([#84](https://github.com/postalsys/mailauth/issues/84)) ([cbfed81](https://github.com/postalsys/mailauth/commit/cbfed816d953eee3c7eed99055c53f689a46a101))
41
+ * ZMS-246: add required policy headers in BIMI for Apple Mail ([#92](https://github.com/postalsys/mailauth/issues/92)) ([f6b3008](https://github.com/postalsys/mailauth/commit/f6b300837f9453877386ce3e76aff80fee01d913))
42
+ * ZMS-262 remove control chars from record add support for mappers in validateTagValueRecord ([#95](https://github.com/postalsys/mailauth/issues/95)) ([42828a6](https://github.com/postalsys/mailauth/commit/42828a6cb38add3aed35881f102488f8143407cb))
43
+ * ZMS-262: Add raw record sanitanization and validation util functions ([#93](https://github.com/postalsys/mailauth/issues/93)) ([e4842cf](https://github.com/postalsys/mailauth/commit/e4842cf222bd6db29f34c25434b5c38c44edefdc))
44
+
45
+ ## [4.9.5](https://github.com/postalsys/mailauth/compare/v4.9.4...v4.9.5) (2025-09-10)
46
+
47
+
48
+ ### Bug Fixes
49
+
50
+ * **spf:** expand macros in mx mechanism ([d8c05f9](https://github.com/postalsys/mailauth/commit/d8c05f90589e3fb5a56ecb4498e6dcb795dcc047))
51
+
3
52
  ## [4.9.4](https://github.com/postalsys/mailauth/compare/v4.9.3...v4.9.4) (2025-09-02)
4
53
 
5
54
 
package/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2020-2024 Postal Systems OÜ
1
+ Copyright (c) 2020-2025 Postal Systems OÜ
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -6,14 +6,14 @@
6
6
 
7
7
  **Key Features:**
8
8
 
9
- - **SPF** verification
10
- - **DKIM** signing and verification
11
- - **DMARC** verification
12
- - **ARC** verification and sealing
13
- - Sealing during authentication
14
- - Sealing after message modifications
15
- - **BIMI** resolving and **VMC** validation
16
- - **MTA-STS** helper functions
9
+ - **SPF** verification
10
+ - **DKIM** signing and verification
11
+ - **DMARC** verification
12
+ - **ARC** verification and sealing
13
+ - Sealing during authentication
14
+ - Sealing after message modifications
15
+ - **BIMI** resolving and **VMC** validation
16
+ - **MTA-STS** helper functions
17
17
 
18
18
  mailauth is a pure JavaScript implementation, requiring no external applications or compilation. It runs on any server or device with Node.js version 16 or later.
19
19
 
@@ -73,24 +73,24 @@ await authenticate(message [, options])
73
73
 
74
74
  #### Parameters
75
75
 
76
- - **message**: A `String`, `Buffer`, or `Readable` stream representing the email message.
77
- - **options** (optional):
78
- - **sender** (`string`): Email address from the MAIL FROM command. Defaults to the `Return-Path` header if not set.
79
- - **ip** (`string`): IP address of the remote client that sent the message.
80
- - **helo** (`string`): Hostname from the HELO/EHLO command.
81
- - **trustReceived** (`boolean`): If `true`, parses `ip` and `helo` from the latest `Received` header if not provided. Defaults to `false`.
82
- - **mta** (`string`): Hostname of the server performing the authentication. Defaults to `os.hostname()`. Included in Authentication headers.
83
- - **minBitLength** (`number`): Minimum allowed bits for RSA public keys. Defaults to `1024`. Keys with fewer bits will fail validation.
84
- - **disableArc** (`boolean`): If `true`, skips ARC checks.
85
- - **disableDmarc** (`boolean`): If `true`, skips DMARC checks, also disabling dependent checks like BIMI.
86
- - **disableBimi** (`boolean`): If `true`, skips BIMI checks.
87
- - **seal** (`object`): Options for ARC sealing if the message doesn't have a broken ARC chain.
88
- - **signingDomain** (`string`): ARC key domain name.
89
- - **selector** (`string`): ARC key selector.
90
- - **privateKey** (`string` or `Buffer`): Private key for signing (RSA or Ed25519).
91
- - **resolver** (`async function`): Custom DNS resolver function. Defaults to [`dns.promises.resolve`](https://nodejs.org/api/dns.html#dns_dnspromises_resolve_hostname_rrtype).
92
- - **maxResolveCount** (`number`): DNS lookup limit for SPF. Defaults to `10` as per [RFC7208](https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4).
93
- - **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).
76
+ - **message**: A `String`, `Buffer`, or `Readable` stream representing the email message.
77
+ - **options** (optional):
78
+ - **sender** (`string`): Email address from the MAIL FROM command. Defaults to the `Return-Path` header if not set.
79
+ - **ip** (`string`): IP address of the remote client that sent the message.
80
+ - **helo** (`string`): Hostname from the HELO/EHLO command.
81
+ - **trustReceived** (`boolean`): If `true`, parses `ip` and `helo` from the latest `Received` header if not provided. Defaults to `false`.
82
+ - **mta** (`string`): Hostname of the server performing the authentication. Defaults to `os.hostname()`. Included in Authentication headers.
83
+ - **minBitLength** (`number`): Minimum allowed bits for RSA public keys. Defaults to `1024`. Keys with fewer bits will fail validation.
84
+ - **disableArc** (`boolean`): If `true`, skips ARC checks.
85
+ - **disableDmarc** (`boolean`): If `true`, skips DMARC checks, also disabling dependent checks like BIMI.
86
+ - **disableBimi** (`boolean`): If `true`, skips BIMI checks.
87
+ - **seal** (`object`): Options for ARC sealing if the message doesn't have a broken ARC chain.
88
+ - **signingDomain** (`string`): ARC key domain name.
89
+ - **selector** (`string`): ARC key selector.
90
+ - **privateKey** (`string` or `Buffer`): Private key for signing (RSA or Ed25519).
91
+ - **resolver** (`async function`): Custom DNS resolver function. Defaults to [`dns.promises.resolve`](https://nodejs.org/api/dns.html#dns_dnspromises_resolve_hostname_rrtype).
92
+ - **maxResolveCount** (`number`): DNS lookup limit for SPF. Defaults to `10` as per [RFC7208](https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4).
93
+ - **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).
94
94
 
95
95
  #### Example
96
96
 
@@ -155,18 +155,18 @@ const signResult = await dkimSign(message, options);
155
155
 
156
156
  ##### Parameters
157
157
 
158
- - **message**: A `String`, `Buffer`, or `Readable` stream representing the email message.
159
- - **options**:
160
- - **canonicalization** (`string`): Canonicalization method. Defaults to `'relaxed/relaxed'`.
161
- - **algorithm** (`string`): Signing and hashing algorithm. Defaults to `'rsa-sha256'`.
162
- - **signTime** (`Date`): Signing time. Defaults to current time.
163
- - **signatureData** (`Array`): Array of signature objects. Each object may contain:
164
- - **signingDomain** (`string`): DKIM key domain name.
165
- - **selector** (`string`): DKIM key selector.
166
- - **privateKey** (`string` or `Buffer`): Private key for signing (RSA or Ed25519).
167
- - **algorithm** (`string`, optional): Overrides parent `algorithm`.
168
- - **canonicalization** (`string`, optional): Overrides parent `canonicalization`.
169
- - **maxBodyLength** (`number`, optional): Maximum number of canonicalized body bytes to sign (`l=` tag). Not recommended for general use.
158
+ - **message**: A `String`, `Buffer`, or `Readable` stream representing the email message.
159
+ - **options**:
160
+ - **canonicalization** (`string`): Canonicalization method. Defaults to `'relaxed/relaxed'`.
161
+ - **algorithm** (`string`): Signing and hashing algorithm. Defaults to `'rsa-sha256'`.
162
+ - **signTime** (`Date`): Signing time. Defaults to current time.
163
+ - **signatureData** (`Array`): Array of signature objects. Each object may contain:
164
+ - **signingDomain** (`string`): DKIM key domain name.
165
+ - **selector** (`string`): DKIM key selector.
166
+ - **privateKey** (`string` or `Buffer`): Private key for signing (RSA or Ed25519).
167
+ - **algorithm** (`string`, optional): Overrides parent `algorithm`.
168
+ - **canonicalization** (`string`, optional): Overrides parent `canonicalization`.
169
+ - **maxBodyLength** (`number`, optional): Maximum number of canonicalized body bytes to sign (`l=` tag). Not recommended for general use.
170
170
 
171
171
  ##### Example
172
172
 
@@ -286,11 +286,11 @@ const result = await spf(options);
286
286
 
287
287
  ##### Parameters
288
288
 
289
- - **options**:
290
- - **sender** (`string`): MAIL FROM address.
291
- - **ip** (`string`): SMTP client IP.
292
- - **helo** (`string`): HELO/EHLO hostname.
293
- - **mta** (`string`): Hostname of the MTA performing the check.
289
+ - **options**:
290
+ - **sender** (`string`): MAIL FROM address.
291
+ - **ip** (`string`): SMTP client IP.
292
+ - **helo** (`string`): HELO/EHLO hostname.
293
+ - **mta** (`string`): Hostname of the MTA performing the check.
294
294
 
295
295
  ##### Example
296
296
 
@@ -433,8 +433,8 @@ const dmarcRecord = await getDmarcRecord(domain [, resolver]);
433
433
 
434
434
  ###### Parameters
435
435
 
436
- - **domain** (`string`): The domain to check for a DMARC record.
437
- - **resolver** (`function`, optional): Custom DNS resolver function. Defaults to `dns.resolve`.
436
+ - **domain** (`string`): The domain to check for a DMARC record.
437
+ - **resolver** (`function`, optional): Custom DNS resolver function. Defaults to `dns.resolve`.
438
438
 
439
439
  ###### Example
440
440
 
@@ -486,8 +486,8 @@ if (bimi?.location) {
486
486
 
487
487
  **Note:**
488
488
 
489
- - The `BIMI-Location` header is ignored by mailauth.
490
- - The `BIMI-Selector` header can be used for selector selection if available.
489
+ - The `BIMI-Location` header is ignored by mailauth.
490
+ - The `BIMI-Selector` header can be used for selector selection if available.
491
491
 
492
492
  #### Verified Mark Certificate (VMC)
493
493
 
@@ -495,8 +495,8 @@ If an Authority Evidence Document is specified in the BIMI record, its location
495
495
 
496
496
  **Example Authority Evidence Documents:**
497
497
 
498
- - [CNN's VMC](https://amplify.valimail.com/bimi/time-warner/LysAFUdG-Hw-cnn_vmc.pem)
499
- - [Entrust's VMC](https://www.entrustdatacard.com/-/media/certificate/Entrust%20VMC%20July%2014%202020.pem)
498
+ - [CNN's VMC](https://amplify.valimail.com/bimi/time-warner/LysAFUdG-Hw-cnn_vmc.pem)
499
+ - [Entrust's VMC](https://www.entrustdatacard.com/-/media/certificate/Entrust%20VMC%20July%2014%202020.pem)
500
500
 
501
501
  ### MTA-STS
502
502
 
@@ -517,8 +517,8 @@ const { policy, status } = await getPolicy(domain [, knownPolicy]);
517
517
 
518
518
  ##### Parameters
519
519
 
520
- - **domain** (`string`): The domain to retrieve the policy for.
521
- - **knownPolicy** (`object`, optional): Previously cached policy for the domain.
520
+ - **domain** (`string`): The domain to retrieve the policy for.
521
+ - **knownPolicy** (`object`, optional): Previously cached policy for the domain.
522
522
 
523
523
  ##### Example
524
524
 
@@ -539,11 +539,11 @@ if (policy.mode === 'enforce') {
539
539
 
540
540
  **Possible Status Values:**
541
541
 
542
- - `"not_found"`: No policy was found.
543
- - `"cached"`: Existing policy is still valid.
544
- - `"found"`: New or updated policy found.
545
- - `"renew"`: Existing policy is valid; renew cache.
546
- - `"errored"`: Policy discovery failed due to a temporary error.
542
+ - `"not_found"`: No policy was found.
543
+ - `"cached"`: Existing policy is still valid.
544
+ - `"found"`: New or updated policy found.
545
+ - `"renew"`: Existing policy is valid; renew cache.
546
+ - `"errored"`: Policy discovery failed due to a temporary error.
547
547
 
548
548
  #### MX Validation
549
549
 
@@ -560,8 +560,8 @@ const validation = validateMx(mx, policy);
560
560
 
561
561
  ##### Parameters
562
562
 
563
- - **mx** (`string`): The resolved MX hostname.
564
- - **policy** (`object`): The MTA-STS policy object.
563
+ - **mx** (`string`): The resolved MX hostname.
564
+ - **policy** (`object`): The MTA-STS policy object.
565
565
 
566
566
  ##### Example
567
567
 
@@ -586,18 +586,18 @@ mailauth uses the following test suites:
586
586
 
587
587
  Based on the [OpenSPF test suite](http://www.openspf.org/Test_Suite), with some differences:
588
588
 
589
- - Less strict whitespace checks.
590
- - Some macro tests are skipped.
591
- - Some tests are skipped where the invalid component is after a matching part.
592
- - All other tests pass.
589
+ - Less strict whitespace checks.
590
+ - Some macro tests are skipped.
591
+ - Some tests are skipped where the invalid component is after a matching part.
592
+ - All other tests pass.
593
593
 
594
594
  ### ARC Test Suite from ValiMail
595
595
 
596
596
  Based on ValiMail's [arc_test_suite](https://github.com/ValiMail/arc_test_suite):
597
597
 
598
- - mailauth is less strict on header tags and casing.
599
- - Signing test suite is used for input; mailauth validates signatures and checks for the same `cv=` output.
600
- - All tests pass, aside from minor differences.
598
+ - mailauth is less strict on header tags and casing.
599
+ - Signing test suite is used for input; mailauth validates signatures and checks for the same `cv=` output.
600
+ - All tests pass, aside from minor differences.
601
601
 
602
602
  ## License
603
603
 
package/cli.md CHANGED
@@ -6,33 +6,32 @@ mailauth provides a command-line utility for email authentication, complementing
6
6
 
7
7
  ## Table of Contents
8
8
 
9
- - [Installation](#installation)
10
- - [Getting Help](#getting-help)
11
- - [Available Commands](#available-commands)
12
- - [`report`](#report) — Validate SPF, DKIM, DMARC, ARC, and BIMI
13
- - [`sign`](#sign) — Sign an email with DKIM
14
- - [`seal`](#seal) — Seal an email with ARC
15
- - [`spf`](#spf) — Validate SPF for an IP address and email address
16
- - [`vmc`](#vmc) — Validate BIMI VMC logo files
17
- - [`bodyhash`](#bodyhash) — Generate the body hash value for an email
18
- - [`license`](#license) — Display licenses for mailauth and included modules
19
- - [DNS Cache File](#dns-cache-file)
20
- - [License](#license)
9
+ - [Installation](#installation)
10
+ - [Getting Help](#getting-help)
11
+ - [Available Commands](#available-commands)
12
+ - [`report`](#report) — Validate SPF, DKIM, DMARC, ARC, and BIMI
13
+ - [`sign`](#sign) — Sign an email with DKIM
14
+ - [`seal`](#seal) — Seal an email with ARC
15
+ - [`spf`](#spf) — Validate SPF for an IP address and email address
16
+ - [`vmc`](#vmc) — Validate BIMI VMC logo files
17
+ - [`bodyhash`](#bodyhash) — Generate the body hash value for an email
18
+ - [`license`](#license) — Display licenses for mailauth and included modules
19
+ - [DNS Cache File](#dns-cache-file)
20
+ - [License](#license)
21
21
 
22
22
  ## Installation
23
23
 
24
24
  Install the mailauth CLI by downloading the appropriate package for your platform or via npm:
25
25
 
26
- - **MacOS:**
27
- - [Intel processors](https://github.com/postalsys/mailauth/releases/latest/download/mailauth.pkg)
28
- - [Apple silicon](https://github.com/postalsys/mailauth/releases/latest/download/mailauth-arm.pkg)
29
- - **Linux:**
30
- - [Download mailauth.tar.gz](https://github.com/postalsys/mailauth/releases/latest/download/mailauth.tar.gz)
31
- - **Windows:**
32
- - [Download mailauth.exe](https://github.com/postalsys/mailauth/releases/latest/download/mailauth.exe)
33
- - **NPM Registry:**
34
-
35
- - Install globally using npm:
26
+ - **MacOS:**
27
+ - [Intel processors](https://github.com/postalsys/mailauth/releases/latest/download/mailauth.pkg)
28
+ - [Apple silicon](https://github.com/postalsys/mailauth/releases/latest/download/mailauth-arm.pkg)
29
+ - **Linux:**
30
+ - [Download mailauth.tar.gz](https://github.com/postalsys/mailauth/releases/latest/download/mailauth.tar.gz)
31
+ - **Windows:**
32
+ - [Download mailauth.exe](https://github.com/postalsys/mailauth/releases/latest/download/mailauth.exe)
33
+ - **NPM Registry:**
34
+ - Install globally using npm:
36
35
 
37
36
  ```bash
38
37
  npm install -g mailauth
@@ -72,18 +71,18 @@ The `report` command analyzes an email message and returns a JSON-formatted repo
72
71
  mailauth report [options] [email]
73
72
  ```
74
73
 
75
- - **email**: (Optional) Path to the EML-formatted email message file. If omitted, the email is read from standard input.
74
+ - **email**: (Optional) Path to the EML-formatted email message file. If omitted, the email is read from standard input.
76
75
 
77
76
  #### Options
78
77
 
79
- - `--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.
80
- - `--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.
81
- - `--helo hostname`, `-e hostname`: Hostname from the HELO/EHLO command. Used in some SPF validations.
82
- - `--mta hostname`, `-m hostname`: Hostname of the server performing validations. Defaults to the local hostname.
83
- - `--dns-cache /path/to/dns.json`, `-n /path/to/dns.json`: Path to a DNS cache file. When provided, DNS queries use cached responses.
84
- - `--verbose`, `-v`: Enables verbose output, displaying debugging information.
85
- - `--max-lookups number`, `-x number`: Sets the maximum number of DNS lookups for SPF checks. Defaults to `10`.
86
- - `--max-void-lookups number`, `-z number`: Sets the maximum number of void DNS lookups for SPF checks. Defaults to `2`.
78
+ - `--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.
79
+ - `--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.
80
+ - `--helo hostname`, `-e hostname`: Hostname from the HELO/EHLO command. Used in some SPF validations.
81
+ - `--mta hostname`, `-m hostname`: Hostname of the server performing validations. Defaults to the local hostname.
82
+ - `--dns-cache /path/to/dns.json`, `-n /path/to/dns.json`: Path to a DNS cache file. When provided, DNS queries use cached responses.
83
+ - `--verbose`, `-v`: Enables verbose output, displaying debugging information.
84
+ - `--max-lookups number`, `-x number`: Sets the maximum number of DNS lookups for SPF checks. Defaults to `10`.
85
+ - `--max-void-lookups number`, `-z number`: Sets the maximum number of void DNS lookups for SPF checks. Defaults to `2`.
87
86
 
88
87
  #### Example
89
88
 
@@ -116,19 +115,19 @@ The `sign` command signs an email message using a DKIM signature.
116
115
  mailauth sign [options] [email]
117
116
  ```
118
117
 
119
- - **email**: (Optional) Path to the EML-formatted email message file. If omitted, the email is read from standard input.
118
+ - **email**: (Optional) Path to the EML-formatted email message file. If omitted, the email is read from standard input.
120
119
 
121
120
  #### Options
122
121
 
123
- - `--private-key /path/to/private.key`, `-k /path/to/private.key`: Path to the private key used for signing.
124
- - `--domain example.com`, `-d example.com`: Domain name for the DKIM signature (`d=` tag).
125
- - `--selector selector`, `-s selector`: Selector for the DKIM key (`s=` tag).
126
- - `--algo algorithm`, `-a algorithm`: Signing algorithm (e.g., `rsa-sha256`). Defaults based on the private key type.
127
- - `--canonicalization method`, `-c method`: Canonicalization method (e.g., `relaxed/relaxed`). Defaults to `relaxed/relaxed`.
128
- - `--time timestamp`, `-t timestamp`: Signing time as a Unix timestamp (`t=` tag).
129
- - `--header-fields "field1:field2"`, `-h "field1:field2"`: Colon-separated list of header fields to include in the signature (`h=` tag).
130
- - `--body-length length`, `-l length`: Maximum length of the body to include in the signature (`l=` tag).
131
- - `--headers-only`, `-o`: Outputs only the DKIM signature headers without the entire message.
122
+ - `--private-key /path/to/private.key`, `-k /path/to/private.key`: Path to the private key used for signing.
123
+ - `--domain example.com`, `-d example.com`: Domain name for the DKIM signature (`d=` tag).
124
+ - `--selector selector`, `-s selector`: Selector for the DKIM key (`s=` tag).
125
+ - `--algo algorithm`, `-a algorithm`: Signing algorithm (e.g., `rsa-sha256`). Defaults based on the private key type.
126
+ - `--canonicalization method`, `-c method`: Canonicalization method (e.g., `relaxed/relaxed`). Defaults to `relaxed/relaxed`.
127
+ - `--time timestamp`, `-t timestamp`: Signing time as a Unix timestamp (`t=` tag).
128
+ - `--header-fields "field1:field2"`, `-h "field1:field2"`: Colon-separated list of header fields to include in the signature (`h=` tag).
129
+ - `--body-length length`, `-l length`: Maximum length of the body to include in the signature (`l=` tag).
130
+ - `--headers-only`, `-o`: Outputs only the DKIM signature headers without the entire message.
132
131
 
133
132
  #### Example
134
133
 
@@ -161,28 +160,28 @@ The `seal` command adds an ARC (Authenticated Received Chain) seal to an email m
161
160
  mailauth seal [options] [email]
162
161
  ```
163
162
 
164
- - **email**: (Optional) Path to the EML-formatted email message file. If omitted, the email is read from standard input.
163
+ - **email**: (Optional) Path to the EML-formatted email message file. If omitted, the email is read from standard input.
165
164
 
166
165
  #### Options
167
166
 
168
167
  **Sealing Options:**
169
168
 
170
- - `--private-key /path/to/private.key`, `-k /path/to/private.key`: Path to the private key used for sealing.
171
- - `--domain example.com`, `-d example.com`: Domain name for the ARC seal (`d=` tag).
172
- - `--selector selector`, `-s selector`: Selector for the ARC key (`s=` tag).
173
- - `--algo algorithm`, `-a algorithm`: Sealing algorithm (e.g., `rsa-sha256`). Defaults based on the private key type.
174
- - `--time timestamp`, `-t timestamp`: Sealing time as a Unix timestamp (`t=` tag).
175
- - `--header-fields "field1:field2"`, `-h "field1:field2"`: Colon-separated list of header fields to include in the seal (`h=` tag).
176
- - `--headers-only`, `-o`: Outputs only the ARC seal headers without the entire message.
169
+ - `--private-key /path/to/private.key`, `-k /path/to/private.key`: Path to the private key used for sealing.
170
+ - `--domain example.com`, `-d example.com`: Domain name for the ARC seal (`d=` tag).
171
+ - `--selector selector`, `-s selector`: Selector for the ARC key (`s=` tag).
172
+ - `--algo algorithm`, `-a algorithm`: Sealing algorithm (e.g., `rsa-sha256`). Defaults based on the private key type.
173
+ - `--time timestamp`, `-t timestamp`: Sealing time as a Unix timestamp (`t=` tag).
174
+ - `--header-fields "field1:field2"`, `-h "field1:field2"`: Colon-separated list of header fields to include in the seal (`h=` tag).
175
+ - `--headers-only`, `-o`: Outputs only the ARC seal headers without the entire message.
177
176
 
178
177
  **Authentication Options (from `report` command):**
179
178
 
180
- - `--client-ip x.x.x.x`, `-i x.x.x.x`: IP address of the remote client that sent the email.
181
- - `--sender user@example.com`, `-f user@example.com`: Email address from the MAIL FROM command.
182
- - `--helo hostname`, `-e hostname`: Hostname from the HELO/EHLO command.
183
- - `--mta hostname`, `-m hostname`: Hostname of the server performing validations.
184
- - `--dns-cache /path/to/dns.json`, `-n /path/to/dns.json`: Path to a DNS cache file.
185
- - `--verbose`, `-v`: Enables verbose output.
179
+ - `--client-ip x.x.x.x`, `-i x.x.x.x`: IP address of the remote client that sent the email.
180
+ - `--sender user@example.com`, `-f user@example.com`: Email address from the MAIL FROM command.
181
+ - `--helo hostname`, `-e hostname`: Hostname from the HELO/EHLO command.
182
+ - `--mta hostname`, `-m hostname`: Hostname of the server performing validations.
183
+ - `--dns-cache /path/to/dns.json`, `-n /path/to/dns.json`: Path to a DNS cache file.
184
+ - `--verbose`, `-v`: Enables verbose output.
186
185
 
187
186
  **Note:** The canonicalization method (`c=` tag) for ARC sealing is always `relaxed/relaxed` and cannot be changed.
188
187
 
@@ -219,15 +218,15 @@ mailauth spf [options]
219
218
 
220
219
  #### Options
221
220
 
222
- - `--sender user@example.com`, `-f user@example.com`: Email address from the MAIL FROM command. **Required.**
223
- - `--client-ip x.x.x.x`, `-i x.x.x.x`: IP address of the remote client that sent the email. **Required.**
224
- - `--helo hostname`, `-e hostname`: Hostname from the HELO/EHLO command.
225
- - `--mta hostname`, `-m hostname`: Hostname of the server performing the SPF check.
226
- - `--dns-cache /path/to/dns.json`, `-n /path/to/dns.json`: Path to a DNS cache file.
227
- - `--verbose`, `-v`: Enables verbose output.
228
- - `--headers-only`, `-o`: Outputs only the SPF authentication header.
229
- - `--max-lookups number`, `-x number`: Sets the maximum number of DNS lookups. Defaults to `10`.
230
- - `--max-void-lookups number`, `-z number`: Sets the maximum number of void DNS lookups. Defaults to `2`.
221
+ - `--sender user@example.com`, `-f user@example.com`: Email address from the MAIL FROM command. **Required.**
222
+ - `--client-ip x.x.x.x`, `-i x.x.x.x`: IP address of the remote client that sent the email. **Required.**
223
+ - `--helo hostname`, `-e hostname`: Hostname from the HELO/EHLO command.
224
+ - `--mta hostname`, `-m hostname`: Hostname of the server performing the SPF check.
225
+ - `--dns-cache /path/to/dns.json`, `-n /path/to/dns.json`: Path to a DNS cache file.
226
+ - `--verbose`, `-v`: Enables verbose output.
227
+ - `--headers-only`, `-o`: Outputs only the SPF authentication header.
228
+ - `--max-lookups number`, `-x number`: Sets the maximum number of DNS lookups. Defaults to `10`.
229
+ - `--max-void-lookups number`, `-z number`: Sets the maximum number of void DNS lookups. Defaults to `2`.
231
230
 
232
231
  #### Example
233
232
 
@@ -263,9 +262,9 @@ mailauth vmc [options]
263
262
 
264
263
  #### Options
265
264
 
266
- - `--authority <url>`, `-a <url>`: URL of the VMC resource.
267
- - `--authorityPath <path>`, `-p <path>`: Path to a local VMC file, used to avoid network requests.
268
- - `--domain <domain>`, `-d <domain>`: Sender domain to validate against the certificate.
265
+ - `--authority <url>`, `-a <url>`: URL of the VMC resource.
266
+ - `--authorityPath <path>`, `-p <path>`: Path to a local VMC file, used to avoid network requests.
267
+ - `--domain <domain>`, `-d <domain>`: Sender domain to validate against the certificate.
269
268
 
270
269
  #### Example
271
270
 
@@ -332,14 +331,14 @@ The `bodyhash` command computes the body hash value of an email message, which i
332
331
  mailauth bodyhash [options] [email]
333
332
  ```
334
333
 
335
- - **email**: (Optional) Path to the EML-formatted email message file. If omitted, the email is read from standard input.
334
+ - **email**: (Optional) Path to the EML-formatted email message file. If omitted, the email is read from standard input.
336
335
 
337
336
  #### Options
338
337
 
339
- - `--algo algorithm`, `-a algorithm`: Hashing algorithm (e.g., `sha256`). Defaults to `sha256`. Can also specify DKIM-style algorithms (e.g., `rsa-sha256`).
340
- - `--canonicalization method`, `-c method`: Body canonicalization method (e.g., `relaxed`). Defaults to `relaxed`. Can use DKIM-style (e.g., `relaxed/relaxed`).
341
- - `--body-length length`, `-l length`: Maximum length of the body to hash (`l=` tag).
342
- - `--verbose`, `-v`: Enables verbose output.
338
+ - `--algo algorithm`, `-a algorithm`: Hashing algorithm (e.g., `sha256`). Defaults to `sha256`. Can also specify DKIM-style algorithms (e.g., `rsa-sha256`).
339
+ - `--canonicalization method`, `-c method`: Body canonicalization method (e.g., `relaxed`). Defaults to `relaxed`. Can use DKIM-style (e.g., `relaxed/relaxed`).
340
+ - `--body-length length`, `-l length`: Maximum length of the body to hash (`l=` tag).
341
+ - `--verbose`, `-v`: Enables verbose output.
343
342
 
344
343
  #### Example
345
344
 
@@ -390,8 +389,8 @@ The `--dns-cache` option allows you to use a JSON-formatted DNS cache file for t
390
389
 
391
390
  The DNS cache file is a JSON object where:
392
391
 
393
- - **Keys**: Fully qualified domain names (e.g., `"example.com"`).
394
- - **Values**: Objects with DNS record types as keys (e.g., `"TXT"`, `"MX"`) and their corresponding values.
392
+ - **Keys**: Fully qualified domain names (e.g., `"example.com"`).
393
+ - **Values**: Objects with DNS record types as keys (e.g., `"TXT"`, `"MX"`) and their corresponding values.
395
394
 
396
395
  **Example:**
397
396
 
@@ -0,0 +1,40 @@
1
+ const prettierConfig = require('eslint-config-prettier');
2
+
3
+ module.exports = [
4
+ {
5
+ ignores: ['node_modules/**', 'ee-dist/**', 'test/fixtures/**', 'examples/devel-*']
6
+ },
7
+ prettierConfig,
8
+ {
9
+ languageOptions: {
10
+ ecmaVersion: 2020,
11
+ sourceType: 'commonjs',
12
+ globals: {
13
+ BigInt: true,
14
+ console: 'readonly',
15
+ process: 'readonly',
16
+ Buffer: 'readonly',
17
+ __dirname: 'readonly',
18
+ __filename: 'readonly',
19
+ exports: 'writable',
20
+ module: 'writable',
21
+ require: 'readonly',
22
+ setTimeout: 'readonly',
23
+ setInterval: 'readonly',
24
+ clearTimeout: 'readonly',
25
+ clearInterval: 'readonly',
26
+ setImmediate: 'readonly',
27
+ clearImmediate: 'readonly'
28
+ }
29
+ },
30
+ rules: {
31
+ 'no-await-in-loop': 0,
32
+ 'require-atomic-updates': 0,
33
+ 'no-unused-vars': ['error', {
34
+ argsIgnorePattern: '^_',
35
+ caughtErrors: 'none'
36
+ }],
37
+ 'no-console': 0
38
+ }
39
+ }
40
+ ];
package/lib/spf/index.js CHANGED
@@ -19,7 +19,85 @@ const formatHeaders = result => {
19
19
  return libmime.foldLines(header, 160);
20
20
  };
21
21
 
22
- // DNS resolver method
22
+ /**
23
+ * Dual-stack DNS resolver for SPF A/AAAA mechanism queries
24
+ *
25
+ * When evaluating A or AAAA mechanisms, both record types should be considered
26
+ * to determine if a lookup is "void" (empty). This prevents incorrectly counting
27
+ * IPv4-only or IPv6-only hosts as void lookups.
28
+ *
29
+ * Behavior:
30
+ * - Queries both A and AAAA records in parallel (optimization)
31
+ * - Only counts as void if BOTH A and AAAA return ENOTFOUND/ENODATA
32
+ * - Real errors (ETIMEOUT, EREFUSED) for the client's IP type are propagated
33
+ * - Returns only the records matching the client's IP type (IPv4 → A, IPv6 → AAAA)
34
+ *
35
+ * Example: IPv6 client checking an IPv4-only host
36
+ * - A query returns: 192.0.2.1
37
+ * - AAAA query returns: ENODATA (empty)
38
+ * - Result: Returns empty AAAA array (no match), but does NOT count as void
39
+ *
40
+ * @param {Function} resolver - Base DNS resolver function
41
+ * @param {String} domain - Domain to query
42
+ * @param {Object} opts - Options object with clientIpType (4 or 6)
43
+ * @returns {Promise<Array>} - Array of IP addresses matching client type
44
+ * @throws {Error} - Throws on real DNS errors or when both A and AAAA are void
45
+ */
46
+ let dualStackResolver = async (resolver, domain, opts) => {
47
+ const isIPv6 = opts.clientIpType === 6;
48
+
49
+ // Query both A and AAAA records in parallel for efficiency
50
+ const [aResult, aaaaResult] = await Promise.allSettled([resolver(domain, 'A'), resolver(domain, 'AAAA')]);
51
+
52
+ // Extract successful records and error details
53
+ const aRecords = aResult.status === 'fulfilled' ? aResult.value : [];
54
+ const aError = aResult.status === 'rejected' ? aResult.reason : null;
55
+
56
+ const aaaaRecords = aaaaResult.status === 'fulfilled' ? aaaaResult.value : [];
57
+ const aaaaError = aaaaResult.status === 'rejected' ? aaaaResult.reason : null;
58
+
59
+ // Classify errors: void (no records exist) vs real (DNS server error)
60
+ // Void errors: ENOTFOUND (no such domain), ENODATA (domain exists but no records)
61
+ const aIsVoid = aError && (aError.code === 'ENOTFOUND' || aError.code === 'ENODATA');
62
+ const aaaaIsVoid = aaaaError && (aaaaError.code === 'ENOTFOUND' || aaaaError.code === 'ENODATA');
63
+
64
+ // Propagate real DNS errors for the record type matching the client's IP family
65
+ // IPv6 client: throw AAAA errors (except void), ignore A errors
66
+ if (isIPv6 && aaaaError && !aaaaIsVoid) {
67
+ throw aaaaError;
68
+ }
69
+ // IPv4 client: throw A errors (except void), ignore AAAA errors
70
+ if (!isIPv6 && aError && !aIsVoid) {
71
+ throw aError;
72
+ }
73
+
74
+ // Only throw void error if BOTH record types are void
75
+ // This prevents single-stack hosts from being counted as void lookups
76
+ if (aIsVoid && aaaaIsVoid) {
77
+ // Prefer the error matching client IP type for better error messages
78
+ let voidError = isIPv6 ? aaaaError || aError : aError || aaaaError;
79
+ throw voidError;
80
+ }
81
+
82
+ // Return only the records matching the client's IP type
83
+ // Empty arrays are valid (host exists but doesn't match client IP type)
84
+ return isIPv6 ? aaaaRecords : aRecords;
85
+ };
86
+
87
+ /**
88
+ * Creates a rate-limited DNS resolver with SPF-specific constraints
89
+ *
90
+ * SPF evaluation must enforce limits to prevent DoS:
91
+ * - Maximum 10 DNS lookups per SPF check (mechanisms that trigger DNS: a, mx, ptr, exists, include, redirect)
92
+ * - Maximum 2 "void" lookups (queries returning no records)
93
+ * Mailauth allows to configure both if different limits are required.
94
+ *
95
+ * @param {Function} resolver - Base DNS resolver function (e.g., dns.promises.resolve)
96
+ * @param {Number} maxResolveCount - Maximum DNS lookups allowed (default: 10)
97
+ * @param {Number} maxVoidCount - Maximum void lookups allowed (default: 2)
98
+ * @param {Boolean} ignoreFirst - If true, don't count the first DNS lookup (used for initial TXT record fetch)
99
+ * @returns {Function} - Rate-limited resolver function with signature: (domain, type, opts) => Promise<Array>
100
+ */
23
101
  let limitedResolver = (resolver, maxResolveCount, maxVoidCount, ignoreFirst) => {
24
102
  let resolveCount = 0;
25
103
  let voidCount = 0;
@@ -30,15 +108,16 @@ let limitedResolver = (resolver, maxResolveCount, maxVoidCount, ignoreFirst) =>
30
108
  maxResolveCount = maxResolveCount || MAX_RESOLVE_COUNT;
31
109
  maxVoidCount = maxVoidCount || MAX_VOID_COUNT;
32
110
 
33
- let resolverFunc = async (domain, type) => {
34
- // do not allow to make more that MAX_RESOLVE_COUNT DNS requests per SPF check
35
-
111
+ let resolverFunc = async (domain, type, opts) => {
112
+ // Increment DNS lookup counter
113
+ // Note: Dual-stack queries (A+AAAA) still count as 1 lookup
36
114
  if (firstCounted) {
37
115
  resolveCount++;
38
116
  } else {
39
117
  firstCounted = true;
40
118
  }
41
119
 
120
+ // Enforce maximum DNS lookup limit
42
121
  if (resolveCount > maxResolveCount) {
43
122
  let error = new Error('Too many DNS requests');
44
123
  error.spfResult = {
@@ -48,8 +127,9 @@ let limitedResolver = (resolver, maxResolveCount, maxVoidCount, ignoreFirst) =>
48
127
  throw error;
49
128
  }
50
129
 
130
+ // Validate domain name format before querying
131
+ // This is a lenient check to pass test suites and prevent obvious invalid queries
51
132
  try {
52
- // domain check is pretty lax, mostly to pass the test suite
53
133
  if (!/^([\x20-\x2D\x2f-\x7e]+\.)+[a-z]+[a-z\-0-9]*$/i.test(domain)) {
54
134
  throw new Error('Failed to validate domain');
55
135
  }
@@ -61,13 +141,24 @@ let limitedResolver = (resolver, maxResolveCount, maxVoidCount, ignoreFirst) =>
61
141
  throw err;
62
142
  }
63
143
 
144
+ // Execute DNS query with dual-stack optimization for A/AAAA queries
64
145
  try {
65
- let result = await resolver(domain, type);
66
- return result;
146
+ // Use dual-stack resolver when:
147
+ // 1. Query type is A or AAAA (address lookups)
148
+ // 2. Client IP type is provided (4 for IPv4, 6 for IPv6)
149
+ // This prevents single-stack hosts from being counted as void lookups
150
+ if (opts?.clientIpType && (type === 'A' || type === 'AAAA')) {
151
+ return await dualStackResolver(resolver, domain, opts);
152
+ } else {
153
+ // Standard single-query resolution for other record types (TXT, MX, PTR, etc.) and A if no client info provided.
154
+ return await resolver(domain, type);
155
+ }
67
156
  } catch (err) {
68
157
  switch (err.code) {
69
- case 'ENOTFOUND':
158
+ case 'ENOTFOUND': // Domain does not exist
70
159
  case 'ENODATA': {
160
+ // Domain exists but has no records of this type
161
+ // Increment void lookup counter
71
162
  voidCount++;
72
163
  if (voidCount > maxVoidCount) {
73
164
  err.spfResult = {
@@ -76,10 +167,12 @@ let limitedResolver = (resolver, maxResolveCount, maxVoidCount, ignoreFirst) =>
76
167
  };
77
168
  throw err;
78
169
  }
170
+ // Return empty array to continue SPF evaluation
79
171
  return [];
80
172
  }
81
173
 
82
174
  case 'ETIMEOUT':
175
+ // DNS server timeout - temporary error
83
176
  err.spfResult = {
84
177
  error: 'temperror',
85
178
  text: 'DNS timeout'
@@ -87,6 +180,7 @@ let limitedResolver = (resolver, maxResolveCount, maxVoidCount, ignoreFirst) =>
87
180
  throw err;
88
181
 
89
182
  case 'EREFUSED':
183
+ // DNS server refused query - temporary error
90
184
  err.spfResult = {
91
185
  error: 'temperror',
92
186
  text: `DNS request refused by server when resolving ${domain}`
@@ -94,6 +188,7 @@ let limitedResolver = (resolver, maxResolveCount, maxVoidCount, ignoreFirst) =>
94
188
  throw err;
95
189
 
96
190
  default:
191
+ // Unknown error - propagate as-is
97
192
  throw err;
98
193
  }
99
194
  }
@@ -336,7 +336,9 @@ const spfVerify = async (domain, opts) => {
336
336
  // ignore punycode conversion errors
337
337
  }
338
338
 
339
- let responses = await resolver(a, net.isIPv6(opts.ip) ? 'AAAA' : 'A');
339
+ // Query A or AAAA based on client IP type, with dual-stack void optimization
340
+ // Pass clientIpType to enable smart void counting (see dualStackResolver in index.js)
341
+ let responses = await resolver(a, net.isIPv6(opts.ip) ? 'AAAA' : 'A', { clientIpType: net.isIPv6(opts.ip) ? 6 : 4 });
340
342
  if (responses) {
341
343
  for (let ip of responses) {
342
344
  if (matchIp(addr, ip + cidr)) {
@@ -352,6 +354,8 @@ const spfVerify = async (domain, opts) => {
352
354
  let { domain: mxDomain, cidr4, cidr6 } = parseCidrValue(val, domain, type);
353
355
  let cidr = net.isIPv6(opts.ip) ? cidr6 : cidr4;
354
356
 
357
+ mxDomain = macro(mxDomain, opts);
358
+
355
359
  try {
356
360
  mxDomain = punycode.toASCII(mxDomain);
357
361
  } catch (err) {
@@ -360,13 +364,18 @@ const spfVerify = async (domain, opts) => {
360
364
 
361
365
  let mxList = await resolver(mxDomain, 'MX');
362
366
  if (mxList) {
363
- // MX resolver has separate counter
367
+ // MX mechanism uses a separate resolver with independent DNS lookup counter
368
+ // This prevents MX A/AAAA lookups from consuming the main query limit
364
369
  let subResolver = typeof opts.createSubResolver === 'function' ? opts.createSubResolver() : resolver;
365
370
  try {
366
371
  mxList = mxList.sort((a, b) => a.priority - b.priority);
367
372
  for (let mx of mxList) {
368
373
  if (mx.exchange) {
369
- let responses = await subResolver(mx.exchange, net.isIPv6(opts.ip) ? 'AAAA' : 'A');
374
+ // Query A or AAAA for each MX host, with dual-stack void optimization
375
+ // Pass clientIpType to enable smart void counting (see dualStackResolver in index.js)
376
+ let responses = await subResolver(mx.exchange, net.isIPv6(opts.ip) ? 'AAAA' : 'A', {
377
+ clientIpType: net.isIPv6(opts.ip) ? 6 : 4
378
+ });
370
379
  if (responses) {
371
380
  for (let a of responses) {
372
381
  if (matchIp(addr, a + cidr)) {
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "mailauth",
3
- "version": "4.9.4",
3
+ "version": "4.10.0",
4
4
  "description": "Email authentication library for Node.js",
5
5
  "main": "lib/mailauth.js",
6
6
  "scripts": {
7
7
  "test": "eslint \"lib/**/*.js\" \"test/**/*.js\" && mocha --recursive \"./test/**/*.js\" --reporter spec",
8
+ "format": "prettier --write .",
8
9
  "build-source": "rm -rf node_modules package-lock.json && npm install && npm run licenses && rm -rf node_modules package-lock.json && npm install --production && rm -rf package-lock.json",
9
10
  "build-dist": "npx pkg --compress Brotli package.json && rm -rf package-lock.json && npm install && node winconf.js",
10
11
  "build-dist-fast": "pkg --debug package.json && npm install && node winconf.js",
@@ -33,14 +34,15 @@
33
34
  "homepage": "https://github.com/postalsys/mailauth",
34
35
  "devDependencies": {
35
36
  "chai": "4.4.1",
36
- "eslint": "8.56.0",
37
+ "eslint": "9.38.0",
37
38
  "eslint-config-nodemailer": "1.2.0",
38
- "eslint-config-prettier": "9.1.0",
39
+ "eslint-config-prettier": "10.1.8",
39
40
  "js-yaml": "4.1.0",
40
- "license-report": "6.8.0",
41
+ "license-report": "6.8.1",
41
42
  "mbox-reader": "1.2.0",
42
- "mocha": "11.7.2",
43
- "resedit": "^2.0.3"
43
+ "mocha": "11.7.4",
44
+ "prettier": "^3.6.2",
45
+ "resedit": "^3.0.0"
44
46
  },
45
47
  "dependencies": {
46
48
  "@postalsys/vmc": "1.1.2",
@@ -48,10 +50,10 @@
48
50
  "ipaddr.js": "2.2.0",
49
51
  "joi": "18.0.1",
50
52
  "libmime": "5.3.7",
51
- "nodemailer": "7.0.6",
53
+ "nodemailer": "7.0.10",
52
54
  "punycode.js": "2.3.1",
53
- "tldts": "7.0.12",
54
- "undici": "7.15.0",
55
+ "tldts": "7.0.17",
56
+ "undici": "7.16.0",
55
57
  "yargs": "17.7.2"
56
58
  },
57
59
  "engines": {
@@ -0,0 +1,9 @@
1
+ {
2
+ "packages": {
3
+ ".": {
4
+ "release-type": "node",
5
+ "package-name": "mailauth",
6
+ "pull-request-title-pattern": "chore${scope}: release ${version} [skip-ci]"
7
+ }
8
+ }
9
+ }