mailauth 7.1.1 → 7.3.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/.c8rc.json +11 -0
- package/.prettierignore +3 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +68 -0
- package/README.md +131 -31
- package/bin/mailauth.js +62 -0
- package/cli.md +150 -8
- package/docs/arc.md +62 -54
- package/docs/bimi.md +153 -54
- package/docs/dkim.md +37 -36
- package/docs/dkim2.md +154 -0
- package/docs/dmarc.md +4 -1
- package/docs/mta-sts.md +12 -9
- package/docs/spf.md +14 -13
- package/index.d.ts +583 -42
- package/lib/arc/index.d.ts +6 -2
- package/lib/arc/index.js +45 -15
- package/lib/bimi/index.js +362 -102
- package/lib/bimi/validate-svg.js +493 -60
- package/lib/commands/dkim2-hash.js +25 -0
- package/lib/commands/dkim2-sign.js +77 -0
- package/lib/commands/dkim2-verify.js +59 -0
- package/lib/commands/report.js +10 -35
- package/lib/commands/resolver.js +51 -0
- package/lib/commands/seal.js +10 -38
- package/lib/commands/spf.js +4 -35
- package/lib/dkim/dkim-signer.js +50 -30
- package/lib/dkim/dkim-verifier.js +79 -27
- package/lib/dkim/header/simple.js +2 -2
- package/lib/dkim/sign.js +7 -2
- package/lib/dkim/syntax.js +11 -0
- package/lib/dkim/verify.js +1 -0
- package/lib/dkim2/dkim2-parser.js +52 -0
- package/lib/dkim2/dkim2-signer.js +334 -0
- package/lib/dkim2/dkim2-verifier.js +572 -0
- package/lib/dkim2/fields.js +570 -0
- package/lib/dkim2/hash.d.ts +3 -0
- package/lib/dkim2/hash.js +51 -0
- package/lib/dkim2/key.js +203 -0
- package/lib/dkim2/message-state.js +181 -0
- package/lib/dkim2/recipe.js +134 -0
- package/lib/dkim2/sign.d.ts +3 -0
- package/lib/dkim2/sign.js +38 -0
- package/lib/dkim2/verify.d.ts +3 -0
- package/lib/dkim2/verify.js +19 -0
- package/lib/dmarc/author-domain.js +270 -5
- package/lib/dmarc/get-dmarc-record.js +24 -11
- package/lib/dmarc/verify.js +7 -3
- package/lib/mailauth.js +57 -14
- package/lib/mta-sts.js +37 -11
- package/lib/parse-received.js +214 -25
- package/lib/spf/index.js +21 -4
- package/lib/spf/macro.js +5 -3
- package/lib/spf/spf-verify.js +14 -5
- package/lib/spf/syntax.js +5 -0
- package/lib/tools.js +160 -21
- package/package.json +15 -6
- package/stryker.config.json +16 -0
package/lib/parse-received.js
CHANGED
|
@@ -208,49 +208,238 @@ const parseReceived = buf => {
|
|
|
208
208
|
const ADDRESS_LITERAL = /\[(?:IPv6:)?([^\]\s]+)\](?::\d+)?/gi;
|
|
209
209
|
// "helo=" or "ehlo=" right before a literal, the client's own claim, not its address
|
|
210
210
|
const HELO_PREFIX = /(?:^|[\s(;,])(?:helo|ehlo)=\s*$/i;
|
|
211
|
+
// a "helo=" or "ehlo=" anywhere in a comment, the HELO name as Exim writes it
|
|
212
|
+
const HELO_KEYWORD = /(?:^|[\s(;,])(?:helo|ehlo)=/i;
|
|
213
|
+
// a word of the Received header, anything up to whitespace or a special character
|
|
214
|
+
const WORD = /[^\s()";\\]+/y;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Splits the from clause of a Received header into its words and comments, up to the "by"
|
|
218
|
+
* keyword that ends it. The receiving server writes the TCP-info comment after the HELO name,
|
|
219
|
+
* and some servers (Postfix) copy the HELO argument as it was given, spaces, parentheses and
|
|
220
|
+
* quotes included. Such a HELO can look like a TCP-info comment and a "by" keyword of its own,
|
|
221
|
+
* so a header that can not be split without guessing returns false: a comment or a quoted
|
|
222
|
+
* string that is not closed, a stray ")" or a backslash, a quoted string in the from clause,
|
|
223
|
+
* more than one top level "by" or ";", or a "by" that is not followed by a host name.
|
|
224
|
+
*
|
|
225
|
+
* @param {String} header Received header
|
|
226
|
+
* @returns {Object|false} { words, comments } of the from clause, or false
|
|
227
|
+
*/
|
|
228
|
+
const splitFromClause = header => {
|
|
229
|
+
let value = header.substring(header.indexOf(':') + 1);
|
|
230
|
+
|
|
231
|
+
let tokens = [];
|
|
232
|
+
// where the date-time part starts in `tokens`
|
|
233
|
+
let dateTimeStart = -1;
|
|
234
|
+
let i = 0;
|
|
235
|
+
while (i < value.length) {
|
|
236
|
+
let c = value.charAt(i);
|
|
237
|
+
|
|
238
|
+
if (/\s/.test(c)) {
|
|
239
|
+
i++;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (c === ';') {
|
|
244
|
+
// the date-time part. RFC 5322 section 3.6.7 has a single ";", in front of the
|
|
245
|
+
// date-time, so another one came from the HELO argument, and so may the first
|
|
246
|
+
// one. The rest of the header is still read to find such a ";"
|
|
247
|
+
if (dateTimeStart >= 0) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
dateTimeStart = tokens.length;
|
|
251
|
+
i++;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (c === ')' || c === '\\') {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (c === '(') {
|
|
260
|
+
// RFC 5322 comment, nested comments are part of it
|
|
261
|
+
let start = i;
|
|
262
|
+
let level = 0;
|
|
263
|
+
for (; i < value.length; i++) {
|
|
264
|
+
let cc = value.charAt(i);
|
|
265
|
+
if (cc === '\\') {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
if (cc === '(') {
|
|
269
|
+
level++;
|
|
270
|
+
} else if (cc === ')' && --level === 0) {
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (level) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
tokens.push({ type: 'comment', value: value.substring(start + 1, i) });
|
|
278
|
+
i++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (c === '"') {
|
|
283
|
+
let end = i + 1;
|
|
284
|
+
while (end < value.length && value.charAt(end) !== '"') {
|
|
285
|
+
// a quoted-pair keeps the next character
|
|
286
|
+
end += value.charAt(end) === '\\' ? 2 : 1;
|
|
287
|
+
}
|
|
288
|
+
if (end >= value.length) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
tokens.push({ type: 'quoted', value: value.substring(i, end + 1) });
|
|
292
|
+
i = end + 1;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
WORD.lastIndex = i;
|
|
297
|
+
let word = WORD.exec(value)[0];
|
|
298
|
+
tokens.push({ type: 'word', value: word });
|
|
299
|
+
i += word.length;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (dateTimeStart >= 0) {
|
|
303
|
+
tokens = tokens.slice(0, dateTimeStart);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (tokens[0]?.type !== 'word' || tokens[0].value.toLowerCase() !== 'from') {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let byPositions = [];
|
|
311
|
+
tokens.forEach((token, pos) => {
|
|
312
|
+
if (token.type === 'word' && token.value.toLowerCase() === 'by') {
|
|
313
|
+
byPositions.push(pos);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (byPositions.length !== 1 || tokens[byPositions[0] + 1]?.type !== 'word') {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let clause = tokens.slice(1, byPositions[0]);
|
|
322
|
+
if (clause.some(token => token.type === 'quoted')) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
words: clause.filter(token => token.type === 'word').map(token => token.value),
|
|
328
|
+
comments: clause.filter(token => token.type === 'comment').map(token => token.value)
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// splitFromClause results of parsed Received headers, so that getClientAddress and getClientHelo
|
|
333
|
+
// split each header once. Kept aside, the parsed header object is not modified
|
|
334
|
+
const fromClauseCache = new WeakMap();
|
|
335
|
+
|
|
336
|
+
// The split from clause of a parsed Received header (see splitFromClause), or false
|
|
337
|
+
const getFromClause = received => {
|
|
338
|
+
if (!received || typeof received !== 'object' || typeof received.full !== 'string') {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
let cached = fromClauseCache.get(received);
|
|
342
|
+
if (cached?.full !== received.full) {
|
|
343
|
+
cached = { full: received.full, clause: splitFromClause(received.full) };
|
|
344
|
+
fromClauseCache.set(received, cached);
|
|
345
|
+
}
|
|
346
|
+
return cached.clause;
|
|
347
|
+
};
|
|
211
348
|
|
|
212
349
|
/**
|
|
213
350
|
* Finds the IP address of the client in the from clause of a parsed Received header.
|
|
214
351
|
*
|
|
215
352
|
* 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])"
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
353
|
+
* of the from clause, "from helo.name (rdns.name [192.0.2.1])". The rest of the clause is the
|
|
354
|
+
* client's own claim: the HELO name, which can itself be an address literal or hold
|
|
355
|
+
* parentheses, and in Exim's format a "helo=" value inside the comment. Those are never used.
|
|
356
|
+
* Exim writes "from [192.0.2.1] (helo=...)" for a client without reverse DNS, where the literal
|
|
357
|
+
* in front of the comment is the address of the connection. With a "helo=" comment it is used
|
|
358
|
+
* unless a comment has a different address literal, otherwise only when no comment has an
|
|
359
|
+
* address literal of its own.
|
|
222
360
|
*
|
|
223
|
-
*
|
|
361
|
+
* Only an unambiguous address is returned. A header that can not be split into a from clause
|
|
362
|
+
* and a single "by" (see splitFromClause), a from clause with more than one address in its
|
|
363
|
+
* comments, or Exim's layout with a different address in a comment, returns false instead of
|
|
364
|
+
* a guess.
|
|
365
|
+
*
|
|
366
|
+
* @param {Object} received Parsed Received header, the result of parseReceived()
|
|
224
367
|
* @returns {String|false} IP address, without an "IPv6:" prefix, or false if none was found
|
|
225
368
|
*/
|
|
226
|
-
const getClientAddress =
|
|
227
|
-
|
|
369
|
+
const getClientAddress = received => {
|
|
370
|
+
let clause = getFromClause(received);
|
|
371
|
+
if (!clause) {
|
|
228
372
|
return false;
|
|
229
373
|
}
|
|
230
374
|
|
|
231
|
-
let
|
|
232
|
-
let
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
continue;
|
|
375
|
+
let addresses = new Set();
|
|
376
|
+
for (let comment of clause.comments) {
|
|
377
|
+
for (let match of comment.matchAll(ADDRESS_LITERAL)) {
|
|
378
|
+
if (!HELO_PREFIX.test(comment.substring(0, match.index)) && isIP(match[1])) {
|
|
379
|
+
addresses.add(match[1]);
|
|
380
|
+
}
|
|
238
381
|
}
|
|
239
|
-
|
|
240
|
-
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let valueMatch = clause.comments.length && clause.words.length && clause.words[0].match(/^\[(?:IPv6:)?([^\]\s]+)\](?::\d+)?$/i);
|
|
385
|
+
let valueAddress = valueMatch && isIP(valueMatch[1]) ? valueMatch[1] : false;
|
|
386
|
+
|
|
387
|
+
if (valueAddress && clause.comments.some(comment => HELO_KEYWORD.test(comment))) {
|
|
388
|
+
// Exim's layout for a client without reverse DNS, "from [192.0.2.1] (helo=name)". The
|
|
389
|
+
// literal in front of the comment is the address of the connection, and everything
|
|
390
|
+
// after "helo=" is the client's own text, which can add words, comments and address
|
|
391
|
+
// literals of its own. The same header can also come from Postfix, with
|
|
392
|
+
// "[192.0.2.1] (helo=name)" as the HELO argument and the TCP-info comment after it. So
|
|
393
|
+
// a different address in the comments may be the real one or a forged one, and the
|
|
394
|
+
// header is ambiguous
|
|
395
|
+
if (clause.words.length === 1 && (!addresses.size || (addresses.size === 1 && addresses.has(valueAddress)))) {
|
|
396
|
+
return valueAddress;
|
|
241
397
|
}
|
|
398
|
+
return false;
|
|
242
399
|
}
|
|
243
400
|
|
|
244
|
-
if (
|
|
245
|
-
return
|
|
401
|
+
if (addresses.size === 1) {
|
|
402
|
+
return addresses.values().next().value;
|
|
246
403
|
}
|
|
247
404
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return
|
|
405
|
+
if (addresses.size > 1) {
|
|
406
|
+
// the HELO name holds an address literal of its own, can not tell which one is real
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return clause.words.length === 1 && valueAddress;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// the HELO name in the TCP-info comment, "helo=name" as Exim writes it or "HELO name" as qmail does
|
|
414
|
+
const HELO_COMMENT = /(?:^|[\s(;,])(?:helo|ehlo)=([^\s()]+)|^(?:helo|ehlo)\s+([^\s()]+)$/i;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Finds the HELO name of the client in the from clause of a parsed Received header. Most servers
|
|
418
|
+
* write the HELO name as the from value, "from helo.name (rdns.name [192.0.2.1])", but Exim and
|
|
419
|
+
* qmail write the reverse DNS name there and the HELO name into a comment when the two differ,
|
|
420
|
+
* "from rdns.name ([192.0.2.1] helo=helo.name)" and "from rdns.name (HELO helo.name) (192.0.2.1)".
|
|
421
|
+
*
|
|
422
|
+
* A header that can not be split into a from clause and a single "by" (see splitFromClause)
|
|
423
|
+
* returns false, as the HELO name in it can not be told apart from the rest.
|
|
424
|
+
*
|
|
425
|
+
* @param {Object} received Parsed Received header, the result of parseReceived()
|
|
426
|
+
* @returns {String|false} HELO name or false if none was found
|
|
427
|
+
*/
|
|
428
|
+
const getClientHelo = received => {
|
|
429
|
+
let clause = getFromClause(received);
|
|
430
|
+
if (!clause) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
for (let comment of clause.comments) {
|
|
435
|
+
let match = comment.trim().match(HELO_COMMENT);
|
|
436
|
+
if (match) {
|
|
437
|
+
return match[1] || match[2];
|
|
438
|
+
}
|
|
251
439
|
}
|
|
252
440
|
|
|
253
|
-
|
|
441
|
+
let value = received.from?.value;
|
|
442
|
+
return (typeof value === 'string' && value.trim()) || false;
|
|
254
443
|
};
|
|
255
444
|
|
|
256
|
-
module.exports = { parseReceived, getClientAddress };
|
|
445
|
+
module.exports = { parseReceived, getClientAddress, getClientHelo };
|
package/lib/spf/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { spfVerify } = require('./spf-verify');
|
|
4
|
-
const { parseIp, isValidTargetName } = require('./syntax');
|
|
4
|
+
const { parseIp, isValidTargetName, stripRootDot } = require('./syntax');
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const dns = require('node:dns');
|
|
7
7
|
const libmime = require('libmime');
|
|
@@ -135,7 +135,8 @@ let dualStackResolver = async (resolver, domain, opts, state) => {
|
|
|
135
135
|
* @param {Boolean} [options.strict] - If true, follow RFC 7208 exactly (no dual-stack void optimization)
|
|
136
136
|
* @param {Object} [options.ctx] - Shared evaluation state, lax acceptances are added to ctx.warnings
|
|
137
137
|
* @param {Number} [options.deadline] - Timestamp after which no more queries are made
|
|
138
|
-
* @returns {Function} - Rate-limited resolver function with signature: (domain, type, opts) => Promise<Array
|
|
138
|
+
* @returns {Function} - Rate-limited resolver function with signature: (domain, type, opts) => Promise<Array>.
|
|
139
|
+
* If `opts.ignoreVoid` is set, the query counts toward the lookup limit but an empty answer is not a void lookup
|
|
139
140
|
*/
|
|
140
141
|
let limitedResolver = (resolver, maxResolveCount, maxVoidCount, ignoreFirst, options) => {
|
|
141
142
|
options = options || {};
|
|
@@ -148,8 +149,9 @@ let limitedResolver = (resolver, maxResolveCount, maxVoidCount, ignoreFirst, opt
|
|
|
148
149
|
let subResolveCounts = {};
|
|
149
150
|
let firstCounted = !ignoreFirst;
|
|
150
151
|
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
// 0 is a valid limit, the default is used only if the value is not a non-negative number
|
|
153
|
+
maxResolveCount = typeof maxResolveCount === 'number' && maxResolveCount >= 0 ? maxResolveCount : MAX_RESOLVE_COUNT;
|
|
154
|
+
maxVoidCount = typeof maxVoidCount === 'number' && maxVoidCount >= 0 ? maxVoidCount : MAX_VOID_COUNT;
|
|
153
155
|
|
|
154
156
|
let checkRfcVoidCount = () => {
|
|
155
157
|
if (rfcVoidCount > maxVoidCount && options.ctx && options.ctx.warnings) {
|
|
@@ -233,6 +235,10 @@ let limitedResolver = (resolver, maxResolveCount, maxVoidCount, ignoreFirst, opt
|
|
|
233
235
|
case 'ENOTFOUND': // Domain does not exist
|
|
234
236
|
case 'ENODATA': {
|
|
235
237
|
// Domain exists but has no records of this type
|
|
238
|
+
if (opts?.ignoreVoid) {
|
|
239
|
+
// the void limit counts terms, and the term decides with its own query
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
236
242
|
// Increment void lookup counter
|
|
237
243
|
voidCount++;
|
|
238
244
|
rfcVoidCount++;
|
|
@@ -369,6 +375,11 @@ const verify = async opts => {
|
|
|
369
375
|
|
|
370
376
|
mta = mta || os.hostname();
|
|
371
377
|
|
|
378
|
+
// the root dot is removed from the identities used for evaluation, macros and reporting
|
|
379
|
+
if (typeof helo === 'string') {
|
|
380
|
+
helo = stripRootDot(helo);
|
|
381
|
+
}
|
|
382
|
+
|
|
372
383
|
// a null reverse-path means that the HELO identity is checked (RFC 7208 section 2.4)
|
|
373
384
|
let nullSender = !sender;
|
|
374
385
|
|
|
@@ -394,6 +405,12 @@ const verify = async opts => {
|
|
|
394
405
|
}
|
|
395
406
|
atPos = sender.lastIndexOf('@');
|
|
396
407
|
|
|
408
|
+
let senderDomain = sender.substr(atPos + 1).trim();
|
|
409
|
+
let relativeDomain = stripRootDot(senderDomain);
|
|
410
|
+
if (relativeDomain !== senderDomain) {
|
|
411
|
+
sender = `${sender.substr(0, atPos + 1)}${relativeDomain}`;
|
|
412
|
+
}
|
|
413
|
+
|
|
397
414
|
let domain =
|
|
398
415
|
sender
|
|
399
416
|
.substr(atPos + 1)
|
package/lib/spf/macro.js
CHANGED
|
@@ -54,7 +54,7 @@ const macro = (input, values) => {
|
|
|
54
54
|
if (m === '%') {
|
|
55
55
|
// Lone % found
|
|
56
56
|
let err = new Error('Syntax error on parsing macro');
|
|
57
|
-
err.spfResult = { error: 'permerror',
|
|
57
|
+
err.spfResult = { error: 'permerror', text: `Unexpected % in macro` };
|
|
58
58
|
throw err;
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -143,7 +143,7 @@ const macro = (input, values) => {
|
|
|
143
143
|
|
|
144
144
|
default: {
|
|
145
145
|
let err = new Error('Syntax error on parsing macro');
|
|
146
|
-
err.spfResult = { error: 'permerror',
|
|
146
|
+
err.spfResult = { error: 'permerror', text: `Unknown macro letter "${macroChar}"` };
|
|
147
147
|
throw err;
|
|
148
148
|
}
|
|
149
149
|
}
|
|
@@ -184,7 +184,9 @@ const macro = (input, values) => {
|
|
|
184
184
|
delimiters = delimiters || '.';
|
|
185
185
|
|
|
186
186
|
if (reversed || nr || delimiters !== '.') {
|
|
187
|
-
|
|
187
|
+
// every delimiter is escaped, an unescaped "-" between two others would make a range
|
|
188
|
+
// in the character class, such as "+-." that also matches ","
|
|
189
|
+
curval = curval.split(new RegExp(`[${delimiters.replace(/./g, '\\$&')}]`));
|
|
188
190
|
|
|
189
191
|
if (reversed) {
|
|
190
192
|
curval = curval.reverse();
|
package/lib/spf/spf-verify.js
CHANGED
|
@@ -94,12 +94,13 @@ const parseCidrValue = (val, defaultValue, type) => {
|
|
|
94
94
|
* goes through the given resolver, so with the limited resolver every "ptr" term and every
|
|
95
95
|
* %{p} expansion counts toward the DNS lookup limit (RFC 7208 section 4.6.4).
|
|
96
96
|
*
|
|
97
|
+
* @param {Object} [ptrOpts] Options for the PTR query, `{ ignoreVoid: true }` if an empty answer is not a void lookup
|
|
97
98
|
* @returns {Array} Validated names, empty if there are none or the PTR lookup failed
|
|
98
99
|
*/
|
|
99
|
-
const getValidatedNames = async (addr, resolver, subResolver) => {
|
|
100
|
+
const getValidatedNames = async (addr, resolver, subResolver, ptrOpts) => {
|
|
100
101
|
let ptrValues;
|
|
101
102
|
try {
|
|
102
|
-
ptrValues = await resolver(getPtrHostname(addr), 'PTR');
|
|
103
|
+
ptrValues = await resolver(getPtrHostname(addr), 'PTR', ptrOpts);
|
|
103
104
|
} catch (err) {
|
|
104
105
|
if (isFatalError(err)) {
|
|
105
106
|
throw err;
|
|
@@ -195,9 +196,11 @@ const spfVerify = async (domain, opts) => {
|
|
|
195
196
|
let senderStr = (opts.sender || '').toString();
|
|
196
197
|
let nonAsciiLocalPart = /[^\x00-\x7F]/.test(senderStr.substr(0, Math.max(senderStr.lastIndexOf('@'), 0)));
|
|
197
198
|
|
|
198
|
-
// The macro values with %{p}, the validated domain name of the client IP (RFC 7208 section 7.3)
|
|
199
|
+
// The macro values with %{p}, the validated domain name of the client IP (RFC 7208 section 7.3).
|
|
200
|
+
// The PTR query counts toward the lookup limit but not as a void lookup, the void limit counts
|
|
201
|
+
// terms (RFC 7208 section 4.6.4) and the query of the term itself decides if the term was void
|
|
199
202
|
let withPtr = async (ptrResolver, subResolver) =>
|
|
200
|
-
Object.assign({}, macroValues, { p: selectPtrName(await getValidatedNames(addr, ptrResolver, subResolver), domain) });
|
|
203
|
+
Object.assign({}, macroValues, { p: selectPtrName(await getValidatedNames(addr, ptrResolver, subResolver, { ignoreVoid: true }), domain) });
|
|
201
204
|
|
|
202
205
|
/**
|
|
203
206
|
* Expands a domain-spec into a target name
|
|
@@ -643,7 +646,13 @@ const spfVerify = async (domain, opts) => {
|
|
|
643
646
|
return false;
|
|
644
647
|
}
|
|
645
648
|
|
|
646
|
-
|
|
649
|
+
let explanation = macro(text, usesMacro(text, 'p') ? await withPtr(expResolver, expResolver) : macroValues);
|
|
650
|
+
// the limit applies to the final, macro-expanded string, %{s}, %{l} and %{h} copy the identities as they are
|
|
651
|
+
if (/[^\x20-\x7E]/.test(explanation)) {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return explanation;
|
|
647
656
|
} catch (err) {
|
|
648
657
|
return false;
|
|
649
658
|
}
|
package/lib/spf/syntax.js
CHANGED
|
@@ -356,8 +356,13 @@ const validateTerms = terms => {
|
|
|
356
356
|
return false;
|
|
357
357
|
};
|
|
358
358
|
|
|
359
|
+
// A single trailing dot makes a name fully qualified, it is the same DNS name without it (RFC 7208
|
|
360
|
+
// section 4.3). Only a single one is removed, a zero-length label anywhere else is still malformed
|
|
361
|
+
const stripRootDot = name => (/[^.]\.$/.test(name) ? name.slice(0, -1) : name);
|
|
362
|
+
|
|
359
363
|
module.exports = {
|
|
360
364
|
parseIp,
|
|
365
|
+
stripRootDot,
|
|
361
366
|
ipInNetwork,
|
|
362
367
|
normalizeTargetName,
|
|
363
368
|
isValidTargetName,
|