mailauth 4.7.2 → 4.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.8.0](https://github.com/postalsys/mailauth/compare/v4.7.3...v4.8.0) (2024-11-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * **cert-type:** BIMI authority information includes the type of the cert ('VMC' or 'CMC') ([0dd8db8](https://github.com/postalsys/mailauth/commit/0dd8db81b2ffc8b9d84d1a4396c65bfa9a347088))
9
+
10
+ ## [4.7.3](https://github.com/postalsys/mailauth/compare/v4.7.2...v4.7.3) (2024-10-21)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **BodyHashStream:** Skip header ([3da03d2](https://github.com/postalsys/mailauth/commit/3da03d23baa90acb119c7946c2cd740a72ba069d))
16
+
3
17
  ## [4.7.2](https://github.com/postalsys/mailauth/compare/v4.7.1...v4.7.2) (2024-10-02)
4
18
 
5
19
 
package/README.md CHANGED
@@ -1,84 +1,127 @@
1
- ![](https://github.com/postalsys/mailauth/raw/master/assets/mailauth.png)
1
+ # mailauth: Email Authentication for Node.js
2
2
 
3
- [Command line utility](cli.md) and a Node.js library for email authentication.
3
+ ![mailauth Logo](https://github.com/postalsys/mailauth/raw/master/assets/mailauth.png)
4
+
5
+ **mailauth** is a comprehensive Node.js library and command-line utility for email authentication. It provides tools to work with various email security protocols, including SPF, DKIM, DMARC, ARC, BIMI, and MTA-STS. With mailauth, you can verify and sign emails, handle authentication results, and enhance your email security setup.
6
+
7
+ **Key Features:**
4
8
 
5
9
  - **SPF** verification
6
- - **DKIM** signing
7
- - DKIM verification
10
+ - **DKIM** signing and verification
8
11
  - **DMARC** verification
9
- - **ARC** verification
10
- - ARC sealing
11
- - Sealing on authentication
12
- - Sealing after modifications
12
+ - **ARC** verification and sealing
13
+ - Sealing during authentication
14
+ - Sealing after message modifications
13
15
  - **BIMI** resolving and **VMC** validation
14
- - **MTA-STS** helpers
16
+ - **MTA-STS** helper functions
17
+
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
+
20
+ ## Table of Contents
21
+
22
+ 1. [Installation](#installation)
23
+ 2. [Command-Line Usage](#command-line-usage)
24
+ 3. [Library Usage](#library-usage)
25
+ - [Authentication](#authentication)
26
+ - [DKIM](#dkim)
27
+ - [Signing](#dkim-signing)
28
+ - [Verification](#dkim-verification)
29
+ - [SPF](#spf)
30
+ - [Verification](#spf-verification)
31
+ - [ARC](#arc)
32
+ - [Validation](#arc-validation)
33
+ - [Sealing](#arc-sealing)
34
+ - [DMARC](#dmarc)
35
+ - [Helpers](#dmarc-helpers)
36
+ - [BIMI](#bimi)
37
+ - [MTA-STS](#mta-sts)
38
+ - [Policy Retrieval](#policy-retrieval)
39
+ - [MX Validation](#mx-validation)
40
+ 4. [Testing](#testing)
41
+ 5. [License](#license)
42
+
43
+ ## Installation
44
+
45
+ First, install mailauth from npm:
46
+
47
+ ```bash
48
+ npm install mailauth
49
+ ```
15
50
 
16
- Pure JavaScript implementation, no external applications or compilation needed. It runs on any server/device that has Node 16+ installed.
51
+ Then, import the desired methods into your script:
17
52
 
18
- ## Command line usage
53
+ ```javascript
54
+ const { authenticate } = require('mailauth');
55
+ ```
19
56
 
20
- See [command line documentation](cli.md) for the `mailauth` command.
57
+ ## Command-Line Usage
58
+
59
+ mailauth includes a command-line utility called `mailauth`. For detailed information on how to use it, see the [command-line documentation](cli.md).
21
60
 
22
61
  ## Library Usage
23
62
 
24
- ## Authentication
63
+ ### Authentication
64
+
65
+ Use the `authenticate` function to validate DKIM signatures, SPF, DMARC, ARC, and BIMI for an email.
25
66
 
26
- Validate DKIM signatures, SPF, DMARC, ARC, and BIMI for an email.
67
+ #### Syntax
27
68
 
28
- ```js
29
- await authenticate(message [,options]) ->
30
- { dkim, spf, arc, dmarc, bimi, receivedChain, headers }
69
+ ```javascript
70
+ await authenticate(message [, options])
71
+ // Returns: { dkim, spf, arc, dmarc, bimi, receivedChain, headers }
31
72
  ```
32
73
 
33
- Where
34
-
35
- - **message** is either a String, a Buffer, or a Readable stream that represents an email message
36
- - **options** (_object_) is an optional options object
37
- - **sender** (_string_) is the email address from MAIL FROM command. If not set, then it is parsed from the `Return-Path` header
38
- - **ip** (_string_) is the IP of the remote client that sent this message
39
- - **helo** (_string_) is the hostname value from HELO/EHLO command
40
- - **trustReceived** (_boolean_) if true, then parses values for `ip` and `helo` from the latest `Received` header if you have not set these values yourself. Defaults to `false`.
41
- - **mta** (_string_) is the hostname of the server performing the authentication (defaults to `os.hostname()`)
42
- - **minBitLength** (_number_) is the minimum allowed bits of RSA public keys (defaults to 1024). If a DKIM or ARC key has fewer bits, then validation is considered as failed
43
- - **disableArc** (_boolean_) if true then skip ARC checks
44
- - **disableDmarc** (_boolean_) if true then skip DMARC checks. It also disables checks that are dependent on DMARC (e.g., BIMI)
45
- - **disableBimi** (_boolean_) if true, then skip BIMI checks
46
- - **seal** (_object_) if set and message does not have a broken ARC chain, then seals the message using these values
47
- - **signingDomain** (_string_) ARC key domain name
48
- - **selector** (_string_) ARC key selector
49
- - **privateKey** (_string_ or _buffer_) Private key for signing. Either an RSA or an Ed25519 key
50
- - **resolver** (_async function_) is an optional async function for DNS requests. Defaults to [dns.promises.resolve](https://nodejs.org/api/dns.html#dns_dnspromises_resolve_hostname_rrtype)
51
- - **maxResolveCount** (_number_ defaults to _10_) is the DNS lookup limit for SPF. [RFC7208](https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4) requires this limit to be 10.
52
- - **maxVoidCount** (_number_ defaults to _2_) is the DNS lookup limit for SPF that produce an empty result. [RFC7208](https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4) requires this limit to be 2.
53
-
54
- **Example**
55
-
56
- ```js
74
+ #### Parameters
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).
94
+
95
+ #### Example
96
+
97
+ ```javascript
57
98
  const { authenticate } = require('mailauth');
58
- const { dkim, spf, arc, dmarc, bimi, receivedChain, headers } = await authenticate(
59
- message, // either a String, a Buffer or a Readable Stream
60
- {
61
- // SMTP transmission options if available
62
- ip: '217.146.67.33', // SMTP client IP
63
- helo: 'uvn-67-33.tll01.zonevs.eu', // EHLO/HELO hostname
64
- sender: 'andris@ekiri.ee', // MAIL FROM address
65
-
66
- // Uncomment if you do not want to provide ip/helo/sender manually but parse from the message
67
- //trustReceived: true,
68
-
69
- // Server processing this message, defaults to os.hostname(). Inserted into Authentication headers
70
- mta: 'mx.ethereal.email',
71
-
72
- // Optional DNS resolver function (defaults to `dns.promises.resolve`)
73
- resolver: async (name, rr) => await dns.promises.resolve(name, rr)
74
- }
75
- );
76
- // output authenticated message
77
- process.stdout.write(headers); // includes terminating line break
99
+ const dns = require('dns');
100
+
101
+ const message = /* Your email message here */;
102
+
103
+ const { dkim, spf, arc, dmarc, bimi, receivedChain, headers } = await authenticate(message, {
104
+ // SMTP transmission options
105
+ ip: '217.146.67.33', // SMTP client IP
106
+ helo: 'uvn-67-33.tll01.zonevs.eu', // HELO/EHLO hostname
107
+ sender: 'andris@ekiri.ee', // MAIL FROM address
108
+
109
+ // Uncomment to parse `ip` and `helo` from the latest `Received` header
110
+ // trustReceived: true,
111
+
112
+ // Server performing the authentication
113
+ mta: 'mx.ethereal.email',
114
+
115
+ // Optional DNS resolver function
116
+ resolver: async (name, rr) => await dns.promises.resolve(name, rr),
117
+ });
118
+
119
+ // Output authenticated message
120
+ process.stdout.write(headers); // Includes terminating line break
78
121
  process.stdout.write(message);
79
122
  ```
80
123
 
81
- Example output:
124
+ **Sample Output:**
82
125
 
83
126
  ```
84
127
  Received-SPF: pass (mx.ethereal.email: domain of andris@ekiri.ee designates 217.146.67.33 as permitted sender) client-ip=217.146.67.33;
@@ -91,65 +134,72 @@ Authentication-Results: mx.ethereal.email;
91
134
  From: ...
92
135
  ```
93
136
 
94
- You can see the full output (structured data for DKIM, SPF, DMARC, and ARC) from [this example](https://gist.github.com/andris9/6514b5e7c59154a5b08636f99052ce37).
137
+ You can see the full output, including structured data for DKIM, SPF, DMARC, and ARC, from [this example](https://gist.github.com/andris9/6514b5e7c59154a5b08636f99052ce37).
138
+
139
+ **Note:** The `receivedChain` property is an array of parsed representations of the `Received:` headers.
140
+
141
+ ### DKIM
95
142
 
96
- ### receivedChain
143
+ #### DKIM Signing
97
144
 
98
- `receivedChain` property is an array of parsed representations of the `Received:` headers.
145
+ Use the `dkimSign` function to sign an email message with DKIM.
99
146
 
100
- ## DKIM
147
+ ##### Syntax
101
148
 
102
- ### Signing
149
+ ```javascript
150
+ const { dkimSign } = require('mailauth/lib/dkim/sign');
151
+
152
+ const signResult = await dkimSign(message, options);
153
+ // Returns: { signatures: String, errors: Array }
154
+ ```
155
+
156
+ ##### Parameters
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.
103
170
 
104
- ```js
171
+ ##### Example
172
+
173
+ ```javascript
105
174
  const { dkimSign } = require('mailauth/lib/dkim/sign');
106
- const signResult = await dkimSign(
107
- message, // either a String, a Buffer or a Readable Stream
175
+ const fs = require('fs');
176
+
177
+ const message = /* Your email message here */;
178
+
179
+ const signResult = await dkimSign(message, {
180
+ canonicalization: 'relaxed/relaxed',
181
+ algorithm: 'rsa-sha256',
182
+ signTime: new Date(),
183
+ signatureData: [
108
184
  {
109
- // Optional, default canonicalization, default is "relaxed/relaxed"
110
- canonicalization: 'relaxed/relaxed', // c=
111
-
112
- // Optional, default signing and hashing algorithm
113
- // Mostly useful when you want to use rsa-sha1, otherwise no need to set
114
- algorithm: 'rsa-sha256',
115
-
116
- // Optional, default is current time
117
- signTime: new Date(), // t=
118
-
119
- // Keys for one or more signatures
120
- // Different signatures can use different algorithms (mostly useful when
121
- // you want to sign a message both with RSA and Ed25519)
122
- signatureData: [
123
- {
124
- signingDomain: 'tahvel.info', // d=
125
- selector: 'test.rsa', // s=
126
- // supported key types: RSA, Ed25519
127
- privateKey: fs.readFileSync('./test/fixtures/private-rsa.pem'),
128
-
129
- // Optional algorithm, default is derived from the key.
130
- // Overrides whatever was set in parent object
131
- algorithm: 'rsa-sha256',
132
-
133
- // Optional signature specifc canonicalization, overrides whatever was set in parent object
134
- canonicalization: 'relaxed/relaxed' // c=
135
-
136
- // Maximum number of canonicalized body bytes to sign (eg. the "l=" tag).
137
- // Do not use though. This is available only for compatibility testing.
138
- // maxBodyLength: 12345
139
- }
140
- ]
141
- }
142
- ); // -> {signatures: String, errors: Array} signature headers using \r\n as the line separator
143
- // show signing errors (if any)
185
+ signingDomain: 'tahvel.info',
186
+ selector: 'test.rsa',
187
+ privateKey: fs.readFileSync('./test/fixtures/private-rsa.pem'),
188
+ },
189
+ ],
190
+ });
191
+
192
+ // Display signing errors if any
144
193
  if (signResult.errors.length) {
145
- console.log(signResult.errors);
194
+ console.error('Signing errors:', signResult.errors);
146
195
  }
147
- // output signed message
148
- process.stdout.write(signResult.signatures); // includes terminating line break
196
+
197
+ // Output signed message
198
+ process.stdout.write(signResult.signatures); // Includes terminating line break
149
199
  process.stdout.write(message);
150
200
  ```
151
201
 
152
- Example output:
202
+ **Sample Output:**
153
203
 
154
204
  ```
155
205
  DKIM-Signature: a=rsa-sha256; v=1; c=relaxed/relaxed; d=tahvel.info;
@@ -157,365 +207,400 @@ DKIM-Signature: a=rsa-sha256; v=1; c=relaxed/relaxed; d=tahvel.info;
157
207
  From: ...
158
208
  ```
159
209
 
160
- ### Signing as a PassThrough Stream
210
+ #### DKIM Signing as a Stream
161
211
 
162
- Use `DkimSignStream` stream if you want to use DKIM signing as part of a stream processing pipeline.
212
+ Use `DkimSignStream` to sign messages as part of a stream processing pipeline.
163
213
 
164
- ```js
214
+ ##### Example
215
+
216
+ ```javascript
165
217
  const { DkimSignStream } = require('mailauth/lib/dkim/sign');
218
+ const fs = require('fs');
166
219
 
167
220
  const dkimSignStream = new DkimSignStream({
168
- // Optional, default canonicalization, default is "relaxed/relaxed"
169
- canonicalization: 'relaxed/relaxed', // c=
170
-
171
- // Optional, default signing and hashing algorithm
172
- // Mostly useful when you want to use rsa-sha1, otherwise no need to set
221
+ canonicalization: 'relaxed/relaxed',
173
222
  algorithm: 'rsa-sha256',
174
-
175
- // Optional, default is current time
176
- signTime: new Date(), // t=
177
-
178
- // Keys for one or more signatures
179
- // Different signatures can use different algorithms (mostly useful when
180
- // you want to sign a message both with RSA and Ed25519)
223
+ signTime: new Date(),
181
224
  signatureData: [
182
225
  {
183
- signingDomain: 'tahvel.info', // d=
184
- selector: 'test.rsa', // s=
185
- // supported key types: RSA, Ed25519
186
- privateKey: fs.readFileSync('./test/fixtures/private-rsa.pem'),
187
-
188
- // Optional algorithm, default is derived from the key.
189
- // Overrides whatever was set in parent object
190
- algorithm: 'rsa-sha256',
191
-
192
- // Optional signature specifc canonicalization, overrides whatever was set in parent object
193
- canonicalization: 'relaxed/relaxed' // c=
194
-
195
- // Maximum number of canonicalized body bytes to sign (eg. the "l=" tag).
196
- // Do not use though. This is available only for compatibility testing.
197
- // maxBodyLength: 12345
226
+ signingDomain: 'tahvel.info',
227
+ selector: 'test.rsa',
228
+ privateKey: fs.readFileSync('./test/fixtures/private-rsa.pem')
198
229
  }
199
230
  ]
200
231
  });
201
232
 
202
- // Writes a signed message to the output
233
+ // Read from stdin, write signed message to stdout
203
234
  process.stdin.pipe(dkimSignStream).pipe(process.stdout);
204
235
  ```
205
236
 
206
- ### Verifying
237
+ #### DKIM Verification
238
+
239
+ Use the `dkimVerify` function to verify DKIM signatures in an email message.
207
240
 
208
- ```js
241
+ ##### Syntax
242
+
243
+ ```javascript
209
244
  const { dkimVerify } = require('mailauth/lib/dkim/verify');
210
- // `message` is either a String, a Buffer or a Readable Stream
245
+
211
246
  const result = await dkimVerify(message);
212
- for (let { info } of result.results) {
213
- console.log(info);
247
+ // Returns an object containing verification results
248
+ ```
249
+
250
+ ##### Example
251
+
252
+ ```javascript
253
+ const { dkimVerify } = require('mailauth/lib/dkim/verify');
254
+
255
+ const message = /* Your email message here */;
256
+
257
+ const result = await dkimVerify(message);
258
+
259
+ for (const { info } of result.results) {
260
+ console.log(info);
214
261
  }
215
262
  ```
216
263
 
217
- Example output:
264
+ **Sample Output:**
218
265
 
219
- ```txt
266
+ ```
220
267
  dkim=neutral (invalid public key) header.i=@tahvel.info header.s=test.invalid header.b="b85yao+1"
221
268
  dkim=pass header.i=@tahvel.info header.s=test.rsa header.b="BrEgDN4A"
222
269
  dkim=policy policy.dkim-rules=weak-key header.i=@tahvel.info header.s=test.small header.b="d0jjgPun"
223
270
  ```
224
271
 
225
- ## SPF
272
+ ### SPF
273
+
274
+ #### SPF Verification
226
275
 
227
- ### Verifying
276
+ Use the `spf` function to verify the SPF record for an email sender.
228
277
 
229
- ```js
278
+ ##### Syntax
279
+
280
+ ```javascript
281
+ const { spf } = require('mailauth/lib/spf');
282
+
283
+ const result = await spf(options);
284
+ // Returns an object containing SPF verification results
285
+ ```
286
+
287
+ ##### Parameters
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.
294
+
295
+ ##### Example
296
+
297
+ ```javascript
230
298
  const { spf } = require('mailauth/lib/spf');
231
299
 
232
- let result = await spf({
300
+ const result = await spf({
233
301
  sender: 'andris@wildduck.email',
234
302
  ip: '217.146.76.20',
235
303
  helo: 'foo',
236
304
  mta: 'mx.myhost.com'
237
305
  });
306
+
238
307
  console.log(result.header);
239
308
  ```
240
309
 
241
- Example output:
310
+ **Sample Output:**
242
311
 
243
- ```txt
312
+ ```
244
313
  Received-SPF: pass (mx.myhost.com: domain of andris@wildduck.email
245
314
  designates 217.146.76.20 as permitted sender) client-ip=217.146.76.20;
246
315
  envelope-from="andris@wildduck.email";
247
316
  ```
248
317
 
249
- ## ARC
318
+ ### ARC
250
319
 
251
- ### Validation
320
+ #### ARC Validation
252
321
 
253
- ARC seals are automatically validated during the authentication step.
322
+ ARC seals are validated automatically during the authentication step.
254
323
 
255
- ```js
324
+ ##### Example
325
+
326
+ ```javascript
256
327
  const { authenticate } = require('mailauth');
257
- const { arc } = await authenticate(
258
- message, // either a String, a Buffer or a Readable Stream
259
- {
260
- trustReceived: true
261
- }
262
- );
328
+
329
+ const message = /* Your email message here */;
330
+
331
+ const { arc } = await authenticate(message, {
332
+ trustReceived: true,
333
+ });
334
+
263
335
  console.log(arc);
264
336
  ```
265
337
 
266
- The output is something like this:
338
+ **Sample Output:**
267
339
 
268
- ```
340
+ ```json
269
341
  {
270
- "status": {
271
- "result": "pass",
272
- "comment": "i=2 spf=neutral dkim=pass dkdomain=zonevs.eu dkim=pass dkdomain=srs3.zonevs.eu dmarc=fail fromdomain=zone.ee"
273
- },
274
- "i": 2,
275
- ...
342
+ "status": {
343
+ "result": "pass",
344
+ "comment": "i=2 spf=neutral dkim=pass dkdomain=zonevs.eu dkim=pass dkdomain=srs3.zonevs.eu dmarc=fail fromdomain=zone.ee"
345
+ },
346
+ "i": 2
347
+ // Additional properties...
276
348
  }
277
349
  ```
278
350
 
279
- ### Sealing
351
+ #### ARC Sealing
352
+
353
+ You can seal messages with ARC either during authentication or after modifications.
280
354
 
281
- #### During authentication
355
+ ##### Sealing During Authentication
282
356
 
283
- You can seal messages with ARC automatically in the authentication step by providing the sealing key. In this case, you can not modify the message any more as this would break the seal.
357
+ Provide the sealing key in the options to seal messages automatically during authentication.
284
358
 
285
- ```js
359
+ ```javascript
286
360
  const { authenticate } = require('mailauth');
287
- const { headers } = await authenticate(
288
- message, // either a String, a Buffer or a Readable Stream
289
- {
290
- trustReceived: true,
361
+ const fs = require('fs');
291
362
 
292
- // ARC seal settings. If this is set then resulting headers include
293
- // a complete ARC header set (unless the message has a failing ARC chain)
294
- seal: {
295
- signingDomain: 'tahvel.info',
296
- selector: 'test.rsa',
297
- privateKey: fs.readFileSync('./test/fixtures/private-rsa.pem')
298
- }
299
- }
300
- );
301
- // output authenticated and sealed message
302
- process.stdout.write(headers); // includes terminating line break
363
+ const message = /* Your email message here */;
364
+
365
+ const { headers } = await authenticate(message, {
366
+ trustReceived: true,
367
+ seal: {
368
+ signingDomain: 'tahvel.info',
369
+ selector: 'test.rsa',
370
+ privateKey: fs.readFileSync('./test/fixtures/private-rsa.pem'),
371
+ },
372
+ });
373
+
374
+ // Output authenticated and sealed message
375
+ process.stdout.write(headers); // Includes terminating line break
303
376
  process.stdout.write(message);
304
377
  ```
305
378
 
306
- #### After modifications
379
+ ##### Sealing After Modifications
307
380
 
308
- If you want to modify the message before sealing, you have to authenticate the message first and then use authentication results as input for the sealing step.
381
+ If you need to modify the message before sealing, first authenticate it, modify as needed, then seal using the authentication results.
309
382
 
310
- ```js
383
+ ```javascript
311
384
  const { authenticate, sealMessage } = require('mailauth');
385
+ const fs = require('fs');
312
386
 
313
- // 1. authenticate the message
314
- const { arc, headers } = await authenticate(
315
- message, // either a String, a Buffer or a Readable Stream
316
- {
317
- ip: '217.146.67.33', // SMTP client IP
318
- helo: 'uvn-67-33.tll01.zonevs.eu', // EHLO/HELO hostname
319
- mta: 'mx.ethereal.email', // server processing this message, defaults to os.hostname()
320
- sender: 'andris@ekiri.ee' // MAIL FROM address
321
- }
322
- );
387
+ const message = /* Your email message here */;
323
388
 
324
- // 2. perform some modifications with the message ...
389
+ // Step 1: Authenticate the message
390
+ const { arc, headers } = await authenticate(message, {
391
+ ip: '217.146.67.33',
392
+ helo: 'uvn-67-33.tll01.zonevs.eu',
393
+ mta: 'mx.ethereal.email',
394
+ sender: 'andris@ekiri.ee',
395
+ });
325
396
 
326
- // 3. seal the modified message using the initial authentication results
327
- const sealHeaders = await sealMessage(message, {
328
- signingDomain: 'tahvel.info',
329
- selector: 'test.rsa',
330
- privateKey: fs.readFileSync('./test/fixtures/private-rsa.pem'),
397
+ // Step 2: Modify the message as needed
398
+ // ... your modifications ...
331
399
 
332
- // values from the authentication step
333
- authResults: arc.authResults,
334
- cv: arc.status.result
400
+ // Step 3: Seal the modified message
401
+ const sealHeaders = await sealMessage(message, {
402
+ signingDomain: 'tahvel.info',
403
+ selector: 'test.rsa',
404
+ privateKey: fs.readFileSync('./test/fixtures/private-rsa.pem'),
405
+ authResults: arc.authResults,
406
+ cv: arc.status.result,
335
407
  });
336
408
 
337
- // output authenticated message
338
- process.stdout.write(sealHeaders); // ARC set
339
- process.stdout.write(headers); // authentication results
409
+ // Output the sealed message
410
+ process.stdout.write(sealHeaders); // ARC headers
411
+ process.stdout.write(headers); // Authentication results
340
412
  process.stdout.write(message);
341
413
  ```
342
414
 
343
- ## DMARC
415
+ ### DMARC
416
+
417
+ DMARC is verified during the authentication process. Although the `dmarc` handler is exported, it requires input from previous steps like SPF and DKIM.
344
418
 
345
- DMARC is verified as part of the authentication process and even as the `dmarc` handler is exported, it requires input from previous steps.
419
+ #### DMARC Helpers
346
420
 
347
- ### Helpers
421
+ ##### `getDmarcRecord(domain [, resolver])`
348
422
 
349
- #### getDmarcRecord(domain [,resolver])
423
+ Fetches and parses the DMARC DNS record for a domain or subdomain. Returns `false` if no record exists.
350
424
 
351
- Returns parsed DMARC DNS record for a domain or a subdomain or `false` is no record exists.
425
+ ###### Syntax
426
+
427
+ ```javascript
428
+ const getDmarcRecord = require('mailauth/lib/dmarc/get-dmarc-record');
352
429
 
430
+ const dmarcRecord = await getDmarcRecord(domain [, resolver]);
431
+ // Returns an object with DMARC record details or `false` if not found
353
432
  ```
433
+
434
+ ###### Parameters
435
+
436
+ - **domain** (`string`): The domain to check for a DMARC record.
437
+ - **resolver** (`function`, optional): Custom DNS resolver function. Defaults to `dns.resolve`.
438
+
439
+ ###### Example
440
+
441
+ ```javascript
354
442
  const getDmarcRecord = require('mailauth/lib/dmarc/get-dmarc-record');
355
- const dmarcRecord = await getDmarcRecord("ethereal.email");
443
+
444
+ const dmarcRecord = await getDmarcRecord('ethereal.email');
356
445
  console.log(dmarcRecord);
357
446
  ```
358
447
 
359
- **Output**
448
+ **Sample Output:**
360
449
 
361
- ```
450
+ ```json
362
451
  {
363
- v: 'DMARC1',
364
- p: 'none',
365
- pct: 100,
366
- rua: 'mailto:re+joqy8fpatm3@dmarc.postmarkapp.com',
367
- sp: 'none',
368
- aspf: 'r',
369
- rr: 'v=DMARC1; p=none; pct=100; rua=mailto:re+joqy8fpatm3@dmarc.postmarkapp.com; sp=none; aspf=r;',
370
- isOrgRecord: false
452
+ "v": "DMARC1",
453
+ "p": "none",
454
+ "pct": 100,
455
+ "rua": "mailto:re+joqy8fpatm3@dmarc.postmarkapp.com",
456
+ "sp": "none",
457
+ "aspf": "r",
458
+ "rr": "v=DMARC1; p=none; pct=100; rua=mailto:re+joqy8fpatm3@dmarc.postmarkapp.com; sp=none; aspf=r;",
459
+ "isOrgRecord": false
371
460
  }
372
461
  ```
373
462
 
374
- `isOrgRecord` is `true` for sudomains, where organizational domain's DMARC policy applies, so use the `sp`, not `p` policy.
463
+ ### BIMI
375
464
 
376
- Optionally set `resolver` argument with custom resolver (uses `dns.resolve` by default).
465
+ Brand Indicators for Message Identification (BIMI) support is based on [draft-blank-ietf-bimi-02](https://tools.ietf.org/html/draft-blank-ietf-bimi-02). BIMI information is resolved during the authentication step, provided the message passes DMARC validation with a policy other than "none".
377
466
 
378
- ## BIMI
467
+ #### Example
379
468
 
380
- Brand Indicators for Message Identification (BIMI) support is based on [draft-blank-ietf-bimi-01](https://tools.ietf.org/html/draft-blank-ietf-bimi-01).
469
+ ```javascript
470
+ const { authenticate } = require('mailauth');
381
471
 
382
- BIMI information is resolved in the authentication step, and the results can be found from the `bimi` property. The message must pass DMARC validation to be processed for BIMI. DMARC policy can not be "none" for BIMI to pass.
472
+ const message = /* Your email message here */;
473
+
474
+ const { bimi } = await authenticate(message, {
475
+ ip: '217.146.67.33',
476
+ helo: 'uvn-67-33.tll01.zonevs.eu',
477
+ mta: 'mx.ethereal.email',
478
+ sender: 'andris@ekiri.ee',
479
+ bimiWithAlignedDkim: false, // If true, ignores SPF in DMARC and requires a valid DKIM signature
480
+ });
383
481
 
384
- ```js
385
- const { bimi } = await authenticate(
386
- message, // either a String, a Buffer or a Readable Stream
387
- {
388
- ip: '217.146.67.33', // SMTP client IP
389
- helo: 'uvn-67-33.tll01.zonevs.eu', // EHLO/HELO hostname
390
- mta: 'mx.ethereal.email', // server processing this message, defaults to os.hostname()
391
- sender: 'andris@ekiri.ee', // MAIL FROM address
392
-
393
- bimiWithAlignedDkim: false // If true then ignores SPF in DMARC and requires a valid DKIM signature
394
- }
395
- );
396
482
  if (bimi?.location) {
397
- console.log(`BIMI location: ${bimi.location}`);
483
+ console.log(`BIMI location: ${bimi.location}`);
398
484
  }
399
485
  ```
400
486
 
401
- `BIMI-Location` header is ignored by `mailauth`, it is not checked for, and it is not modified in any way if it is present. `BIMI-Selector` is used for selector selection (if available).
487
+ **Note:**
402
488
 
403
- ### Verified Mark Certificate
489
+ - The `BIMI-Location` header is ignored by mailauth.
490
+ - The `BIMI-Selector` header can be used for selector selection if available.
404
491
 
405
- Authority Evidence Document location is available from the `bimi.authority` property (if set).
492
+ #### Verified Mark Certificate (VMC)
406
493
 
407
- VMC (Verified Mark Certificates) for Authority Evidence Documents is a X509 certificate with an `id-pe-logotype` extension (`oid=1.3.6.1.5.5.7.1.12`) that includes a compressed SVG formatted logo file ([read more here](https://bimigroup.org/resources/VMC_Guidelines_latest.pdf)).
494
+ If an Authority Evidence Document is specified in the BIMI record, its location is available in `bimi.authority`. mailauth exposes the certificate type (`"VMC"` or `"CMC"`) in `bimi.authority.vmc.type`.
408
495
 
409
- Some example authority evidence documents:
496
+ **Example Authority Evidence Documents:**
410
497
 
411
- - [from default.\_bimi.cnn.com](https://amplify.valimail.com/bimi/time-warner/LysAFUdG-Hw-cnn_vmc.pem)
412
- - [from default.\_bimi.entrustdatacard.com](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)
413
500
 
414
- ## MTA-STS
501
+ ### MTA-STS
415
502
 
416
- `mailauth` allows you to fetch MTA-STS information for a domain name.
503
+ mailauth provides functions to fetch and validate MTA-STS policies for a domain.
417
504
 
418
- ```js
419
- const { getPolicy, validateMx } = require('mailauth/lib/mta-sts');
505
+ #### Policy Retrieval
506
+
507
+ Use the `getPolicy` function to fetch the MTA-STS policy for a domain.
508
+
509
+ ##### Syntax
510
+
511
+ ```javascript
512
+ const { getPolicy } = require('mailauth/lib/mta-sts');
513
+
514
+ const { policy, status } = await getPolicy(domain [, knownPolicy]);
515
+ // Returns an object with the policy and status
516
+ ```
517
+
518
+ ##### Parameters
420
519
 
421
- let knownPolicy = getCachedPolicy('gmail.com'); // optional
422
- let mx = 'alt4.gmail-smtp-in.l.google.com';
520
+ - **domain** (`string`): The domain to retrieve the policy for.
521
+ - **knownPolicy** (`object`, optional): Previously cached policy for the domain.
423
522
 
523
+ ##### Example
524
+
525
+ ```javascript
526
+ const { getPolicy } = require('mailauth/lib/mta-sts');
527
+
528
+ const knownPolicy = /* Retrieve from your cache if available */;
424
529
  const { policy, status } = await getPolicy('gmail.com', knownPolicy);
425
- const policyMatch = validateMx(mx, policy);
426
530
 
427
531
  if (policy.id !== knownPolicy?.id) {
428
- // policy has been updated, update cache
532
+ // Update your cache with the new policy
429
533
  }
430
534
 
431
535
  if (policy.mode === 'enforce') {
432
- // must use TLS
433
- }
434
-
435
- if (policy.mx && !policyMatch.valid) {
436
- // can't connect, unlisted MX
536
+ // TLS must be used when sending to this domain
437
537
  }
438
538
  ```
439
539
 
440
- ### Resolve policy
540
+ **Possible Status Values:**
441
541
 
442
- Resolve MTA-STS policy for a domain
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.
443
547
 
444
- ```
445
- async getPolicy(domain [,knownPolicy]) -> {policy, status}
446
- ```
548
+ #### MX Validation
447
549
 
448
- Where
550
+ Use the `validateMx` function to check if an MX hostname is valid according to the MTA-STS policy.
449
551
 
450
- - **domain** is the domain to check for (e.g. "gmail.com")
451
- - **knownPolicy** (optional) is the policy object from the last check for this domain. This is used to check if the policy is still valid or it was updated.
552
+ ##### Syntax
452
553
 
453
- The function returns an object with the following properties:
554
+ ```javascript
555
+ const { validateMx } = require('mailauth/lib/mta-sts');
454
556
 
455
- - **policy** (object)
456
- - **id** (string or `false`) ID of the policy
457
- - **mode** (string) one of _"none"_, _"testing"_ or _"enforce"_
458
- - **mx** (array, if available) an Array of whitelisted MX hostnames
459
- - **expires** (string, if available) ISO date string for cacheing
460
- - **status** (string) one of the following values:
461
- - _"not_found"_ no policy was found for this domain. You can decide yourself how long you want to cache this response
462
- - _"cached"_ no changes detected, current policy is still valid and can be used
463
- - _"found"_ new or updated policy was found. Cache this in your system until _policy.expires_
464
- - _"renew"_ existing policy is still valid, renew cached version until _policy.expires_
465
- - _"errored"_ policy discovery failed for some temporary error (e.g., failing DNS queries). See _policy.error_ for details
466
-
467
- ### Validate MX hostname
468
-
469
- Check if a resolved MX hostname is valid by MTA-STS policy or not.
470
-
471
- ```
472
- validateMx(mx, policy) -> Object
557
+ const validation = validateMx(mx, policy);
558
+ // Returns an object indicating if the MX is valid
473
559
  ```
474
560
 
475
- Where
561
+ ##### Parameters
476
562
 
477
- - **mx** is the resolved MX hostname (eg. "gmail-smtp-in.l.google.com")
478
- - **policy** is the policy object returned by `getPolicy()`
563
+ - **mx** (`string`): The resolved MX hostname.
564
+ - **policy** (`object`): The MTA-STS policy object.
479
565
 
480
- The function returns an object. If `{valid}` is `true`, then MX hostname is allowed to be used.
566
+ ##### Example
481
567
 
482
- ## Testing
483
-
484
- `mailauth` uses the following test suites:
568
+ ```javascript
569
+ const { getPolicy, validateMx } = require('mailauth/lib/mta-sts');
485
570
 
486
- ### SPF test suite
571
+ const { policy } = await getPolicy('gmail.com');
487
572
 
488
- [OpenSPF test suite](http://www.openspf.org/Test_Suite) ([archive.org mirror](https://web.archive.org/web/20190130131432/http://www.openspf.org/Test_Suite)) with the following differences:
573
+ const mx = 'alt4.gmail-smtp-in.l.google.com';
574
+ const policyMatch = validateMx(mx, policy);
489
575
 
490
- - Less strict whitespace checks (`mailauth` accepts multiple spaces between tags etc.)
491
- - Some macro tests are skipped (macro expansion is supported _in most parts_)
492
- - Some tests where the invalid component is listed after a matching part (mailauth processes from left to right and returns on the first match found)
493
- - Other than that, all tests pass
576
+ if (policy.mx && !policyMatch.valid) {
577
+ // The MX host is not listed in the policy; do not connect
578
+ }
579
+ ```
494
580
 
495
- ### ARC test suite from ValiMail
581
+ ## Testing
496
582
 
497
- ValiMail [arc_test_suite](https://github.com/ValiMail/arc_test_suite)
583
+ mailauth uses the following test suites:
498
584
 
499
- - `mailauth` is less strict on header tags and casing. For example, uppercase `S=` for a selector passes in `mailauth` but fails in ValiMail.
500
- - Signing test suite is used for input only. All listed messages are signed using provided keys, but signatures are not matched against the reference. Instead, `mailauth` validates the signatures itself and looks for the same cv= output that the ARC-Seal header in the test suite has
501
- - Other than that, all tests pass
585
+ ### SPF Test Suite
502
586
 
503
- ## Setup
587
+ Based on the [OpenSPF test suite](http://www.openspf.org/Test_Suite), with some differences:
504
588
 
505
- First, install the module from npm:
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.
506
593
 
507
- ```
508
- $ npm install mailauth
509
- ```
594
+ ### ARC Test Suite from ValiMail
510
595
 
511
- next import any method you want to use from mailauth package into your script:
596
+ Based on ValiMail's [arc_test_suite](https://github.com/ValiMail/arc_test_suite):
512
597
 
513
- ```js
514
- const { authenticate } = require('mailauth');
515
- ```
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.
516
601
 
517
602
  ## License
518
603
 
519
604
  © 2020-2024 Postal Systems OÜ
520
605
 
521
- Licensed under MIT license
606
+ Licensed under the [MIT License](LICENSE).
@@ -3,7 +3,6 @@
3
3
  const { SimpleHash } = require('./simple');
4
4
  const { RelaxedHash } = require('./relaxed');
5
5
  const { Transform } = require('node:stream');
6
- const { MessageParser } = require('../message-parser');
7
6
 
8
7
  const dkimBody = (canonicalization, ...options) => {
9
8
  canonicalization = (canonicalization || 'simple/simple').toString().split('/').pop().toLowerCase().trim();
@@ -20,50 +19,15 @@ const dkimBody = (canonicalization, ...options) => {
20
19
  }
21
20
  };
22
21
 
23
- class MessageHasher extends MessageParser {
24
- constructor(canonicalization, ...options) {
25
- super();
26
- this.hasher = dkimBody(canonicalization, ...options);
27
- this.bodyHash = null;
28
- }
29
-
30
- async nextChunk(chunk) {
31
- this.hasher.update(chunk);
32
- }
33
-
34
- async finalChunk() {
35
- this.bodyHash = this.hasher.digest('base64');
36
- }
37
- }
38
-
39
22
  class BodyHashStream extends Transform {
40
23
  constructor(canonicalization, ...options) {
41
24
  super();
42
25
 
43
- this.finished = false;
44
- this.finishCb = null;
45
-
46
26
  this.byteLength = 0;
47
27
 
48
- this.messageHasher = new MessageHasher(canonicalization, ...options);
49
- this.bodyHash = null;
50
- this.messageHasher.once('finish', () => this.finishHashing());
51
- this.messageHasher.once('end', () => this.finishHashing());
52
- this.messageHasher.once('error', err => this.destroy(err));
53
- }
54
-
55
- finishHashing() {
56
- if (this.finished || !this.finishCb) {
57
- return;
58
- }
59
- this.finished = true;
60
- let done = this.finishCb;
61
- this.finishCb = null;
62
-
63
- this.bodyHash = this.messageHasher.bodyHash;
64
- this.emit('hash', this.bodyHash);
28
+ this.hasher = dkimBody(canonicalization, ...options);
65
29
 
66
- done();
30
+ this.bodyHash = null;
67
31
  }
68
32
 
69
33
  _transform(chunk, encoding, done) {
@@ -77,19 +41,16 @@ class BodyHashStream extends Transform {
77
41
 
78
42
  this.byteLength += chunk.length;
79
43
 
44
+ this.hasher.update(chunk);
80
45
  this.push(chunk);
81
46
 
82
- if (this.messageHasher.write(chunk) === false) {
83
- // wait for drain
84
- return this.messageHasher.once('drain', done);
85
- }
86
-
87
47
  done();
88
48
  }
89
49
 
90
50
  _flush(done) {
91
- this.finishCb = done;
92
- this.messageHasher.end();
51
+ this.bodyHash = this.hasher.digest('base64');
52
+ this.emit('hash', this.bodyHash);
53
+ done();
93
54
  }
94
55
  }
95
56
 
package/man/mailauth.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH "MAILAUTH" "1" "October 2024" "v4.7.2" "Mailauth Help"
1
+ .TH "MAILAUTH" "1" "November 2024" "v4.8.0" "Mailauth Help"
2
2
  .SH "NAME"
3
3
  \fBmailauth\fR
4
4
  .QP
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailauth",
3
- "version": "4.7.2",
3
+ "version": "4.8.0",
4
4
  "description": "Email authentication library for Node.js",
5
5
  "main": "lib/mailauth.js",
6
6
  "scripts": {
@@ -42,17 +42,17 @@
42
42
  "marked": "0.7.0",
43
43
  "marked-man": "0.7.0",
44
44
  "mbox-reader": "1.2.0",
45
- "mocha": "10.7.3"
45
+ "mocha": "10.8.2"
46
46
  },
47
47
  "dependencies": {
48
- "@postalsys/vmc": "1.0.8",
48
+ "@postalsys/vmc": "1.1.0",
49
49
  "fast-xml-parser": "4.5.0",
50
50
  "ipaddr.js": "2.2.0",
51
51
  "joi": "17.13.3",
52
52
  "libmime": "5.3.5",
53
- "nodemailer": "6.9.15",
53
+ "nodemailer": "6.9.16",
54
54
  "punycode.js": "2.3.1",
55
- "tldts": "6.1.49",
55
+ "tldts": "6.1.58",
56
56
  "undici": "5.28.4",
57
57
  "yargs": "17.7.2"
58
58
  },
@@ -72,10 +72,10 @@
72
72
  "LICENSE.txt"
73
73
  ],
74
74
  "targets": [
75
- "latest-linux-x64",
76
- "latest-macos-x64",
77
- "latest-macos-arm64",
78
- "latest-win-x64"
75
+ "node20-linux-x64",
76
+ "node20-macos-x64",
77
+ "node20-macos-arm64",
78
+ "node20-win-x64"
79
79
  ],
80
80
  "outputPath": "ee-dist"
81
81
  }