mailauth 6.0.0 → 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 +12 -0
- package/README.md +111 -39
- 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 +80 -33
- package/docs/mta-sts.md +140 -42
- package/docs/spf.md +69 -4
- package/index.d.ts +495 -43
- 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 +140 -47
- package/lib/dmarc/index.d.ts +29 -2
- package/lib/dmarc/verify.js +120 -44
- package/lib/mailauth.js +75 -36
- 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 +522 -116
- package/package.json +1 -1
|
@@ -8,13 +8,166 @@ const { DANGEROUS_KEYS } = require('./safe-keys');
|
|
|
8
8
|
// parser (valueParser has its own reservation: "header" is a legitimate ptype there).
|
|
9
9
|
const RESERVED_RESULT_KEYS = new Set(['header', 'value']);
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// Header fields whose value follows the Authentication-Results grammar of RFC 8601 section
|
|
12
|
+
// 2.2, which has RFC 5322 comments and quoted strings. Every other header field this module
|
|
13
|
+
// is used for (DKIM-Signature, ARC-Seal, ARC-Message-Signature, BIMI-Selector, and the DNS
|
|
14
|
+
// records passed in with a "DNS: TXT;" prefix) is a tag-list of RFC 6376 section 3.2, where
|
|
15
|
+
// quotes, parentheses and backslashes are ordinary value characters
|
|
16
|
+
const AR_HEADERS = new Set(['authentication-results', 'arc-authentication-results']);
|
|
17
|
+
|
|
18
|
+
// tags whose values have all whitespace removed (RFC 6376 section 3.5: FWS in b= and bh= is
|
|
19
|
+
// ignored, section 3.6.1 allows it anywhere in p=, and h= allows it around the colons)
|
|
20
|
+
const WHITESPACE_FREE_TAGS = new Set(['bh', 'b', 'p', 'h']);
|
|
21
|
+
// tags whose values are converted to numbers when they look like one
|
|
22
|
+
const NUMERIC_TAGS = new Set(['l', 'v', 't', 'x']);
|
|
23
|
+
|
|
24
|
+
// RFC 6376 section 3.2: tag-name = ALPHA *ALNUMPUNC
|
|
25
|
+
const TAG_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
26
|
+
// FWS around a tag-spec, only SP, HTAB and the CRLF of a fold
|
|
27
|
+
const FWS_EDGES = /^[ \t\r\n]+|[ \t\r\n]+$/g;
|
|
28
|
+
|
|
29
|
+
const normalizeString = value => value.replace(/\s+/g, ' ').trim();
|
|
30
|
+
|
|
31
|
+
// Converts the value of a known tag (lower case `key`) to the form readers expect: no
|
|
32
|
+
// whitespace in the values of WHITESPACE_FREE_TAGS, numbers for NUMERIC_TAGS and for the
|
|
33
|
+
// instance of an ARC header field
|
|
34
|
+
const normalizeTagValue = (key, value, headerKey) => {
|
|
35
|
+
if (WHITESPACE_FREE_TAGS.has(key)) {
|
|
36
|
+
return value.replace(/\s+/g, '');
|
|
37
|
+
}
|
|
38
|
+
if (NUMERIC_TAGS.has(key) && !isNaN(value)) {
|
|
39
|
+
return Number(value);
|
|
40
|
+
}
|
|
41
|
+
if (key === 'i' && /^arc-/i.test(headerKey || '')) {
|
|
42
|
+
return Number(value);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Builds the { key: { value } } map of a tag-list from tags whose values tagListParser has
|
|
48
|
+
// already normalized. `caseSensitive` keeps tag names as they are (RFC 6376 section 3.2,
|
|
49
|
+
// "Tags MUST be interpreted in a case-sensitive manner"), the default folds them to lower
|
|
50
|
+
// case the way this parser always has
|
|
51
|
+
const buildTagMap = (tags, headerKey, caseSensitive) => {
|
|
52
|
+
let result = {
|
|
53
|
+
header: headerKey
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
for (let tag of tags) {
|
|
57
|
+
let key = normalizeString(tag.name);
|
|
58
|
+
if (!caseSensitive) {
|
|
59
|
+
key = key.toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!key || RESERVED_RESULT_KEYS.has(key) || DANGEROUS_KEYS.has(key)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let value = tag.value;
|
|
67
|
+
if (!caseSensitive || key === key.toLowerCase()) {
|
|
68
|
+
// every tag name this module interprets is lower case, so in the case-sensitive
|
|
69
|
+
// mode a name with an upper case letter in it is an unknown tag, and unknown tags
|
|
70
|
+
// are kept verbatim
|
|
71
|
+
value = normalizeTagValue(key.toLowerCase(), value, headerKey);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
result[key] = { value };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parses a tag-list (RFC 6376 section 3.2). Semicolons separate the tag-specs, the first
|
|
82
|
+
* "=" separates a tag name from its value, and nothing else is syntax: a tag-value can hold
|
|
83
|
+
* any printable character except the semicolon, so quotes, parentheses and backslashes in
|
|
84
|
+
* n= notes, z= copies or unknown tags are part of the value.
|
|
85
|
+
*
|
|
86
|
+
* Returns the same shape the parser always had ({ parsed }) plus
|
|
87
|
+
* - tags: every tag-spec in order, as { name, value }, with the name case preserved and the
|
|
88
|
+
* value a string (FWS around it removed, inner whitespace collapsed to single spaces)
|
|
89
|
+
* - syntaxErrors: a list of reasons why the tag-list is not valid RFC 6376 syntax. Empty for
|
|
90
|
+
* a valid tag-list. The lenient readers ignore it, strict readers reject the tag-list
|
|
91
|
+
*
|
|
92
|
+
* With `strict` set, `parsed` is keyed by the tag names as they are written (RFC 6376 section
|
|
93
|
+
* 3.2) and holds the tag-specs only: key-only parts, which are syntax errors, are left out
|
|
94
|
+
*/
|
|
95
|
+
const tagListParser = (line, headerKey, strict) => {
|
|
96
|
+
let tags = [];
|
|
97
|
+
// parts without "=" after the first one
|
|
98
|
+
let keyOnly = [];
|
|
99
|
+
let syntaxErrors = [];
|
|
100
|
+
let defaultValue;
|
|
101
|
+
|
|
102
|
+
let segments = line.split(';');
|
|
103
|
+
let seen = new Set();
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < segments.length; i++) {
|
|
106
|
+
let segment = segments[i];
|
|
107
|
+
let trimmed = segment.replace(FWS_EDGES, '');
|
|
108
|
+
|
|
109
|
+
if (!trimmed) {
|
|
110
|
+
// one trailing semicolon is allowed, an empty tag-spec anywhere else is not
|
|
111
|
+
if (i < segments.length - 1 || segments.length === 1) {
|
|
112
|
+
syntaxErrors.push('empty tag-spec');
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
12
116
|
|
|
117
|
+
let eqPos = segment.indexOf('=');
|
|
118
|
+
if (eqPos < 0) {
|
|
119
|
+
syntaxErrors.push(`tag-spec without "=": ${JSON.stringify(normalizeString(trimmed))}`);
|
|
120
|
+
// the first key-only part is the default value, which is how the "DNS: TXT;"
|
|
121
|
+
// prefix and the authserv-id of an AR header have always been exposed
|
|
122
|
+
if (typeof defaultValue === 'undefined') {
|
|
123
|
+
defaultValue = normalizeString(trimmed).toLowerCase();
|
|
124
|
+
} else {
|
|
125
|
+
keyOnly.push(normalizeString(trimmed));
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let name = segment.substring(0, eqPos).replace(FWS_EDGES, '');
|
|
131
|
+
let value = normalizeString(segment.substring(eqPos + 1));
|
|
132
|
+
|
|
133
|
+
if (!TAG_NAME.test(name)) {
|
|
134
|
+
syntaxErrors.push(`invalid tag name: ${JSON.stringify(normalizeString(name))}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (seen.has(name)) {
|
|
138
|
+
// RFC 6376 section 3.2: a duplicate tag name makes the entire tag-list invalid
|
|
139
|
+
syntaxErrors.push(`duplicate tag: ${JSON.stringify(name)}`);
|
|
140
|
+
}
|
|
141
|
+
seen.add(name);
|
|
142
|
+
|
|
143
|
+
tags.push({ name, value });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let parsed = buildTagMap(tags, headerKey, strict);
|
|
147
|
+
|
|
148
|
+
if (!strict) {
|
|
149
|
+
// key-only parts after the first one were stored as empty values, keep doing that
|
|
150
|
+
for (let name of keyOnly) {
|
|
151
|
+
let key = name.toLowerCase();
|
|
152
|
+
if (key && !RESERVED_RESULT_KEYS.has(key) && !DANGEROUS_KEYS.has(key)) {
|
|
153
|
+
parsed[key] = { value: '' };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (typeof defaultValue !== 'undefined') {
|
|
158
|
+
parsed.value = defaultValue;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { parsed, tags, syntaxErrors };
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Parses the resinfo payload of a single Authentication-Results method, eg.
|
|
166
|
+
// `pass header.i=@example.com header.s="sel 1"`, into { value, ptype: { property } }
|
|
13
167
|
const valueParser = str => {
|
|
14
168
|
let line = str.replace(/\s+/g, ' ').trim();
|
|
15
169
|
|
|
16
170
|
let parts = [];
|
|
17
|
-
let lastState = false;
|
|
18
171
|
|
|
19
172
|
const createPart = () => {
|
|
20
173
|
let part = {
|
|
@@ -27,74 +180,54 @@ const valueParser = str => {
|
|
|
27
180
|
|
|
28
181
|
const parse = () => {
|
|
29
182
|
let state = 'key';
|
|
30
|
-
let
|
|
31
|
-
let quote;
|
|
183
|
+
let quoted = false;
|
|
32
184
|
|
|
33
185
|
let curPart = createPart();
|
|
34
186
|
|
|
35
187
|
for (let i = 0; i < line.length; i++) {
|
|
36
188
|
let c = line.charAt(i);
|
|
37
189
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
190
|
+
if (quoted) {
|
|
191
|
+
// RFC 5322 quoted-string, a quoted-pair stands for the character it escapes
|
|
192
|
+
if (c === '\\' && i < line.length - 1) {
|
|
193
|
+
curPart[state] += line.charAt(++i);
|
|
194
|
+
} else if (c === '"') {
|
|
195
|
+
quoted = false;
|
|
196
|
+
} else {
|
|
197
|
+
curPart[state] += c;
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
45
201
|
|
|
46
|
-
|
|
47
|
-
|
|
202
|
+
switch (c) {
|
|
203
|
+
case '=':
|
|
204
|
+
if (state === 'key') {
|
|
205
|
+
state = 'value';
|
|
206
|
+
} else {
|
|
48
207
|
curPart[state] += c;
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
switch (c) {
|
|
53
|
-
case ' ':
|
|
54
|
-
// start new part
|
|
55
|
-
curPart = createPart();
|
|
56
|
-
state = 'key';
|
|
57
|
-
break;
|
|
58
|
-
|
|
59
|
-
case '\\':
|
|
60
|
-
escaped = true;
|
|
61
|
-
break;
|
|
62
|
-
|
|
63
|
-
case '"':
|
|
64
|
-
case "'":
|
|
65
|
-
lastState = state;
|
|
66
|
-
state = 'quoted';
|
|
67
|
-
quote = c;
|
|
68
|
-
break;
|
|
69
|
-
|
|
70
|
-
default:
|
|
71
|
-
curPart[state] += c;
|
|
72
|
-
break;
|
|
73
208
|
}
|
|
209
|
+
break;
|
|
74
210
|
|
|
211
|
+
case ' ':
|
|
212
|
+
// start new part
|
|
213
|
+
curPart = createPart();
|
|
214
|
+
state = 'key';
|
|
75
215
|
break;
|
|
76
|
-
}
|
|
77
216
|
|
|
78
|
-
case '
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
217
|
+
case '\\':
|
|
218
|
+
// not meaningful outside a quoted string, kept as an escape of the next
|
|
219
|
+
// character the way this parser has always read it
|
|
220
|
+
if (i < line.length - 1) {
|
|
221
|
+
curPart[state] += line.charAt(++i);
|
|
82
222
|
}
|
|
223
|
+
break;
|
|
83
224
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
break;
|
|
88
|
-
|
|
89
|
-
case quote:
|
|
90
|
-
state = lastState;
|
|
91
|
-
break;
|
|
92
|
-
|
|
93
|
-
default:
|
|
94
|
-
curPart[lastState] += c;
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
225
|
+
case '"':
|
|
226
|
+
quoted = true;
|
|
227
|
+
break;
|
|
97
228
|
|
|
229
|
+
default:
|
|
230
|
+
curPart[state] += c;
|
|
98
231
|
break;
|
|
99
232
|
}
|
|
100
233
|
}
|
|
@@ -132,15 +265,11 @@ const valueParser = str => {
|
|
|
132
265
|
return parse();
|
|
133
266
|
};
|
|
134
267
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
headerKey = line.substr(0, splitterPos).trim().toLowerCase();
|
|
141
|
-
line = line.substr(splitterPos + 1).trim();
|
|
142
|
-
}
|
|
143
|
-
|
|
268
|
+
// Authentication-Results grammar (RFC 8601 section 2.2): semicolons separate the resinfo
|
|
269
|
+
// parts unless they are inside a comment or a quoted string. Comments (which nest) are
|
|
270
|
+
// collected into the comment property of their part, quoted strings are kept as they are,
|
|
271
|
+
// quotes and quoted-pairs included, so that valueParser can read them
|
|
272
|
+
const arParser = (line, headerKey) => {
|
|
144
273
|
let parts = [];
|
|
145
274
|
let lastState = false;
|
|
146
275
|
|
|
@@ -155,166 +284,222 @@ const headerParser = buf => {
|
|
|
155
284
|
return part;
|
|
156
285
|
};
|
|
157
286
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
let escaped;
|
|
161
|
-
let quote;
|
|
287
|
+
let state = 'key';
|
|
288
|
+
let commentDepth = 0;
|
|
162
289
|
|
|
163
|
-
|
|
290
|
+
let curPart = createPart();
|
|
164
291
|
|
|
165
|
-
|
|
166
|
-
|
|
292
|
+
for (let i = 0; i < line.length; i++) {
|
|
293
|
+
let c = line.charAt(i);
|
|
167
294
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
295
|
+
switch (state) {
|
|
296
|
+
case 'key':
|
|
297
|
+
if (c === '=') {
|
|
298
|
+
state = 'value';
|
|
299
|
+
curPart.hasValue = true;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
// falls through
|
|
303
|
+
|
|
304
|
+
case 'value': {
|
|
305
|
+
switch (c) {
|
|
306
|
+
case ';':
|
|
307
|
+
// start new part
|
|
308
|
+
curPart = createPart();
|
|
309
|
+
state = 'key';
|
|
173
310
|
break;
|
|
174
|
-
}
|
|
175
|
-
// falls through
|
|
176
311
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
312
|
+
case '\\':
|
|
313
|
+
// not meaningful outside a quoted string or a comment, the next
|
|
314
|
+
// character is taken as it is
|
|
315
|
+
if (i < line.length - 1) {
|
|
316
|
+
curPart[state] += line.charAt(++i);
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
181
319
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
break;
|
|
192
|
-
|
|
193
|
-
case '(':
|
|
194
|
-
lastState = state;
|
|
195
|
-
state = 'comment';
|
|
196
|
-
break;
|
|
197
|
-
|
|
198
|
-
case '"':
|
|
199
|
-
case "'":
|
|
200
|
-
lastState = state;
|
|
201
|
-
curPart[state] += c;
|
|
202
|
-
state = 'quoted';
|
|
203
|
-
quote = c;
|
|
204
|
-
break;
|
|
205
|
-
|
|
206
|
-
default:
|
|
207
|
-
curPart[state] += c;
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
320
|
+
case '(':
|
|
321
|
+
lastState = state;
|
|
322
|
+
state = 'comment';
|
|
323
|
+
commentDepth = 1;
|
|
324
|
+
if (curPart.comment) {
|
|
325
|
+
// a second comment in the same part
|
|
326
|
+
curPart.comment += ' ';
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
210
329
|
|
|
211
|
-
|
|
330
|
+
case '"':
|
|
331
|
+
lastState = state;
|
|
332
|
+
curPart[state] += c;
|
|
333
|
+
state = 'quoted';
|
|
334
|
+
break;
|
|
335
|
+
|
|
336
|
+
default:
|
|
337
|
+
curPart[state] += c;
|
|
338
|
+
break;
|
|
212
339
|
}
|
|
213
340
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
case '\\':
|
|
217
|
-
escaped = true;
|
|
218
|
-
break;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
219
343
|
|
|
220
|
-
|
|
344
|
+
case 'comment':
|
|
345
|
+
switch (c) {
|
|
346
|
+
case '\\':
|
|
347
|
+
// quoted-pair
|
|
348
|
+
if (i < line.length - 1) {
|
|
349
|
+
curPart.comment += line.charAt(++i);
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
|
|
353
|
+
case '(':
|
|
354
|
+
commentDepth++;
|
|
355
|
+
curPart.comment += c;
|
|
356
|
+
break;
|
|
357
|
+
|
|
358
|
+
case ')':
|
|
359
|
+
commentDepth--;
|
|
360
|
+
if (commentDepth <= 0) {
|
|
221
361
|
state = lastState;
|
|
222
|
-
|
|
362
|
+
} else {
|
|
363
|
+
curPart.comment += c;
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
223
366
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
367
|
+
default:
|
|
368
|
+
curPart.comment += c;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
228
371
|
|
|
229
|
-
|
|
372
|
+
break;
|
|
230
373
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
374
|
+
case 'quoted':
|
|
375
|
+
switch (c) {
|
|
376
|
+
case '\\':
|
|
377
|
+
// quoted-pair, kept for valueParser to decode
|
|
378
|
+
curPart[lastState] += c;
|
|
379
|
+
if (i < line.length - 1) {
|
|
380
|
+
curPart[lastState] += line.charAt(++i);
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
236
383
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
384
|
+
case '"':
|
|
385
|
+
state = lastState;
|
|
386
|
+
curPart[lastState] += c;
|
|
387
|
+
break;
|
|
240
388
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
389
|
+
default:
|
|
390
|
+
curPart[lastState] += c;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
245
393
|
|
|
246
|
-
|
|
247
|
-
}
|
|
394
|
+
break;
|
|
248
395
|
}
|
|
396
|
+
}
|
|
249
397
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
398
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
399
|
+
for (let key of Object.keys(parts[i])) {
|
|
400
|
+
if (typeof parts[i][key] === 'string') {
|
|
401
|
+
parts[i][key] = normalizeString(parts[i][key]);
|
|
255
402
|
}
|
|
403
|
+
}
|
|
256
404
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
parts[i].value = parts[i].value.replace(/\s+/g, '');
|
|
265
|
-
} else if (['l', 'v', 't', 'x'].includes(parts[i].key) && !isNaN(parts[i].value)) {
|
|
266
|
-
parts[i].value = Number(parts[i].value);
|
|
267
|
-
} else if (parts[i].key === 'i' && /^arc-/i.test(headerKey)) {
|
|
268
|
-
parts[i].value = Number(parts[i].value);
|
|
269
|
-
}
|
|
405
|
+
parts[i].key = parts[i].key.toLowerCase();
|
|
406
|
+
|
|
407
|
+
if (!parts[i].key) {
|
|
408
|
+
// remove empty value
|
|
409
|
+
parts.splice(i, 1);
|
|
410
|
+
} else {
|
|
411
|
+
parts[i].value = normalizeTagValue(parts[i].key, parts[i].value, headerKey);
|
|
270
412
|
}
|
|
413
|
+
}
|
|
271
414
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
415
|
+
let result = {
|
|
416
|
+
header: headerKey
|
|
417
|
+
};
|
|
275
418
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
419
|
+
for (let i = 0; i < parts.length; i++) {
|
|
420
|
+
// find the first entry with key only and use it as the default value
|
|
421
|
+
if (parts[i].key && !parts[i].hasValue) {
|
|
422
|
+
result.value = parts[i].key;
|
|
423
|
+
parts.splice(i, 1);
|
|
424
|
+
break;
|
|
283
425
|
}
|
|
426
|
+
}
|
|
284
427
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
428
|
+
parts.forEach(part => {
|
|
429
|
+
if (!part.key || RESERVED_RESULT_KEYS.has(part.key) || DANGEROUS_KEYS.has(part.key)) {
|
|
430
|
+
// nothing would be assigned, so do not parse the part either
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
290
433
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
434
|
+
let entry = {
|
|
435
|
+
value: part.value
|
|
436
|
+
};
|
|
294
437
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
438
|
+
if (AR_HEADERS.has(headerKey) && typeof part.value === 'string') {
|
|
439
|
+
// parse value into subparts as well
|
|
440
|
+
entry = Object.assign(entry, valueParser(entry.value));
|
|
441
|
+
}
|
|
299
442
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
443
|
+
if (part.comment) {
|
|
444
|
+
entry.comment = part.comment;
|
|
445
|
+
}
|
|
303
446
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
result[part.key].push(entry);
|
|
309
|
-
} else {
|
|
310
|
-
result[part.key] = entry;
|
|
447
|
+
if (AR_HEADERS.has(headerKey) && part.key === 'dkim') {
|
|
448
|
+
if (!result[part.key]) {
|
|
449
|
+
result[part.key] = [];
|
|
311
450
|
}
|
|
312
|
-
|
|
451
|
+
result[part.key].push(entry);
|
|
452
|
+
} else {
|
|
453
|
+
result[part.key] = entry;
|
|
454
|
+
}
|
|
455
|
+
});
|
|
313
456
|
|
|
314
|
-
|
|
315
|
-
|
|
457
|
+
return result;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Parses a header field. DKIM-Signature, ARC-Seal, ARC-Message-Signature and other tag-list
|
|
462
|
+
* fields are parsed with the RFC 6376 section 3.2 grammar, Authentication-Results and
|
|
463
|
+
* ARC-Authentication-Results with the RFC 8601 grammar. A value with no header name in front
|
|
464
|
+
* of it is parsed with the RFC 8601 grammar, as it always was; use parseTagList() for a bare
|
|
465
|
+
* tag-list such as a DNS record.
|
|
466
|
+
*
|
|
467
|
+
* @param {Buffer|String} buf Header line
|
|
468
|
+
* @param {Object} [options]
|
|
469
|
+
* @param {Boolean} [options.strict] Key the map of a tag-list by the tag names as written
|
|
470
|
+
* (RFC 6376 section 3.2), see tagListParser. Authentication-Results are not affected
|
|
471
|
+
* @returns {Object} { parsed, original } and, for tag-lists, also { tags, syntaxErrors }
|
|
472
|
+
*/
|
|
473
|
+
const headerParser = (buf, options) => {
|
|
474
|
+
let line = (buf || '').toString().trim();
|
|
475
|
+
let splitterPos = line.indexOf(':');
|
|
476
|
+
let headerKey;
|
|
477
|
+
if (splitterPos >= 0) {
|
|
478
|
+
headerKey = line.substr(0, splitterPos).trim().toLowerCase();
|
|
479
|
+
line = line.substr(splitterPos + 1).trim();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (headerKey && !AR_HEADERS.has(headerKey)) {
|
|
483
|
+
let { parsed, tags, syntaxErrors } = tagListParser(line, headerKey, !!options?.strict);
|
|
484
|
+
return { parsed, original: buf, tags, syntaxErrors };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return { parsed: arParser(line, headerKey), original: buf };
|
|
488
|
+
};
|
|
316
489
|
|
|
317
|
-
|
|
490
|
+
/**
|
|
491
|
+
* Parses a bare tag-list, such as a DKIM key record, without looking for a header name
|
|
492
|
+
*
|
|
493
|
+
* @param {Buffer|String} value Tag-list
|
|
494
|
+
* @param {Object} [options]
|
|
495
|
+
* @param {Boolean} [options.strict] Key the map by the tag names as written, see tagListParser
|
|
496
|
+
* @returns {Object} { parsed, original, tags, syntaxErrors }
|
|
497
|
+
*/
|
|
498
|
+
const parseTagList = (value, options) => {
|
|
499
|
+
let line = (value || '').toString().trim();
|
|
500
|
+
let { parsed, tags, syntaxErrors } = tagListParser(line, undefined, !!options?.strict);
|
|
501
|
+
return { parsed, original: value, tags, syntaxErrors };
|
|
318
502
|
};
|
|
319
503
|
|
|
320
504
|
module.exports = headerParser;
|
|
505
|
+
module.exports.parseTagList = parseTagList;
|