mailauth 5.0.3 → 7.0.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 +23 -0
- package/README.md +117 -41
- package/bin/mailauth.js +14 -6
- package/cli.md +8 -3
- package/docs/arc.md +126 -43
- package/docs/bimi.md +54 -37
- package/docs/dkim.md +79 -40
- package/docs/dmarc.md +82 -31
- package/docs/mta-sts.md +140 -42
- package/docs/spf.md +69 -4
- package/index.d.ts +499 -45
- package/lib/arc/index.d.ts +10 -8
- package/lib/arc/index.js +505 -124
- package/lib/bimi/index.js +162 -81
- package/lib/commands/report.js +2 -1
- package/lib/commands/seal.js +35 -5
- package/lib/commands/sign.js +8 -1
- package/lib/commands/spf.js +8 -0
- package/lib/dkim/body/relaxed.js +6 -5
- package/lib/dkim/body/simple.js +1 -3
- package/lib/dkim/dkim-signer.js +219 -33
- package/lib/dkim/dkim-verifier.js +574 -231
- package/lib/dkim/header/relaxed.js +6 -43
- package/lib/dkim/header/signature-opts.js +87 -0
- package/lib/dkim/header/simple.js +6 -43
- package/lib/dkim/message-parser.js +18 -4
- package/lib/dkim/sign.d.ts +13 -7
- package/lib/dkim/sign.js +18 -2
- package/lib/dkim/syntax.js +31 -0
- package/lib/dkim/verify.js +1 -0
- package/lib/dmarc/author-domain.js +181 -0
- package/lib/dmarc/get-dmarc-record.js +267 -53
- package/lib/dmarc/index.d.ts +29 -2
- package/lib/dmarc/index.js +2 -1
- package/lib/dmarc/verify.js +205 -53
- package/lib/mailauth.js +86 -40
- package/lib/mta-sts.d.ts +77 -11
- package/lib/mta-sts.js +497 -182
- package/lib/parse-dkim-headers.js +376 -191
- package/lib/parse-received.js +104 -32
- package/lib/spf/index.d.ts +37 -1
- package/lib/spf/index.js +260 -28
- package/lib/spf/macro.js +50 -13
- package/lib/spf/spf-verify.js +440 -204
- package/lib/spf/syntax.js +366 -0
- package/lib/tools.js +524 -118
- package/package.json +12 -12
package/lib/parse-received.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { DANGEROUS_KEYS } = require('./safe-keys');
|
|
4
|
+
const { isIP } = require('node:net');
|
|
5
|
+
|
|
6
|
+
const TLS_COMMENT = /tls|cipher=|Google Transport Security/i;
|
|
4
7
|
|
|
5
8
|
const parseReceived = buf => {
|
|
6
9
|
let header = (buf || '').toString();
|
|
@@ -31,6 +34,29 @@ const parseReceived = buf => {
|
|
|
31
34
|
|
|
32
35
|
let curValue = nextValue();
|
|
33
36
|
|
|
37
|
+
// adds a character of a key or a value, starting the next one where needed
|
|
38
|
+
let appendChar = c => {
|
|
39
|
+
if (state === 'none') {
|
|
40
|
+
state = 'val';
|
|
41
|
+
switch (curKey) {
|
|
42
|
+
case '':
|
|
43
|
+
curKey = 'key';
|
|
44
|
+
break;
|
|
45
|
+
case 'key':
|
|
46
|
+
curKey = 'value';
|
|
47
|
+
break;
|
|
48
|
+
case 'value':
|
|
49
|
+
case 'comment':
|
|
50
|
+
curValue = nextValue();
|
|
51
|
+
curKey = 'key';
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
} else if (curKey === 'comment' && c === '(') {
|
|
55
|
+
commentLevel++;
|
|
56
|
+
}
|
|
57
|
+
curValue[curKey] += c;
|
|
58
|
+
};
|
|
59
|
+
|
|
34
60
|
for (let i = 0; i < headerValue.length; i++) {
|
|
35
61
|
let c = headerValue.charAt(i);
|
|
36
62
|
|
|
@@ -46,12 +72,16 @@ const parseReceived = buf => {
|
|
|
46
72
|
}
|
|
47
73
|
|
|
48
74
|
if (quoted) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
75
|
+
// RFC 5322 quoted-string, kept as it is, quotes included, so that an address
|
|
76
|
+
// such as <"x;y"@example.com> keeps its local-part
|
|
77
|
+
if (c === '\\' && i < headerValue.length - 1) {
|
|
78
|
+
curValue[curKey] += c + headerValue.charAt(++i);
|
|
52
79
|
continue;
|
|
53
80
|
}
|
|
54
81
|
curValue[curKey] += c;
|
|
82
|
+
if (c === '"') {
|
|
83
|
+
quoted = false;
|
|
84
|
+
}
|
|
55
85
|
continue;
|
|
56
86
|
}
|
|
57
87
|
|
|
@@ -85,8 +115,9 @@ const parseReceived = buf => {
|
|
|
85
115
|
state = 'none';
|
|
86
116
|
break;
|
|
87
117
|
case '"':
|
|
88
|
-
|
|
89
|
-
//
|
|
118
|
+
// start quoting. Only the double quote quotes (RFC 5322 section 3.2.4), an
|
|
119
|
+
// apostrophe is an ordinary character, as in <o'brien@example.com>
|
|
120
|
+
appendChar(c);
|
|
90
121
|
quoted = c;
|
|
91
122
|
break;
|
|
92
123
|
case '(':
|
|
@@ -102,30 +133,7 @@ const parseReceived = buf => {
|
|
|
102
133
|
escaped = true;
|
|
103
134
|
break;
|
|
104
135
|
default:
|
|
105
|
-
|
|
106
|
-
state = 'val';
|
|
107
|
-
switch (curKey) {
|
|
108
|
-
case '':
|
|
109
|
-
curKey = 'key';
|
|
110
|
-
curValue[curKey] += c;
|
|
111
|
-
break;
|
|
112
|
-
case 'key':
|
|
113
|
-
curKey = 'value';
|
|
114
|
-
curValue[curKey] += c;
|
|
115
|
-
break;
|
|
116
|
-
case 'value':
|
|
117
|
-
case 'comment':
|
|
118
|
-
curValue = nextValue();
|
|
119
|
-
curKey = 'key';
|
|
120
|
-
curValue[curKey] += c;
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
} else {
|
|
124
|
-
if (curKey === 'comment' && c === '(') {
|
|
125
|
-
commentLevel++;
|
|
126
|
-
}
|
|
127
|
-
curValue[curKey] += c;
|
|
128
|
-
}
|
|
136
|
+
appendChar(c);
|
|
129
137
|
}
|
|
130
138
|
}
|
|
131
139
|
|
|
@@ -148,13 +156,27 @@ const parseReceived = buf => {
|
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
158
|
|
|
159
|
+
// A from clause can have more than one comment after it. Postfix writes the TCP-info
|
|
160
|
+
// comment after the HELO name, and a HELO name with parentheses in it looks like a
|
|
161
|
+
// comment of its own, as in "from x([192.0.2.1]) (unknown [198.51.100.7])". The comments
|
|
162
|
+
// are kept together, in order, so that the TCP-info comment is always the last part
|
|
163
|
+
for (let i = 1; i < values.length; i++) {
|
|
164
|
+
let val = values[i];
|
|
165
|
+
let prev = values[i - 1];
|
|
166
|
+
if (!val.key && !val.value && val.comment && (prev.key || '').toLowerCase() === 'from' && !TLS_COMMENT.test(val.comment)) {
|
|
167
|
+
prev.comment = [prev.comment || []].concat(val.comment).join(' ');
|
|
168
|
+
values.splice(i, 1);
|
|
169
|
+
i--;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
151
173
|
for (let val of values) {
|
|
152
174
|
if (val.comment) {
|
|
153
175
|
val.comment = val.comment.replace(/\s+/g, ' ').trim();
|
|
154
176
|
}
|
|
155
177
|
if (val.key) {
|
|
156
178
|
let key = val.key.toLowerCase();
|
|
157
|
-
if (key !== 'from' && !result.tls &&
|
|
179
|
+
if (key !== 'from' && !result.tls && TLS_COMMENT.test(val.comment)) {
|
|
158
180
|
result.tls = { value: '', ...(val.comment && { comment: val.comment }) };
|
|
159
181
|
val.comment = '';
|
|
160
182
|
}
|
|
@@ -163,7 +185,7 @@ const parseReceived = buf => {
|
|
|
163
185
|
continue;
|
|
164
186
|
}
|
|
165
187
|
result[key] = { value: val.value, ...(val.comment && { comment: val.comment }) };
|
|
166
|
-
} else if (!result.tls &&
|
|
188
|
+
} else if (!result.tls && TLS_COMMENT.test(val.comment)) {
|
|
167
189
|
result.tls = { value: val.value, ...(val.comment && { comment: val.comment }) };
|
|
168
190
|
}
|
|
169
191
|
}
|
|
@@ -181,4 +203,54 @@ const parseReceived = buf => {
|
|
|
181
203
|
return result;
|
|
182
204
|
};
|
|
183
205
|
|
|
184
|
-
|
|
206
|
+
// An address literal (RFC 5321 section 4.1.3), with an optional port after it as Exim writes
|
|
207
|
+
// it. The content is checked with isIP separately
|
|
208
|
+
const ADDRESS_LITERAL = /\[(?:IPv6:)?([^\]\s]+)\](?::\d+)?/gi;
|
|
209
|
+
// "helo=" or "ehlo=" right before a literal, the client's own claim, not its address
|
|
210
|
+
const HELO_PREFIX = /(?:^|[\s(;,])(?:helo|ehlo)=\s*$/i;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Finds the IP address of the client in the from clause of a parsed Received header.
|
|
214
|
+
*
|
|
215
|
+
* RFC 5321 section 4.4 puts the address the connection came from into the TCP-info comment
|
|
216
|
+
* of the from clause, "from helo.name (rdns.name [192.0.2.1])", so it is the last address
|
|
217
|
+
* literal of that comment. The rest of the clause is the client's own claim: the HELO name,
|
|
218
|
+
* which can itself be an address literal, and in Exim's format a "helo=" value inside the
|
|
219
|
+
* comment. Those are never used. Exim writes "from [192.0.2.1] (helo=...)" for a client
|
|
220
|
+
* without reverse DNS, where the literal in front of the comment is the address of the
|
|
221
|
+
* connection, which is only used when the comment has no address literal of its own.
|
|
222
|
+
*
|
|
223
|
+
* @param {Object} from The "from" entry of parseReceived(), { value, comment }
|
|
224
|
+
* @returns {String|false} IP address, without an "IPv6:" prefix, or false if none was found
|
|
225
|
+
*/
|
|
226
|
+
const getClientAddress = from => {
|
|
227
|
+
if (!from || typeof from !== 'object') {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let comment = typeof from.comment === 'string' ? from.comment : '';
|
|
232
|
+
let value = typeof from.value === 'string' ? from.value.trim() : '';
|
|
233
|
+
|
|
234
|
+
let address = false;
|
|
235
|
+
for (let match of comment.matchAll(ADDRESS_LITERAL)) {
|
|
236
|
+
if (HELO_PREFIX.test(comment.substring(0, match.index))) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (isIP(match[1])) {
|
|
240
|
+
address = match[1];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (address) {
|
|
245
|
+
return address;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let valueMatch = value.match(/^\[(?:IPv6:)?([^\]\s]+)\](?::\d+)?$/i);
|
|
249
|
+
if (valueMatch && comment && isIP(valueMatch[1])) {
|
|
250
|
+
return valueMatch[1];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return false;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
module.exports = { parseReceived, getClientAddress };
|
package/lib/spf/index.d.ts
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
// Type definitions for mailauth/lib/spf
|
|
2
2
|
|
|
3
|
-
import { SPFOptions, SPFResult } from '../../index';
|
|
3
|
+
import { SPFOptions as BaseSPFOptions, SPFResult as BaseSPFResult } from '../../index';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SPF verification options
|
|
7
|
+
*/
|
|
8
|
+
export interface SPFOptions extends BaseSPFOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Follow RFC 7208 exactly instead of the lenient default (default: false). Strict mode
|
|
11
|
+
* validates the whole record before evaluation, rejects the c, r and t macros outside of
|
|
12
|
+
* explanation text, counts void lookups per RFC 7208 section 4.6.4, discards records with
|
|
13
|
+
* leading whitespace, and reports only the checked identity in Authentication-Results
|
|
14
|
+
*/
|
|
15
|
+
strict?: boolean;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Maximum time in milliseconds for the whole evaluation. If exceeded, the result is
|
|
19
|
+
* temperror (RFC 7208 section 4.6.4). No limit by default
|
|
20
|
+
*/
|
|
21
|
+
maxElapsedTime?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* SPF verification result
|
|
26
|
+
*/
|
|
27
|
+
export interface SPFResult extends BaseSPFResult {
|
|
28
|
+
/**
|
|
29
|
+
* Explanation string published by the domain owner with the "exp" modifier
|
|
30
|
+
* (RFC 7208 section 6.2). Only set for a "fail" result. This is third party text
|
|
31
|
+
*/
|
|
32
|
+
explanation?: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Set in the default mode when something was accepted that strict mode would have
|
|
36
|
+
* rejected: "syntax-error", "void-lookup-limit" or "non-ascii-local-part"
|
|
37
|
+
*/
|
|
38
|
+
warnings?: string[];
|
|
39
|
+
}
|
|
4
40
|
|
|
5
41
|
/**
|
|
6
42
|
* Verifies SPF for a sender
|