mailauth 4.12.1 → 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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +13 -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 +4 -4
package/docs/dkim.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# DKIM Verification Result Reference
|
|
2
|
+
|
|
3
|
+
This document describes the result object returned by `dkimVerify()`.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `dkimVerify` function verifies all DKIM-Signature headers in an email message and returns an object containing verification results for each signature.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const { dkimVerify } = require('mailauth/lib/dkim/verify');
|
|
11
|
+
|
|
12
|
+
const result = await dkimVerify(message);
|
|
13
|
+
// result.results is an array of signature verification results
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Top-Level Result Object
|
|
17
|
+
|
|
18
|
+
| Field | Type | Description |
|
|
19
|
+
|-------|------|-------------|
|
|
20
|
+
| `headerFrom` | `string[]` | Array of email addresses from the From header |
|
|
21
|
+
| `envelopeFrom` | `string\|false` | Email address from Return-Path header or sender option |
|
|
22
|
+
| `results` | `object[]` | Array of verification results for each DKIM signature |
|
|
23
|
+
|
|
24
|
+
## Signature Result Object
|
|
25
|
+
|
|
26
|
+
Each entry in the `results` array has the following structure:
|
|
27
|
+
|
|
28
|
+
| Field | Type | Presence | Description |
|
|
29
|
+
|-------|------|----------|-------------|
|
|
30
|
+
| `id` | `string` | Always | SHA256 hash of signature value, or UUID if no signature |
|
|
31
|
+
| `signingDomain` | `string` | Signature exists | Domain from `d=` tag |
|
|
32
|
+
| `selector` | `string` | Signature exists | DKIM selector from `s=` tag |
|
|
33
|
+
| `signature` | `string` | Signature exists | Base64-encoded signature value from `b=` tag |
|
|
34
|
+
| `algo` | `string` | Signature exists | Signing algorithm (e.g., `"rsa-sha256"`, `"ed25519-sha256"`) |
|
|
35
|
+
| `format` | `string` | Signature exists | Canonicalization format from `c=` tag (e.g., `"relaxed/relaxed"`) |
|
|
36
|
+
| `bodyHash` | `string` | Signature exists | Calculated body hash (base64) |
|
|
37
|
+
| `bodyHashExpecting` | `string` | Signature exists | Expected body hash from `bh=` tag |
|
|
38
|
+
| `signingHeaders` | `object` | Signature exists | Signing header details (see below) |
|
|
39
|
+
| `status` | `object` | Always | Verification status (see below) |
|
|
40
|
+
| `signTime` | `string\|null` | Always | ISO 8601 timestamp from `t=` tag, or null |
|
|
41
|
+
| `expiresAfter` | `string\|null` | Always | ISO 8601 expiration from `x=` tag, or null |
|
|
42
|
+
| `signatureTimeValid` | `boolean` | Always | Whether signature is within validity window |
|
|
43
|
+
| `sourceBodyLength` | `number` | Body processed | Original body length in bytes |
|
|
44
|
+
| `canonBodyLength` | `number` | Body processed | Canonicalized bytes actually hashed |
|
|
45
|
+
| `canonBodyLengthTotal` | `number` | Body processed | Total canonicalized body length |
|
|
46
|
+
| `canonBodyLengthLimited` | `boolean` | Signature exists | Whether body length is limited by `l=` tag |
|
|
47
|
+
| `canonBodyLengthLimit` | `number` | `l=` tag present | Maximum body length from `l=` tag |
|
|
48
|
+
| `mimeStructureStart` | `number` | MIME detected | Position where MIME boundary structure starts |
|
|
49
|
+
| `publicKey` | `string` | Key retrieved | PEM-formatted public key |
|
|
50
|
+
| `modulusLength` | `number` | RSA key | RSA key length in bits |
|
|
51
|
+
| `rr` | `string` | DNS lookup done | Raw DNS TXT record value |
|
|
52
|
+
| `info` | `string` | Always | Formatted Authentication-Results header value |
|
|
53
|
+
|
|
54
|
+
### signingHeaders Object
|
|
55
|
+
|
|
56
|
+
| Field | Type | Description |
|
|
57
|
+
|-------|------|-------------|
|
|
58
|
+
| `keys` | `string[]` | List of header field names that were signed |
|
|
59
|
+
| `headers` | `string[]` | Raw header lines that were signed |
|
|
60
|
+
| `canonicalizedHeader` | `string` | Base64-encoded canonicalized header data |
|
|
61
|
+
|
|
62
|
+
### status Object
|
|
63
|
+
|
|
64
|
+
| Field | Type | Presence | Description |
|
|
65
|
+
|-------|------|----------|-------------|
|
|
66
|
+
| `result` | `string` | Always | Verification result code (see below) |
|
|
67
|
+
| `comment` | `string` | On error/info | Human-readable explanation |
|
|
68
|
+
| `aligned` | `string\|false` | DKIM signatures | DMARC-aligned domain, or false |
|
|
69
|
+
| `header` | `object` | Always | Signature header info |
|
|
70
|
+
| `policy` | `object` | Policy result | Policy violation details |
|
|
71
|
+
| `underSized` | `number` | Body limited | Number of unsigned bytes |
|
|
72
|
+
|
|
73
|
+
#### status.header Object
|
|
74
|
+
|
|
75
|
+
| Field | Type | Description |
|
|
76
|
+
|-------|------|-------------|
|
|
77
|
+
| `i` | `string\|false` | Signing domain with @ prefix (e.g., `"@example.com"`) |
|
|
78
|
+
| `s` | `string` | DKIM selector |
|
|
79
|
+
| `a` | `string` | Algorithm |
|
|
80
|
+
| `b` | `string` | First 8 characters of signature value |
|
|
81
|
+
|
|
82
|
+
## Result Values
|
|
83
|
+
|
|
84
|
+
| Result | Description |
|
|
85
|
+
|--------|-------------|
|
|
86
|
+
| `pass` | Signature verified successfully |
|
|
87
|
+
| `fail` | Signature verification failed (bad signature) |
|
|
88
|
+
| `neutral` | Signature could not be verified (missing key, expired, body hash mismatch) |
|
|
89
|
+
| `policy` | Signature failed policy check (e.g., weak key) |
|
|
90
|
+
| `temperror` | Temporary error (DNS failure) |
|
|
91
|
+
| `none` | Message not signed |
|
|
92
|
+
|
|
93
|
+
## Comment Values
|
|
94
|
+
|
|
95
|
+
Common values for `status.comment`:
|
|
96
|
+
|
|
97
|
+
| Comment | Description |
|
|
98
|
+
|---------|-------------|
|
|
99
|
+
| `"body hash did not verify"` | Calculated body hash does not match `bh=` tag |
|
|
100
|
+
| `"bad signature"` | Cryptographic signature verification failed |
|
|
101
|
+
| `"invalid expiration"` | Expiration timestamp is before signing timestamp |
|
|
102
|
+
| `"expired"` | Signature has expired (past `x=` timestamp) |
|
|
103
|
+
| `"no key"` | No DKIM key found in DNS |
|
|
104
|
+
| `"unknown key version"` | Unsupported key version in DNS record |
|
|
105
|
+
| `"unknown key type"` | Unsupported key type in DNS record |
|
|
106
|
+
| `"invalid public key"` | Public key in DNS record is malformed |
|
|
107
|
+
| `"DNS failure: {code}"` | DNS lookup failed with error code |
|
|
108
|
+
| `"message not signed"` | No DKIM-Signature headers found |
|
|
109
|
+
|
|
110
|
+
## Example Output
|
|
111
|
+
|
|
112
|
+
### Successful Verification
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"headerFrom": ["sender@example.com"],
|
|
117
|
+
"envelopeFrom": "sender@example.com",
|
|
118
|
+
"results": [
|
|
119
|
+
{
|
|
120
|
+
"id": "a1b2c3d4e5f6...",
|
|
121
|
+
"signingDomain": "example.com",
|
|
122
|
+
"selector": "selector1",
|
|
123
|
+
"signature": "dGhpcyBpcyBhIHNpZ25hdHVyZQ==",
|
|
124
|
+
"algo": "rsa-sha256",
|
|
125
|
+
"format": "relaxed/relaxed",
|
|
126
|
+
"bodyHash": "YWJjZGVmZ2hpamtsbW5vcA==",
|
|
127
|
+
"bodyHashExpecting": "YWJjZGVmZ2hpamtsbW5vcA==",
|
|
128
|
+
"signingHeaders": {
|
|
129
|
+
"keys": ["from", "to", "subject", "date"],
|
|
130
|
+
"headers": ["From: sender@example.com", "..."],
|
|
131
|
+
"canonicalizedHeader": "..."
|
|
132
|
+
},
|
|
133
|
+
"status": {
|
|
134
|
+
"result": "pass",
|
|
135
|
+
"aligned": "example.com",
|
|
136
|
+
"header": {
|
|
137
|
+
"i": "@example.com",
|
|
138
|
+
"s": "selector1",
|
|
139
|
+
"a": "rsa-sha256",
|
|
140
|
+
"b": "dGhpcyBp"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"signTime": "2024-01-15T10:30:00.000Z",
|
|
144
|
+
"expiresAfter": null,
|
|
145
|
+
"signatureTimeValid": true,
|
|
146
|
+
"sourceBodyLength": 1024,
|
|
147
|
+
"canonBodyLength": 1020,
|
|
148
|
+
"canonBodyLengthTotal": 1020,
|
|
149
|
+
"canonBodyLengthLimited": false,
|
|
150
|
+
"publicKey": "-----BEGIN PUBLIC KEY-----\n...",
|
|
151
|
+
"modulusLength": 2048,
|
|
152
|
+
"rr": "v=DKIM1; k=rsa; p=...",
|
|
153
|
+
"info": "dkim=pass header.i=@example.com header.s=selector1 header.a=rsa-sha256 header.b=\"dGhpcyBp\""
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Failed Verification
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"headerFrom": ["sender@example.com"],
|
|
164
|
+
"envelopeFrom": "sender@example.com",
|
|
165
|
+
"results": [
|
|
166
|
+
{
|
|
167
|
+
"id": "a1b2c3d4e5f6...",
|
|
168
|
+
"signingDomain": "example.com",
|
|
169
|
+
"selector": "selector1",
|
|
170
|
+
"status": {
|
|
171
|
+
"result": "neutral",
|
|
172
|
+
"comment": "no key",
|
|
173
|
+
"header": {
|
|
174
|
+
"i": "@example.com",
|
|
175
|
+
"s": "selector1",
|
|
176
|
+
"a": "rsa-sha256",
|
|
177
|
+
"b": "dGhpcyBp"
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
"info": "dkim=neutral (no key) header.i=@example.com header.s=selector1 header.a=rsa-sha256 header.b=\"dGhpcyBp\""
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Unsigned Message
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"headerFrom": ["sender@example.com"],
|
|
191
|
+
"envelopeFrom": "sender@example.com",
|
|
192
|
+
"results": [
|
|
193
|
+
{
|
|
194
|
+
"status": {
|
|
195
|
+
"result": "none",
|
|
196
|
+
"comment": "message not signed"
|
|
197
|
+
},
|
|
198
|
+
"info": "dkim=none (message not signed)"
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
```
|
package/docs/dmarc.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# DMARC Verification Result Reference
|
|
2
|
+
|
|
3
|
+
This document describes the result object returned by DMARC verification.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
DMARC (Domain-based Message Authentication, Reporting, and Conformance) is verified during the authentication process. The DMARC result depends on both SPF and DKIM verification results.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const { authenticate } = require('mailauth');
|
|
11
|
+
|
|
12
|
+
const { dmarc } = await authenticate(message, {
|
|
13
|
+
ip: '192.0.2.1',
|
|
14
|
+
helo: 'mail.example.com',
|
|
15
|
+
sender: 'user@example.com'
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Result Object Fields
|
|
20
|
+
|
|
21
|
+
| Field | Type | Presence | Description |
|
|
22
|
+
|-------|------|----------|-------------|
|
|
23
|
+
| `status` | `object` | Always | Verification status object (see below) |
|
|
24
|
+
| `domain` | `string` | Always | Organizational domain used for DMARC lookup |
|
|
25
|
+
| `policy` | `string` | Record found | Effective policy (`"reject"`, `"quarantine"`, or `"none"`) |
|
|
26
|
+
| `p` | `string` | Record found | Policy from `p=` tag |
|
|
27
|
+
| `sp` | `string` | Record found | Subdomain policy from `sp=` tag (defaults to `p` value) |
|
|
28
|
+
| `pct` | `number` | Record found | Percentage of messages to apply policy (0-100) |
|
|
29
|
+
| `rr` | `string` | Record found | Raw DMARC DNS TXT record |
|
|
30
|
+
| `alignment` | `object` | Record found | SPF and DKIM alignment details (see below) |
|
|
31
|
+
| `error` | `string` | On temperror | Error message |
|
|
32
|
+
| `info` | `string` | Always | Formatted Authentication-Results header value |
|
|
33
|
+
|
|
34
|
+
## status Object
|
|
35
|
+
|
|
36
|
+
| Field | Type | Description |
|
|
37
|
+
|-------|------|-------------|
|
|
38
|
+
| `result` | `string` | DMARC result code (see below) |
|
|
39
|
+
| `header` | `object` | Header information |
|
|
40
|
+
| `comment` | `string` | Policy and ARC information |
|
|
41
|
+
|
|
42
|
+
### status.header Object
|
|
43
|
+
|
|
44
|
+
| Field | Type | Description |
|
|
45
|
+
|-------|------|-------------|
|
|
46
|
+
| `from` | `string` | Organizational domain from the From header |
|
|
47
|
+
| `d` | `string` | Domain where DMARC record was found |
|
|
48
|
+
|
|
49
|
+
## alignment Object
|
|
50
|
+
|
|
51
|
+
| Field | Type | Description |
|
|
52
|
+
|-------|------|-------------|
|
|
53
|
+
| `spf` | `object` | SPF alignment details |
|
|
54
|
+
| `dkim` | `object` | DKIM alignment details |
|
|
55
|
+
|
|
56
|
+
### alignment.spf Object
|
|
57
|
+
|
|
58
|
+
| Field | Type | Description |
|
|
59
|
+
|-------|------|-------------|
|
|
60
|
+
| `result` | `string\|false` | Aligned domain if SPF passed and aligned, otherwise `false` |
|
|
61
|
+
| `strict` | `boolean` | Whether strict alignment is required (`aspf=s`) |
|
|
62
|
+
|
|
63
|
+
### alignment.dkim Object
|
|
64
|
+
|
|
65
|
+
| Field | Type | Description |
|
|
66
|
+
|-------|------|-------------|
|
|
67
|
+
| `result` | `string\|false` | Aligned domain if DKIM passed and aligned, otherwise `false` |
|
|
68
|
+
| `strict` | `boolean` | Whether strict alignment is required (`adkim=s`) |
|
|
69
|
+
| `underSized` | `number` | Number of unsigned body bytes (if `l=` tag limited body) |
|
|
70
|
+
|
|
71
|
+
## Result Values
|
|
72
|
+
|
|
73
|
+
| Result | Description |
|
|
74
|
+
|--------|-------------|
|
|
75
|
+
| `pass` | Message passed DMARC (SPF or DKIM aligned and passed) |
|
|
76
|
+
| `fail` | Message failed DMARC (neither SPF nor DKIM aligned) |
|
|
77
|
+
| `none` | No DMARC record found |
|
|
78
|
+
| `temperror` | Temporary error during DNS lookup |
|
|
79
|
+
|
|
80
|
+
## Policy Values
|
|
81
|
+
|
|
82
|
+
| Policy | Description |
|
|
83
|
+
|--------|-------------|
|
|
84
|
+
| `none` | No specific action requested (monitor mode) |
|
|
85
|
+
| `quarantine` | Suspicious messages should be quarantined |
|
|
86
|
+
| `reject` | Failed messages should be rejected |
|
|
87
|
+
|
|
88
|
+
## Comment Format
|
|
89
|
+
|
|
90
|
+
The `status.comment` field contains policy information in the format:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
p=POLICY sp=SUBDOMAIN_POLICY arc=ARC_RESULT
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For example: `"p=REJECT sp=REJECT arc=pass"`
|
|
97
|
+
|
|
98
|
+
## Example Output
|
|
99
|
+
|
|
100
|
+
### DMARC Pass
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"status": {
|
|
105
|
+
"result": "pass",
|
|
106
|
+
"header": {
|
|
107
|
+
"from": "example.com",
|
|
108
|
+
"d": "example.com"
|
|
109
|
+
},
|
|
110
|
+
"comment": "p=REJECT arc=none"
|
|
111
|
+
},
|
|
112
|
+
"domain": "example.com",
|
|
113
|
+
"policy": "reject",
|
|
114
|
+
"p": "reject",
|
|
115
|
+
"sp": "reject",
|
|
116
|
+
"pct": 100,
|
|
117
|
+
"rr": "v=DMARC1; p=reject; rua=mailto:dmarc@example.com",
|
|
118
|
+
"alignment": {
|
|
119
|
+
"spf": {
|
|
120
|
+
"result": false,
|
|
121
|
+
"strict": false
|
|
122
|
+
},
|
|
123
|
+
"dkim": {
|
|
124
|
+
"result": "example.com",
|
|
125
|
+
"strict": false
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"info": "dmarc=pass (p=REJECT arc=none) header.from=example.com"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### DMARC Fail
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"status": {
|
|
137
|
+
"result": "fail",
|
|
138
|
+
"header": {
|
|
139
|
+
"from": "example.com",
|
|
140
|
+
"d": "example.com"
|
|
141
|
+
},
|
|
142
|
+
"comment": "p=REJECT"
|
|
143
|
+
},
|
|
144
|
+
"domain": "example.com",
|
|
145
|
+
"policy": "reject",
|
|
146
|
+
"p": "reject",
|
|
147
|
+
"sp": "reject",
|
|
148
|
+
"pct": 100,
|
|
149
|
+
"rr": "v=DMARC1; p=reject; rua=mailto:dmarc@example.com",
|
|
150
|
+
"alignment": {
|
|
151
|
+
"spf": {
|
|
152
|
+
"result": false,
|
|
153
|
+
"strict": false
|
|
154
|
+
},
|
|
155
|
+
"dkim": {
|
|
156
|
+
"result": false,
|
|
157
|
+
"strict": false
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"info": "dmarc=fail (p=REJECT) header.from=example.com"
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### DMARC None (No Record)
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"status": {
|
|
169
|
+
"result": "none",
|
|
170
|
+
"header": {
|
|
171
|
+
"from": "no-dmarc.example.com"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"domain": "no-dmarc.example.com",
|
|
175
|
+
"info": "dmarc=none header.from=no-dmarc.example.com"
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### DMARC with Subdomain Policy
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"status": {
|
|
184
|
+
"result": "pass",
|
|
185
|
+
"header": {
|
|
186
|
+
"from": "example.com",
|
|
187
|
+
"d": "sub.example.com"
|
|
188
|
+
},
|
|
189
|
+
"comment": "p=NONE sp=QUARANTINE"
|
|
190
|
+
},
|
|
191
|
+
"domain": "example.com",
|
|
192
|
+
"policy": "quarantine",
|
|
193
|
+
"p": "none",
|
|
194
|
+
"sp": "quarantine",
|
|
195
|
+
"pct": 100,
|
|
196
|
+
"rr": "v=DMARC1; p=none; sp=quarantine; rua=mailto:dmarc@example.com",
|
|
197
|
+
"alignment": {
|
|
198
|
+
"spf": {
|
|
199
|
+
"result": "example.com",
|
|
200
|
+
"strict": false
|
|
201
|
+
},
|
|
202
|
+
"dkim": {
|
|
203
|
+
"result": false,
|
|
204
|
+
"strict": false
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
"info": "dmarc=pass (p=NONE sp=QUARANTINE) header.from=example.com"
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### DMARC Temperror
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
{
|
|
215
|
+
"status": {
|
|
216
|
+
"result": "temperror",
|
|
217
|
+
"header": {
|
|
218
|
+
"from": "example.com"
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
"domain": "example.com",
|
|
222
|
+
"error": "DNS timeout",
|
|
223
|
+
"info": "dmarc=temperror header.from=example.com"
|
|
224
|
+
}
|
|
225
|
+
```
|
package/docs/mta-sts.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# MTA-STS Result Reference
|
|
2
|
+
|
|
3
|
+
This document describes the result objects returned by MTA-STS (Mail Transfer Agent Strict Transport Security) functions.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
MTA-STS allows domain owners to declare that their mail servers support TLS and specify policies for message delivery. mailauth provides functions to fetch, parse, and validate MTA-STS policies.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const { getPolicy, validateMx } = require('mailauth/lib/mta-sts');
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## getPolicy Result
|
|
14
|
+
|
|
15
|
+
The `getPolicy` function fetches and returns the MTA-STS policy for a domain.
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
const { policy, status } = await getPolicy('example.com', knownPolicy);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Result Object Fields
|
|
22
|
+
|
|
23
|
+
| Field | Type | Description |
|
|
24
|
+
|-------|------|-------------|
|
|
25
|
+
| `policy` | `object` | The MTA-STS policy object (see below) |
|
|
26
|
+
| `status` | `string` | Policy retrieval status (see below) |
|
|
27
|
+
|
|
28
|
+
### Status Values
|
|
29
|
+
|
|
30
|
+
| Status | Description |
|
|
31
|
+
|--------|-------------|
|
|
32
|
+
| `found` | New or updated policy was fetched successfully |
|
|
33
|
+
| `not_found` | No MTA-STS policy exists for the domain |
|
|
34
|
+
| `renewed` | Existing policy is still valid (ID unchanged, not expired) |
|
|
35
|
+
| `errored` | Policy discovery failed due to an error |
|
|
36
|
+
|
|
37
|
+
### Policy Object Fields
|
|
38
|
+
|
|
39
|
+
| Field | Type | Presence | Description |
|
|
40
|
+
|-------|------|----------|-------------|
|
|
41
|
+
| `id` | `string\|false` | Always | Policy ID from DNS TXT record, or `false` if not found |
|
|
42
|
+
| `version` | `string` | Policy found | Always `"STSv1"` for valid policies |
|
|
43
|
+
| `mode` | `string` | Always | Policy mode (see below) |
|
|
44
|
+
| `mx` | `string[]` | Mode not "none" | Array of allowed MX hostnames (may include wildcards) |
|
|
45
|
+
| `maxAge` | `number` | Policy found | Policy validity period in seconds |
|
|
46
|
+
| `expires` | `string` | Policy found | ISO 8601 expiration timestamp |
|
|
47
|
+
| `error` | `Error` | On error | Error object if discovery failed |
|
|
48
|
+
|
|
49
|
+
### Mode Values
|
|
50
|
+
|
|
51
|
+
| Mode | Description |
|
|
52
|
+
|------|-------------|
|
|
53
|
+
| `testing` | Policy is in test mode; report violations but don't enforce |
|
|
54
|
+
| `enforce` | Strictly enforce TLS and MX restrictions |
|
|
55
|
+
| `none` | No policy in effect |
|
|
56
|
+
|
|
57
|
+
## validateMx Result
|
|
58
|
+
|
|
59
|
+
The `validateMx` function checks if an MX hostname is valid according to the MTA-STS policy.
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
const result = validateMx('alt1.mx.example.com', policy);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Result Object Fields
|
|
66
|
+
|
|
67
|
+
| Field | Type | Presence | Description |
|
|
68
|
+
|-------|------|----------|-------------|
|
|
69
|
+
| `valid` | `boolean` | Always | Whether the MX hostname is allowed |
|
|
70
|
+
| `mode` | `string` | Always | Policy mode (`"testing"`, `"enforce"`, or `"none"`) |
|
|
71
|
+
| `match` | `string` | Valid match | The pattern that matched (exact hostname or wildcard) |
|
|
72
|
+
| `testing` | `boolean` | Always | Whether policy is in testing mode |
|
|
73
|
+
|
|
74
|
+
## Example Output
|
|
75
|
+
|
|
76
|
+
### Policy Found
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"policy": {
|
|
81
|
+
"id": "20240115T120000",
|
|
82
|
+
"version": "STSv1",
|
|
83
|
+
"mode": "enforce",
|
|
84
|
+
"mx": [
|
|
85
|
+
"mx1.example.com",
|
|
86
|
+
"mx2.example.com",
|
|
87
|
+
"*.mail.example.com"
|
|
88
|
+
],
|
|
89
|
+
"maxAge": 86400,
|
|
90
|
+
"expires": "2024-01-16T12:00:00.000Z"
|
|
91
|
+
},
|
|
92
|
+
"status": "found"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Policy Not Found
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"policy": {
|
|
101
|
+
"id": false,
|
|
102
|
+
"mode": "none"
|
|
103
|
+
},
|
|
104
|
+
"status": "not_found"
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Policy Renewed (Cached)
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"policy": {
|
|
113
|
+
"id": "20240115T120000",
|
|
114
|
+
"version": "STSv1",
|
|
115
|
+
"mode": "enforce",
|
|
116
|
+
"mx": [
|
|
117
|
+
"mx1.example.com",
|
|
118
|
+
"mx2.example.com"
|
|
119
|
+
],
|
|
120
|
+
"maxAge": 86400,
|
|
121
|
+
"expires": "2024-01-16T12:00:00.000Z"
|
|
122
|
+
},
|
|
123
|
+
"status": "renewed"
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Policy Error
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"policy": {
|
|
132
|
+
"id": "20240115T120000",
|
|
133
|
+
"mode": "none",
|
|
134
|
+
"expires": "2024-01-15T13:00:00.000Z",
|
|
135
|
+
"error": {
|
|
136
|
+
"message": "Request timeout for https://mta-sts.example.com/.well-known/mta-sts.txt",
|
|
137
|
+
"code": "HTTP_SOCKET_TIMEOUT"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"status": "errored"
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### MX Validation - Valid Match
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"valid": true,
|
|
149
|
+
"mode": "enforce",
|
|
150
|
+
"match": "mx1.example.com",
|
|
151
|
+
"testing": false
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### MX Validation - Wildcard Match
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"valid": true,
|
|
160
|
+
"mode": "enforce",
|
|
161
|
+
"match": ".mail.example.com",
|
|
162
|
+
"testing": false
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### MX Validation - Invalid
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"valid": false,
|
|
171
|
+
"mode": "enforce",
|
|
172
|
+
"testing": false
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### MX Validation - Testing Mode
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"valid": true,
|
|
181
|
+
"mode": "testing",
|
|
182
|
+
"match": "mx1.example.com",
|
|
183
|
+
"testing": true
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### MX Validation - No Policy
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"valid": true,
|
|
192
|
+
"mode": "none",
|
|
193
|
+
"testing": false
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Error Codes
|
|
198
|
+
|
|
199
|
+
Errors that may appear in `policy.error`:
|
|
200
|
+
|
|
201
|
+
| Code | Description |
|
|
202
|
+
|------|-------------|
|
|
203
|
+
| `multi_sts_records` | Multiple TXT records found for `_mta-sts.{domain}` |
|
|
204
|
+
| `invalid_sts_version` | Policy file has invalid or missing version field |
|
|
205
|
+
| `invalid_sts_mode` | Policy file has invalid mode value |
|
|
206
|
+
| `invalid_sts_max_age` | Policy file has invalid max_age value |
|
|
207
|
+
| `invalid_sts_mx` | Policy file missing mx field in enforce/testing mode |
|
|
208
|
+
| `HTTP_SOCKET_TIMEOUT` | HTTP request timed out |
|
|
209
|
+
| `http_status_{code}` | HTTP request returned non-2xx status |
|
|
210
|
+
| `ENOTFOUND` | DNS lookup failed (domain not found) |
|
|
211
|
+
| `ENODATA` | DNS lookup returned no data |
|
|
212
|
+
|
|
213
|
+
## Usage Example
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
const { getPolicy, validateMx } = require('mailauth/lib/mta-sts');
|
|
217
|
+
|
|
218
|
+
// Fetch policy
|
|
219
|
+
const { policy, status } = await getPolicy('gmail.com');
|
|
220
|
+
|
|
221
|
+
console.log(`Policy status: ${status}`);
|
|
222
|
+
console.log(`Policy mode: ${policy.mode}`);
|
|
223
|
+
|
|
224
|
+
if (policy.mode !== 'none') {
|
|
225
|
+
// Validate MX hostname
|
|
226
|
+
const mx = 'alt1.gmail-smtp-in.l.google.com';
|
|
227
|
+
const validation = validateMx(mx, policy);
|
|
228
|
+
|
|
229
|
+
if (!validation.valid && !validation.testing) {
|
|
230
|
+
console.error(`MX ${mx} is not allowed by MTA-STS policy`);
|
|
231
|
+
// Reject delivery attempt
|
|
232
|
+
} else if (!validation.valid && validation.testing) {
|
|
233
|
+
console.warn(`MX ${mx} violates MTA-STS policy (testing mode)`);
|
|
234
|
+
// Report violation but continue
|
|
235
|
+
} else {
|
|
236
|
+
console.log(`MX ${mx} is allowed`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|