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/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
+ ```
@@ -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
+ ```