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/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.12.0",
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.7.4",
45
+ "prettier": "3.8.1",
46
46
  "resedit": "3.0.1"
47
47
  },
48
48
  "dependencies": {
49
- "@postalsys/vmc": "1.1.2",
50
- "fast-xml-parser": "4.5.2",
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.11",
54
+ "nodemailer": "7.0.13",
55
55
  "punycode.js": "2.3.1",
56
- "tldts": "7.0.19",
57
- "undici": "7.16.0",
56
+ "tldts": "7.0.21",
57
+ "undici": "7.20.0",
58
58
  "yargs": "17.7.2"
59
59
  },
60
60
  "engines": {
61
- "node": ">=18.0.0"
61
+ "node": ">=20.18.1"
62
62
  },
63
63
  "bin": {
64
64
  "mailauth": "bin/mailauth.js"