mailauth 4.7.3 → 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 +7 -0
- package/README.md +409 -324
- package/man/mailauth.1 +1 -1
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## [4.7.3](https://github.com/postalsys/mailauth/compare/v4.7.2...v4.7.3) (2024-10-21)
|
|
4
11
|
|
|
5
12
|
|
package/README.md
CHANGED
|
@@ -1,84 +1,127 @@
|
|
|
1
|
-
|
|
1
|
+
# mailauth: Email Authentication for Node.js
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+

|
|
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
|
-
-
|
|
11
|
-
- Sealing
|
|
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**
|
|
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
|
-
|
|
51
|
+
Then, import the desired methods into your script:
|
|
17
52
|
|
|
18
|
-
|
|
53
|
+
```javascript
|
|
54
|
+
const { authenticate } = require('mailauth');
|
|
55
|
+
```
|
|
19
56
|
|
|
20
|
-
|
|
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
|
-
|
|
63
|
+
### Authentication
|
|
64
|
+
|
|
65
|
+
Use the `authenticate` function to validate DKIM signatures, SPF, DMARC, ARC, and BIMI for an email.
|
|
25
66
|
|
|
26
|
-
|
|
67
|
+
#### Syntax
|
|
27
68
|
|
|
28
|
-
```
|
|
29
|
-
await authenticate(message [,options])
|
|
30
|
-
|
|
69
|
+
```javascript
|
|
70
|
+
await authenticate(message [, options])
|
|
71
|
+
// Returns: { dkim, spf, arc, dmarc, bimi, receivedChain, headers }
|
|
31
72
|
```
|
|
32
73
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- **message
|
|
36
|
-
- **options** (
|
|
37
|
-
- **sender** (
|
|
38
|
-
- **ip** (
|
|
39
|
-
- **helo** (
|
|
40
|
-
- **trustReceived** (
|
|
41
|
-
- **mta** (
|
|
42
|
-
- **minBitLength** (
|
|
43
|
-
- **disableArc** (
|
|
44
|
-
- **disableDmarc** (
|
|
45
|
-
- **disableBimi** (
|
|
46
|
-
- **seal** (
|
|
47
|
-
- **signingDomain** (
|
|
48
|
-
- **selector** (
|
|
49
|
-
- **privateKey** (
|
|
50
|
-
- **resolver** (
|
|
51
|
-
- **maxResolveCount** (
|
|
52
|
-
- **maxVoidCount** (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
143
|
+
#### DKIM Signing
|
|
97
144
|
|
|
98
|
-
`
|
|
145
|
+
Use the `dkimSign` function to sign an email message with DKIM.
|
|
99
146
|
|
|
100
|
-
|
|
147
|
+
##### Syntax
|
|
101
148
|
|
|
102
|
-
|
|
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
|
-
|
|
171
|
+
##### Example
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
105
174
|
const { dkimSign } = require('mailauth/lib/dkim/sign');
|
|
106
|
-
const
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
194
|
+
console.error('Signing errors:', signResult.errors);
|
|
146
195
|
}
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
+
#### DKIM Signing as a Stream
|
|
161
211
|
|
|
162
|
-
Use `DkimSignStream`
|
|
212
|
+
Use `DkimSignStream` to sign messages as part of a stream processing pipeline.
|
|
163
213
|
|
|
164
|
-
|
|
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
|
-
|
|
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',
|
|
184
|
-
selector: 'test.rsa',
|
|
185
|
-
|
|
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
|
-
//
|
|
233
|
+
// Read from stdin, write signed message to stdout
|
|
203
234
|
process.stdin.pipe(dkimSignStream).pipe(process.stdout);
|
|
204
235
|
```
|
|
205
236
|
|
|
206
|
-
|
|
237
|
+
#### DKIM Verification
|
|
238
|
+
|
|
239
|
+
Use the `dkimVerify` function to verify DKIM signatures in an email message.
|
|
207
240
|
|
|
208
|
-
|
|
241
|
+
##### Syntax
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
209
244
|
const { dkimVerify } = require('mailauth/lib/dkim/verify');
|
|
210
|
-
|
|
245
|
+
|
|
211
246
|
const result = await dkimVerify(message);
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
264
|
+
**Sample Output:**
|
|
218
265
|
|
|
219
|
-
```
|
|
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
|
-
|
|
272
|
+
### SPF
|
|
273
|
+
|
|
274
|
+
#### SPF Verification
|
|
226
275
|
|
|
227
|
-
|
|
276
|
+
Use the `spf` function to verify the SPF record for an email sender.
|
|
228
277
|
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
310
|
+
**Sample Output:**
|
|
242
311
|
|
|
243
|
-
```
|
|
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
|
-
|
|
318
|
+
### ARC
|
|
250
319
|
|
|
251
|
-
|
|
320
|
+
#### ARC Validation
|
|
252
321
|
|
|
253
|
-
ARC seals are automatically
|
|
322
|
+
ARC seals are validated automatically during the authentication step.
|
|
254
323
|
|
|
255
|
-
|
|
324
|
+
##### Example
|
|
325
|
+
|
|
326
|
+
```javascript
|
|
256
327
|
const { authenticate } = require('mailauth');
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
338
|
+
**Sample Output:**
|
|
267
339
|
|
|
268
|
-
```
|
|
340
|
+
```json
|
|
269
341
|
{
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
351
|
+
#### ARC Sealing
|
|
352
|
+
|
|
353
|
+
You can seal messages with ARC either during authentication or after modifications.
|
|
280
354
|
|
|
281
|
-
|
|
355
|
+
##### Sealing During Authentication
|
|
282
356
|
|
|
283
|
-
|
|
357
|
+
Provide the sealing key in the options to seal messages automatically during authentication.
|
|
284
358
|
|
|
285
|
-
```
|
|
359
|
+
```javascript
|
|
286
360
|
const { authenticate } = require('mailauth');
|
|
287
|
-
const
|
|
288
|
-
message, // either a String, a Buffer or a Readable Stream
|
|
289
|
-
{
|
|
290
|
-
trustReceived: true,
|
|
361
|
+
const fs = require('fs');
|
|
291
362
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
379
|
+
##### Sealing After Modifications
|
|
307
380
|
|
|
308
|
-
If you
|
|
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
|
-
```
|
|
383
|
+
```javascript
|
|
311
384
|
const { authenticate, sealMessage } = require('mailauth');
|
|
385
|
+
const fs = require('fs');
|
|
312
386
|
|
|
313
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
327
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
//
|
|
338
|
-
process.stdout.write(sealHeaders); // ARC
|
|
339
|
-
process.stdout.write(headers);
|
|
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
|
-
|
|
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
|
|
419
|
+
#### DMARC Helpers
|
|
346
420
|
|
|
347
|
-
|
|
421
|
+
##### `getDmarcRecord(domain [, resolver])`
|
|
348
422
|
|
|
349
|
-
|
|
423
|
+
Fetches and parses the DMARC DNS record for a domain or subdomain. Returns `false` if no record exists.
|
|
350
424
|
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
463
|
+
### BIMI
|
|
375
464
|
|
|
376
|
-
|
|
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
|
-
|
|
467
|
+
#### Example
|
|
379
468
|
|
|
380
|
-
|
|
469
|
+
```javascript
|
|
470
|
+
const { authenticate } = require('mailauth');
|
|
381
471
|
|
|
382
|
-
|
|
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
|
-
|
|
483
|
+
console.log(`BIMI location: ${bimi.location}`);
|
|
398
484
|
}
|
|
399
485
|
```
|
|
400
486
|
|
|
401
|
-
|
|
487
|
+
**Note:**
|
|
402
488
|
|
|
403
|
-
|
|
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
|
-
|
|
492
|
+
#### Verified Mark Certificate (VMC)
|
|
406
493
|
|
|
407
|
-
|
|
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
|
-
|
|
496
|
+
**Example Authority Evidence Documents:**
|
|
410
497
|
|
|
411
|
-
- [
|
|
412
|
-
- [
|
|
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
|
-
|
|
501
|
+
### MTA-STS
|
|
415
502
|
|
|
416
|
-
|
|
503
|
+
mailauth provides functions to fetch and validate MTA-STS policies for a domain.
|
|
417
504
|
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
532
|
+
// Update your cache with the new policy
|
|
429
533
|
}
|
|
430
534
|
|
|
431
535
|
if (policy.mode === 'enforce') {
|
|
432
|
-
|
|
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
|
-
|
|
540
|
+
**Possible Status Values:**
|
|
441
541
|
|
|
442
|
-
|
|
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
|
-
|
|
550
|
+
Use the `validateMx` function to check if an MX hostname is valid according to the MTA-STS policy.
|
|
449
551
|
|
|
450
|
-
|
|
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
|
-
|
|
554
|
+
```javascript
|
|
555
|
+
const { validateMx } = require('mailauth/lib/mta-sts');
|
|
454
556
|
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
561
|
+
##### Parameters
|
|
476
562
|
|
|
477
|
-
- **mx**
|
|
478
|
-
- **policy**
|
|
563
|
+
- **mx** (`string`): The resolved MX hostname.
|
|
564
|
+
- **policy** (`object`): The MTA-STS policy object.
|
|
479
565
|
|
|
480
|
-
|
|
566
|
+
##### Example
|
|
481
567
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
`mailauth` uses the following test suites:
|
|
568
|
+
```javascript
|
|
569
|
+
const { getPolicy, validateMx } = require('mailauth/lib/mta-sts');
|
|
485
570
|
|
|
486
|
-
|
|
571
|
+
const { policy } = await getPolicy('gmail.com');
|
|
487
572
|
|
|
488
|
-
|
|
573
|
+
const mx = 'alt4.gmail-smtp-in.l.google.com';
|
|
574
|
+
const policyMatch = validateMx(mx, policy);
|
|
489
575
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
576
|
+
if (policy.mx && !policyMatch.valid) {
|
|
577
|
+
// The MX host is not listed in the policy; do not connect
|
|
578
|
+
}
|
|
579
|
+
```
|
|
494
580
|
|
|
495
|
-
|
|
581
|
+
## Testing
|
|
496
582
|
|
|
497
|
-
|
|
583
|
+
mailauth uses the following test suites:
|
|
498
584
|
|
|
499
|
-
|
|
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
|
-
|
|
587
|
+
Based on the [OpenSPF test suite](http://www.openspf.org/Test_Suite), with some differences:
|
|
504
588
|
|
|
505
|
-
|
|
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
|
-
|
|
596
|
+
Based on ValiMail's [arc_test_suite](https://github.com/ValiMail/arc_test_suite):
|
|
512
597
|
|
|
513
|
-
|
|
514
|
-
|
|
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
|
|
606
|
+
Licensed under the [MIT License](LICENSE).
|
package/man/mailauth.1
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailauth",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
45
|
+
"mocha": "10.8.2"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@postalsys/vmc": "1.0
|
|
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.
|
|
53
|
+
"nodemailer": "6.9.16",
|
|
54
54
|
"punycode.js": "2.3.1",
|
|
55
|
-
"tldts": "6.1.
|
|
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
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
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
|
}
|