mailauth 4.12.0 → 4.13.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 +0 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +20 -0
- package/LICENSE.txt +1 -1
- package/README.md +16 -2
- package/cli.md +2 -1
- package/docs/arc.md +176 -0
- package/docs/bimi.md +272 -0
- package/docs/dkim.md +202 -0
- package/docs/dmarc.md +225 -0
- package/docs/mta-sts.md +239 -0
- package/docs/spf.md +196 -0
- package/lib/bimi/index.js +20 -0
- package/lib/dkim/dkim-verifier.js +17 -1
- package/package.json +8 -8
package/docs/spf.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# SPF Verification Result Reference
|
|
2
|
+
|
|
3
|
+
This document describes the result object returned by the `spf()` function.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `spf` function verifies the SPF (Sender Policy Framework) record for an email sender and returns an object containing the verification result.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const { spf } = require('mailauth/lib/spf');
|
|
11
|
+
|
|
12
|
+
const result = await spf({
|
|
13
|
+
sender: 'user@example.com',
|
|
14
|
+
ip: '192.0.2.1',
|
|
15
|
+
helo: 'mail.example.com',
|
|
16
|
+
mta: 'mx.receiver.com'
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Result Object Fields
|
|
21
|
+
|
|
22
|
+
| Field | Type | Presence | Description |
|
|
23
|
+
|-------|------|----------|-------------|
|
|
24
|
+
| `domain` | `string` | Always | The domain extracted from the sender address for SPF lookup |
|
|
25
|
+
| `client-ip` | `string` | Always | The client IP address that was checked |
|
|
26
|
+
| `helo` | `string` | If provided | The EHLO/HELO hostname from the SMTP session |
|
|
27
|
+
| `envelope-from` | `string` | If provided | The MAIL FROM address |
|
|
28
|
+
| `status` | `object` | Always | Verification status object (see below) |
|
|
29
|
+
| `header` | `string` | Always | Formatted Received-SPF header value |
|
|
30
|
+
| `info` | `string` | Always | Formatted Authentication-Results header value |
|
|
31
|
+
| `rr` | `string` | Record found | Raw SPF DNS TXT record |
|
|
32
|
+
| `lookups` | `object` | Always | DNS lookup statistics (see below) |
|
|
33
|
+
|
|
34
|
+
## status Object
|
|
35
|
+
|
|
36
|
+
| Field | Type | Description |
|
|
37
|
+
|-------|------|-------------|
|
|
38
|
+
| `result` | `string` | SPF result code (see below) |
|
|
39
|
+
| `comment` | `string` | Human-readable explanation of the result |
|
|
40
|
+
| `smtp` | `object` | SMTP session identifiers |
|
|
41
|
+
|
|
42
|
+
### status.smtp Object
|
|
43
|
+
|
|
44
|
+
| Field | Type | Description |
|
|
45
|
+
|-------|------|-------------|
|
|
46
|
+
| `mailfrom` | `string` | The MAIL FROM address |
|
|
47
|
+
| `helo` | `string` | The HELO/EHLO hostname |
|
|
48
|
+
|
|
49
|
+
## lookups Object
|
|
50
|
+
|
|
51
|
+
| Field | Type | Description |
|
|
52
|
+
|-------|------|-------------|
|
|
53
|
+
| `limit` | `number` | Maximum DNS lookups allowed (default: 10) |
|
|
54
|
+
| `count` | `number` | Number of DNS lookups performed |
|
|
55
|
+
| `void` | `number` | Number of void (empty result) DNS lookups |
|
|
56
|
+
| `subqueries` | `object` | Counts of DNS queries by type (e.g., `{A: 2, MX: 1}`) |
|
|
57
|
+
|
|
58
|
+
## Result Values
|
|
59
|
+
|
|
60
|
+
| Result | SPF Qualifier | Description |
|
|
61
|
+
|--------|---------------|-------------|
|
|
62
|
+
| `pass` | `+` | Sender is authorized |
|
|
63
|
+
| `fail` | `-` | Sender is explicitly not authorized |
|
|
64
|
+
| `softfail` | `~` | Sender is probably not authorized (transitional) |
|
|
65
|
+
| `neutral` | `?` | No policy assertion about the sender |
|
|
66
|
+
| `none` | - | No SPF record found or invalid domain |
|
|
67
|
+
| `permerror` | - | Permanent error (invalid SPF record, too many DNS lookups) |
|
|
68
|
+
| `temperror` | - | Temporary error (DNS timeout, server refused) |
|
|
69
|
+
|
|
70
|
+
## Comment Format
|
|
71
|
+
|
|
72
|
+
The `status.comment` field follows this format based on the result:
|
|
73
|
+
|
|
74
|
+
| Result | Comment Format |
|
|
75
|
+
|--------|----------------|
|
|
76
|
+
| `pass` | `"{mta}: domain of {sender} designates {ip} as permitted sender"` |
|
|
77
|
+
| `fail` | `"{mta}: domain of {sender} does not designate {ip} as permitted sender"` |
|
|
78
|
+
| `softfail` | `"{mta}: domain of transitioning {sender} does not designate {ip} as permitted sender"` |
|
|
79
|
+
| `neutral` | `"{mta}: {ip} is neither permitted nor denied by domain of {sender}"` |
|
|
80
|
+
| `none` | `"{mta}: {domain} does not designate permitted sender hosts"` |
|
|
81
|
+
| `permerror` | `"{mta}: permanent error in processing during lookup of {sender}: {text}"` |
|
|
82
|
+
| `temperror` | `"{mta}: error in processing during lookup of {sender}: {text}"` |
|
|
83
|
+
|
|
84
|
+
## Example Output
|
|
85
|
+
|
|
86
|
+
### SPF Pass
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"domain": "example.com",
|
|
91
|
+
"client-ip": "192.0.2.1",
|
|
92
|
+
"helo": "mail.example.com",
|
|
93
|
+
"envelope-from": "user@example.com",
|
|
94
|
+
"status": {
|
|
95
|
+
"result": "pass",
|
|
96
|
+
"comment": "mx.receiver.com: domain of user@example.com designates 192.0.2.1 as permitted sender",
|
|
97
|
+
"smtp": {
|
|
98
|
+
"mailfrom": "user@example.com",
|
|
99
|
+
"helo": "mail.example.com"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"header": "Received-SPF: pass (mx.receiver.com: domain of user@example.com designates 192.0.2.1 as permitted sender) client-ip=192.0.2.1;",
|
|
103
|
+
"info": "spf=pass (mx.receiver.com: domain of user@example.com designates 192.0.2.1 as permitted sender) smtp.mailfrom=user@example.com smtp.helo=mail.example.com",
|
|
104
|
+
"rr": "v=spf1 ip4:192.0.2.0/24 -all",
|
|
105
|
+
"lookups": {
|
|
106
|
+
"limit": 10,
|
|
107
|
+
"count": 1,
|
|
108
|
+
"void": 0,
|
|
109
|
+
"subqueries": {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### SPF Fail
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"domain": "example.com",
|
|
119
|
+
"client-ip": "203.0.113.1",
|
|
120
|
+
"helo": "attacker.example.net",
|
|
121
|
+
"envelope-from": "user@example.com",
|
|
122
|
+
"status": {
|
|
123
|
+
"result": "fail",
|
|
124
|
+
"comment": "mx.receiver.com: domain of user@example.com does not designate 203.0.113.1 as permitted sender",
|
|
125
|
+
"smtp": {
|
|
126
|
+
"mailfrom": "user@example.com",
|
|
127
|
+
"helo": "attacker.example.net"
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"header": "Received-SPF: fail (mx.receiver.com: domain of user@example.com does not designate 203.0.113.1 as permitted sender) client-ip=203.0.113.1;",
|
|
131
|
+
"info": "spf=fail (mx.receiver.com: domain of user@example.com does not designate 203.0.113.1 as permitted sender) smtp.mailfrom=user@example.com smtp.helo=attacker.example.net",
|
|
132
|
+
"rr": "v=spf1 ip4:192.0.2.0/24 -all",
|
|
133
|
+
"lookups": {
|
|
134
|
+
"limit": 10,
|
|
135
|
+
"count": 1,
|
|
136
|
+
"void": 0,
|
|
137
|
+
"subqueries": {}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### SPF None (No Record)
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"domain": "no-spf.example.com",
|
|
147
|
+
"client-ip": "192.0.2.1",
|
|
148
|
+
"helo": "mail.no-spf.example.com",
|
|
149
|
+
"envelope-from": "user@no-spf.example.com",
|
|
150
|
+
"status": {
|
|
151
|
+
"result": "none",
|
|
152
|
+
"comment": "mx.receiver.com: no-spf.example.com does not designate permitted sender hosts",
|
|
153
|
+
"smtp": {
|
|
154
|
+
"mailfrom": "user@no-spf.example.com",
|
|
155
|
+
"helo": "mail.no-spf.example.com"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"header": "Received-SPF: none (mx.receiver.com: no-spf.example.com does not designate permitted sender hosts) client-ip=192.0.2.1;",
|
|
159
|
+
"info": "spf=none (mx.receiver.com: no-spf.example.com does not designate permitted sender hosts) smtp.mailfrom=user@no-spf.example.com smtp.helo=mail.no-spf.example.com",
|
|
160
|
+
"lookups": {
|
|
161
|
+
"limit": 10,
|
|
162
|
+
"count": 1,
|
|
163
|
+
"void": 1,
|
|
164
|
+
"subqueries": {}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### SPF Permerror (Too Many Lookups)
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"domain": "complex.example.com",
|
|
174
|
+
"client-ip": "192.0.2.1",
|
|
175
|
+
"envelope-from": "user@complex.example.com",
|
|
176
|
+
"status": {
|
|
177
|
+
"result": "permerror",
|
|
178
|
+
"comment": "mx.receiver.com: permanent error in processing during lookup of user@complex.example.com: Too many DNS requests",
|
|
179
|
+
"smtp": {
|
|
180
|
+
"mailfrom": "user@complex.example.com"
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"header": "Received-SPF: permerror (mx.receiver.com: permanent error in processing during lookup of user@complex.example.com: Too many DNS requests) client-ip=192.0.2.1;",
|
|
184
|
+
"info": "spf=permerror (mx.receiver.com: permanent error in processing during lookup of user@complex.example.com: Too many DNS requests) smtp.mailfrom=user@complex.example.com",
|
|
185
|
+
"lookups": {
|
|
186
|
+
"limit": 10,
|
|
187
|
+
"count": 11,
|
|
188
|
+
"void": 0,
|
|
189
|
+
"subqueries": {
|
|
190
|
+
"include": 5,
|
|
191
|
+
"a": 3,
|
|
192
|
+
"mx": 2
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
package/lib/bimi/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const crypto = require('node:crypto');
|
|
|
5
5
|
const dns = require('node:dns');
|
|
6
6
|
const { formatAuthHeaderRow, parseDkimHeaders, formatDomain, getAlignment } = require('../tools');
|
|
7
7
|
const Joi = require('joi');
|
|
8
|
+
const libmime = require('libmime');
|
|
8
9
|
//const packageData = require('../../package.json');
|
|
9
10
|
const httpsSchema = Joi.string().uri({
|
|
10
11
|
scheme: ['https']
|
|
@@ -187,6 +188,10 @@ const lookup = async data => {
|
|
|
187
188
|
response.status.policy = { authority: 'none', 'authority-uri': recordData.parsed.a.value }; // VMC has not been actually checked here yet, so authority is none
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
if (recordData?.parsed?.p?.value) {
|
|
192
|
+
response.preference = recordData.parsed.p.value;
|
|
193
|
+
}
|
|
194
|
+
|
|
190
195
|
response.info = formatAuthHeaderRow('bimi', response.status);
|
|
191
196
|
return response;
|
|
192
197
|
};
|
|
@@ -396,6 +401,21 @@ const validateVMC = async (bimiData, opts) => {
|
|
|
396
401
|
}
|
|
397
402
|
}
|
|
398
403
|
|
|
404
|
+
// Generate headers when validation succeeds
|
|
405
|
+
let canGenerateHeaders =
|
|
406
|
+
result.location?.success && result.location?.logoFile && (!result.authority || (result.authority.success && result.authority.hashMatch !== false));
|
|
407
|
+
|
|
408
|
+
if (canGenerateHeaders) {
|
|
409
|
+
result.headers = {
|
|
410
|
+
indicator: libmime.foldLines(`BIMI-Indicator: ${result.location.logoFile}`, 160),
|
|
411
|
+
location: `BIMI-Location: v=BIMI1; l=${bimiData.location}`
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
if (bimiData.preference) {
|
|
415
|
+
result.headers.preference = `BIMI-Logo-Preference: ${bimiData.preference}`;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
399
419
|
return result;
|
|
400
420
|
};
|
|
401
421
|
|
|
@@ -174,6 +174,19 @@ class DkimVerifier extends MessageParser {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
getSignatureTimeValid(signatureHeader) {
|
|
178
|
+
// Signature claims to be from the future
|
|
179
|
+
if (signatureHeader.timestamp && signatureHeader.timestamp > this.curTime) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
// Signature has expired
|
|
183
|
+
if (signatureHeader.expiration && signatureHeader.expiration < this.curTime) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
// Within validity window or no constraints (vacuously true)
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
177
190
|
async finalChunk() {
|
|
178
191
|
try {
|
|
179
192
|
if (!this.headers || !this.bodyHashes.size) {
|
|
@@ -341,7 +354,10 @@ class DkimVerifier extends MessageParser {
|
|
|
341
354
|
bodyHash,
|
|
342
355
|
bodyHashExpecting: signatureHeader.parsed?.bh?.value,
|
|
343
356
|
signingHeaders,
|
|
344
|
-
status
|
|
357
|
+
status,
|
|
358
|
+
signTime: signatureHeader.timestamp ? signatureHeader.timestamp.toISOString() : null,
|
|
359
|
+
expiresAfter: signatureHeader.expiration ? signatureHeader.expiration.toISOString() : null,
|
|
360
|
+
signatureTimeValid: this.getSignatureTimeValid(signatureHeader)
|
|
345
361
|
};
|
|
346
362
|
|
|
347
363
|
if (typeof signatureHeader.sourceBodyLength === 'number') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailauth",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.13.0",
|
|
4
4
|
"description": "Email authentication library for Node.js",
|
|
5
5
|
"main": "lib/mailauth.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -42,23 +42,23 @@
|
|
|
42
42
|
"license-report": "6.8.1",
|
|
43
43
|
"mbox-reader": "1.2.0",
|
|
44
44
|
"mocha": "11.7.5",
|
|
45
|
-
"prettier": "3.
|
|
45
|
+
"prettier": "3.8.1",
|
|
46
46
|
"resedit": "3.0.1"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@postalsys/vmc": "1.1.
|
|
50
|
-
"fast-xml-parser": "
|
|
49
|
+
"@postalsys/vmc": "1.1.3",
|
|
50
|
+
"fast-xml-parser": "5.3.4",
|
|
51
51
|
"ipaddr.js": "2.3.0",
|
|
52
52
|
"joi": "18.0.2",
|
|
53
53
|
"libmime": "5.3.7",
|
|
54
|
-
"nodemailer": "7.0.
|
|
54
|
+
"nodemailer": "7.0.13",
|
|
55
55
|
"punycode.js": "2.3.1",
|
|
56
|
-
"tldts": "7.0.
|
|
57
|
-
"undici": "7.
|
|
56
|
+
"tldts": "7.0.21",
|
|
57
|
+
"undici": "7.20.0",
|
|
58
58
|
"yargs": "17.7.2"
|
|
59
59
|
},
|
|
60
60
|
"engines": {
|
|
61
|
-
"node": ">=18.
|
|
61
|
+
"node": ">=20.18.1"
|
|
62
62
|
},
|
|
63
63
|
"bin": {
|
|
64
64
|
"mailauth": "bin/mailauth.js"
|