mailauth 3.0.2 → 4.0.1

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/README.md CHANGED
@@ -48,7 +48,8 @@ Where
48
48
  - **selector** (_string_) ARC key selector
49
49
  - **privateKey** (_string_ or _buffer_) Private key for signing. Either an RSA or an Ed25519 key
50
50
  - **resolver** (_async function_) is an optional async function for DNS requests. Defaults to [dns.promises.resolve](https://nodejs.org/api/dns.html#dns_dnspromises_resolve_hostname_rrtype)
51
- - **maxResolveCount** (_number_ defaults to _50_) is the DNS lookup limit for SPF. [RFC7208](https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4) requires this limit to be 10. Mailauth is less strict and defaults to 50.
51
+ - **maxResolveCount** (_number_ defaults to _10_) is the DNS lookup limit for SPF. [RFC7208](https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4) requires this limit to be 10.
52
+ - **maxVoidCount** (_number_ defaults to _2_) is the DNS lookup limit for SPF that produce an empty result. [RFC7208](https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4) requires this limit to be 2.
52
53
 
53
54
  **Example**
54
55
 
@@ -403,7 +404,6 @@ The function returns a boolean. If it is `true`, then MX hostname is allowed to
403
404
 
404
405
  [OpenSPF test suite](http://www.openspf.org/Test_Suite) ([archive.org mirror](https://web.archive.org/web/20190130131432/http://www.openspf.org/Test_Suite)) with the following differences:
405
406
 
406
- - No PTR support in `mailauth`. All PTR related tests are ignored
407
407
  - Less strict whitespace checks (`mailauth` accepts multiple spaces between tags etc.)
408
408
  - Some macro tests are skipped (macro expansion is supported _in most parts_)
409
409
  - Some tests where the invalid component is listed after a matching part (mailauth processes from left to right and returns on the first match found)
package/bin/mailauth.js CHANGED
@@ -52,7 +52,13 @@ const argv = yargs(hideBin(process.argv))
52
52
  alias: 'x',
53
53
  type: 'number',
54
54
  description: 'Maximum allowed DNS lookups',
55
- default: 50
55
+ default: 10
56
+ })
57
+ .option('max-void-lookups', {
58
+ alias: 'z',
59
+ type: 'number',
60
+ description: 'Maximum allowed empty DNS lookups',
61
+ default: 2
56
62
  });
57
63
  yargs.positional('email', {
58
64
  describe: 'Path to the email message file in EML format. If not specified then content is read from stdin'
@@ -275,7 +281,13 @@ const argv = yargs(hideBin(process.argv))
275
281
  alias: 'x',
276
282
  type: 'number',
277
283
  description: 'Maximum allowed DNS lookups',
278
- default: 50
284
+ default: 10
285
+ })
286
+ .option('max-void-lookups', {
287
+ alias: 'z',
288
+ type: 'number',
289
+ description: 'Maximum allowed empty DNS lookups',
290
+ default: 2
279
291
  });
280
292
  },
281
293
  argv => {
@@ -312,6 +324,12 @@ const argv = yargs(hideBin(process.argv))
312
324
  type: 'string',
313
325
  description: 'Sending domain to validate',
314
326
  demandOption: false
327
+ })
328
+ .option('date', {
329
+ alias: 't',
330
+ type: 'string',
331
+ description: 'ISO formatted timestamp for the certificate expiration checks',
332
+ demandOption: false
315
333
  });
316
334
  },
317
335
  argv => {
package/cli.md CHANGED
@@ -59,7 +59,8 @@ Where
59
59
  - `--mta hostname` or `-m hostname` is the server hostname doing the validation checks. Defaults to `os.hostname()`
60
60
  - `--dns-cache /path/to/dns.json` or `-n path` is the path to a file with cached DNS query responses. If this file is provided then no actual DNS requests are performed, only cached values from this file are used.
61
61
  - `--verbose` or `-v` if this flag is set then mailauth writes some debugging info to standard error
62
- - `--max-lookups nr` or `-x nr` defines the allowed DNS lookup limit for SPF checks. Defaults to 50.
62
+ - `--max-lookups nr` or `-x nr` defines the allowed DNS lookup limit for SPF checks. Defaults to 10.
63
+ - `--max-void-lookups nr` or `-z nr` defines the allowed DNS lookup limit for SPF checks. Defaults to 2.
63
64
 
64
65
  **Example**
65
66
 
@@ -187,14 +188,15 @@ Where
187
188
  - `--dns-cache /path/to/dns.json` or `-n path` is the path to a file with cached DNS query responses. If this file is provided then no actual DNS requests are performed, only cached values from this file are used.
188
189
  - `--verbose` or `-v` if this flag is set then mailauth writes some debugging info to standard error
189
190
  - `--headers-only` or `-o` If set return SPF authentication header only. Default is to return a JSON structure.
190
- - `--max-lookups nr` or `-x nr` defines the allowed DNS lookup limit for SPF checks. Defaults to 50.
191
+ - `--max-lookups nr` or `-x nr` defines the allowed DNS lookup limit for SPF checks. Defaults to 10.
192
+ - `--max-void-lookups nr` or `-z nr` defines the allowed DNS lookup limit for SPF checks. Defaults to 2.
191
193
 
192
194
  **Example**
193
195
 
194
196
  ```
195
197
  $ mailauth spf --verbose -f andris@wildduck.email -i 217.146.76.20
196
198
  Checking SPF for andris@wildduck.email
197
- Maximum DNS lookups: 50
199
+ Maximum DNS lookups: 10
198
200
  --------
199
201
  DNS query for TXT wildduck.email: [["v=spf1 mx a -all"]]
200
202
  DNS query for MX wildduck.email: [{"exchange":"mail.wildduck.email","priority":1}]
package/lib/arc/index.js CHANGED
@@ -351,7 +351,6 @@ const arc = async (data, opts) => {
351
351
  if (result.authenticationResults.dkim && result.authenticationResults.dkim.length) {
352
352
  result.authenticationResults.dkim = result.authenticationResults.dkim.map(entry => {
353
353
  let result = entry.value;
354
-
355
354
  delete entry.value;
356
355
  return Object.assign({ result }, entry);
357
356
  });
package/lib/bimi/index.js CHANGED
@@ -240,7 +240,8 @@ const downloadPromise = (url, cachedFile) => {
240
240
  });
241
241
  };
242
242
 
243
- const validateVMC = async bimiData => {
243
+ const validateVMC = async (bimiData, opts) => {
244
+ opts = opts || {};
244
245
  if (!bimiData) {
245
246
  return false;
246
247
  }
@@ -302,7 +303,7 @@ const validateVMC = async bimiData => {
302
303
 
303
304
  if (authorityValue) {
304
305
  try {
305
- let vmcData = await vmc(authorityValue);
306
+ let vmcData = await vmc(authorityValue, opts);
306
307
 
307
308
  if (!vmcData.logoFile) {
308
309
  let error = new Error('VMC does not contain a log file');
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { authenticate } = require('../mailauth');
4
4
  const fs = require('fs');
5
+ const { resolve } = require('dns').promises;
5
6
 
6
7
  const cmd = async argv => {
7
8
  let source = argv.email;
@@ -35,6 +36,10 @@ const cmd = async argv => {
35
36
  opts.maxResolveCount = argv.maxLookups;
36
37
  }
37
38
 
39
+ if (argv.maxVoidLookups) {
40
+ opts.maxVoidCount = argv.maxVoidLookups;
41
+ }
42
+
38
43
  for (let key of ['mta', 'helo', 'sender']) {
39
44
  if (argv[key]) {
40
45
  opts[key] = argv[key];
@@ -48,7 +53,7 @@ const cmd = async argv => {
48
53
  let match = dnsCache?.[name]?.[rr];
49
54
 
50
55
  if (argv.verbose) {
51
- console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
56
+ console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'} (using cache)`);
52
57
  }
53
58
 
54
59
  if (!match) {
@@ -59,6 +64,18 @@ const cmd = async argv => {
59
64
 
60
65
  return match;
61
66
  };
67
+ } else if (argv.verbose) {
68
+ opts.resolver = async (name, rr) => {
69
+ let match;
70
+ try {
71
+ match = await resolve(name, rr);
72
+ console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
73
+ return match;
74
+ } catch (err) {
75
+ console.error(`DNS query for ${rr} ${name}: ${err.message}${err.code ? ` [${err.code}]` : ''}`);
76
+ throw err;
77
+ }
78
+ };
62
79
  }
63
80
 
64
81
  let result = await authenticate(stream, opts);
@@ -3,6 +3,7 @@
3
3
  const { authenticate } = require('../mailauth');
4
4
  const fs = require('fs');
5
5
  const { GathererStream } = require('../gatherer-stream');
6
+ const { resolve } = require('dns').promises;
6
7
 
7
8
  const cmd = async argv => {
8
9
  let source = argv.email;
@@ -82,7 +83,7 @@ const cmd = async argv => {
82
83
  let match = dnsCache?.[name]?.[rr];
83
84
 
84
85
  if (argv.verbose) {
85
- console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
86
+ console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'} (using cache)`);
86
87
  }
87
88
 
88
89
  if (!match) {
@@ -93,6 +94,18 @@ const cmd = async argv => {
93
94
 
94
95
  return match;
95
96
  };
97
+ } else if (argv.verbose) {
98
+ opts.resolver = async (name, rr) => {
99
+ let match;
100
+ try {
101
+ match = await resolve(name, rr);
102
+ console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
103
+ return match;
104
+ } catch (err) {
105
+ console.error(`DNS query for ${rr} ${name}: ${err.message}${err.code ? ` [${err.code}]` : ''}`);
106
+ throw err;
107
+ }
108
+ };
96
109
  }
97
110
 
98
111
  let result = await authenticate(gatherer, opts);
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { spf } = require('../spf');
4
4
  const fs = require('fs');
5
- const dns = require('dns').promises;
5
+ const { resolve } = require('dns').promises;
6
6
 
7
7
  const cmd = async argv => {
8
8
  let address = argv.sender;
@@ -28,6 +28,10 @@ const cmd = async argv => {
28
28
  opts.maxResolveCount = argv.maxLookups;
29
29
  }
30
30
 
31
+ if (argv.maxVoidLookups) {
32
+ opts.maxVoidCount = argv.maxVoidLookups;
33
+ }
34
+
31
35
  for (let key of ['sender', 'helo', 'mta']) {
32
36
  if (argv[key]) {
33
37
  opts[key] = argv[key];
@@ -41,7 +45,7 @@ const cmd = async argv => {
41
45
  let match = dnsCache?.[name]?.[rr];
42
46
 
43
47
  if (argv.verbose) {
44
- console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
48
+ console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'} (using cache)`);
45
49
  }
46
50
 
47
51
  if (!match) {
@@ -52,29 +56,17 @@ const cmd = async argv => {
52
56
 
53
57
  return match;
54
58
  };
55
- } else {
59
+ } else if (argv.verbose) {
56
60
  opts.resolver = async (name, rr) => {
57
61
  let match;
58
62
  try {
59
- match = await dns.resolve(name, rr);
60
- } catch (err) {
61
- if (argv.verbose) {
62
- console.error(`DNS query for ${rr} ${name}: ${err.code || err.message}`);
63
- }
64
- throw err;
65
- }
66
-
67
- if (argv.verbose) {
63
+ match = await resolve(name, rr);
68
64
  console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
69
- }
70
-
71
- if (!match) {
72
- let err = new Error('Error');
73
- err.code = 'ENOTFOUND';
65
+ return match;
66
+ } catch (err) {
67
+ console.error(`DNS query for ${rr} ${name}: ${err.message}${err.code ? ` [${err.code}]` : ''}`);
74
68
  throw err;
75
69
  }
76
-
77
- return match;
78
70
  };
79
71
  }
80
72
 
@@ -18,7 +18,20 @@ const cmd = async argv => {
18
18
  bimiData.status = { header: { d: argv.domain } };
19
19
  }
20
20
 
21
- const result = await validateVMC(bimiData);
21
+ let opts = {};
22
+ if (argv.date) {
23
+ let date = new Date(argv.date);
24
+ if (date.toString() !== 'Invalid Date') {
25
+ opts.now = date;
26
+ if (argv.verbose) {
27
+ console.error(`Setting date to: ${argv.date}`);
28
+ }
29
+ } else if (argv.verbose) {
30
+ console.error(`Invalid date argument: ${argv.date}`);
31
+ }
32
+ }
33
+
34
+ const result = await validateVMC(bimiData, opts);
22
35
  process.stdout.write(JSON.stringify(result.authority, false, 2) + '\n');
23
36
  };
24
37
 
@@ -266,27 +266,3 @@ class RelaxedHash {
266
266
  }
267
267
 
268
268
  module.exports = { RelaxedHash };
269
-
270
- /*
271
- let fs = require('fs');
272
-
273
- const getBody = message => {
274
- message = message.toString('binary');
275
- let match = message.match(/\r?\n\r?\n/);
276
- if (match) {
277
- message = message.substr(match.index + match[0].length);
278
- }
279
- return Buffer.from(message, 'binary');
280
- };
281
-
282
- let s = fs.readFileSync(process.argv[2]);
283
-
284
- let k = new RelaxedHash('rsa-sha256', -1);
285
-
286
- for (let byte of getBody(s)) {
287
- k.update(Buffer.from([byte]));
288
- }
289
-
290
- console.error(k.digest('base64'));
291
- console.error(k.byteLength, k.bodyHashedBytes);
292
- */
@@ -225,7 +225,7 @@ class DkimVerifier extends MessageParser {
225
225
  ? 'pass'
226
226
  : 'fail';
227
227
 
228
- if (status === 'fail') {
228
+ if (status.result === 'fail') {
229
229
  status.comment = 'bad signature';
230
230
  }
231
231
  } catch (err) {
package/lib/spf/index.js CHANGED
@@ -8,7 +8,8 @@ const Joi = require('joi');
8
8
  const domainSchema = Joi.string().domain({ allowUnicode: false, tlds: false });
9
9
  const { formatAuthHeaderRow, escapeCommentValue } = require('../tools');
10
10
 
11
- const MAX_RESOLVE_COUNT = 50;
11
+ const MAX_RESOLVE_COUNT = 10;
12
+ const MAX_VOID_COUNT = 2;
12
13
 
13
14
  const formatHeaders = result => {
14
15
  let header = `Received-SPF: ${result.status.result}${result.status.comment ? ` (${escapeCommentValue(result.status.comment)})` : ''} client-ip=${
@@ -19,23 +20,24 @@ const formatHeaders = result => {
19
20
  };
20
21
 
21
22
  // DNS resolver method
22
- let limitedResolver = (resolver, maxResolveCount) => {
23
+ let limitedResolver = (resolver, maxResolveCount, maxVoidCount, ignoreFirst) => {
23
24
  let resolveCount = 0;
24
- maxResolveCount = maxResolveCount || MAX_RESOLVE_COUNT;
25
+ let voidCount = 0;
25
26
 
26
- return async (domain, type) => {
27
- if (!domain && type === 'resolveCount') {
28
- // special condition to get the counter
29
- return resolveCount;
30
- }
27
+ let subResolveCounts = {};
28
+ let firstCounted = !ignoreFirst;
31
29
 
32
- if (!domain && type === 'resolveLimit') {
33
- // special condition to get the limit
34
- return maxResolveCount;
35
- }
30
+ maxResolveCount = maxResolveCount || MAX_RESOLVE_COUNT;
31
+ maxVoidCount = maxVoidCount || MAX_VOID_COUNT;
36
32
 
33
+ let resolverFunc = async (domain, type) => {
37
34
  // do not allow to make more that MAX_RESOLVE_COUNT DNS requests per SPF check
38
- resolveCount++;
35
+
36
+ if (firstCounted) {
37
+ resolveCount++;
38
+ } else {
39
+ firstCounted = true;
40
+ }
39
41
 
40
42
  if (resolveCount > maxResolveCount) {
41
43
  let error = new Error('Too many DNS requests');
@@ -65,8 +67,17 @@ let limitedResolver = (resolver, maxResolveCount) => {
65
67
  } catch (err) {
66
68
  switch (err.code) {
67
69
  case 'ENOTFOUND':
68
- case 'ENODATA':
70
+ case 'ENODATA': {
71
+ voidCount++;
72
+ if (voidCount > maxVoidCount) {
73
+ err.spfResult = {
74
+ error: 'permerror',
75
+ text: 'Too many void DNS results'
76
+ };
77
+ throw err;
78
+ }
69
79
  return [];
80
+ }
70
81
 
71
82
  case 'ETIMEOUT':
72
83
  err.spfResult = {
@@ -80,6 +91,21 @@ let limitedResolver = (resolver, maxResolveCount) => {
80
91
  }
81
92
  }
82
93
  };
94
+
95
+ resolverFunc.updateSubQueries = (type, count) => {
96
+ if (!subResolveCounts[type]) {
97
+ subResolveCounts[type] = count;
98
+ } else {
99
+ subResolveCounts[type] += count;
100
+ }
101
+ };
102
+
103
+ resolverFunc.getResolveCount = () => resolveCount;
104
+ resolverFunc.getResolveLimit = () => maxResolveCount;
105
+ resolverFunc.getSubResolveCounts = () => subResolveCounts;
106
+ resolverFunc.getVoidCount = () => voidCount;
107
+
108
+ return resolverFunc;
83
109
  };
84
110
 
85
111
  /**
@@ -89,10 +115,11 @@ let limitedResolver = (resolver, maxResolveCount) => {
89
115
  * @param {String} opts.ip Client IP address
90
116
  * @param {String} opts.helo Client EHLO/HELO hostname
91
117
  * @param {String} [opts.mta] Hostname of the MTA or MX server that processes the message
92
- * @param {String} [opts.maxResolveCount=50] Maximum DNS lookups allowed
118
+ * @param {String} [opts.maxResolveCount=10] Maximum DNS lookups allowed
119
+ * @param {String} [opts.maxVoidCount=2] Maximum empty DNS lookups allowed
93
120
  */
94
121
  const verify = async opts => {
95
- let { sender, ip, helo, mta, maxResolveCount, resolver } = opts || {};
122
+ let { sender, ip, helo, mta, maxResolveCount, maxVoidCount, resolver } = opts || {};
96
123
 
97
124
  mta = mta || os.hostname();
98
125
 
@@ -125,7 +152,7 @@ const verify = async opts => {
125
152
  }
126
153
  };
127
154
 
128
- let verifyResolver = limitedResolver(resolver, maxResolveCount);
155
+ let verifyResolver = limitedResolver(resolver, maxResolveCount, maxVoidCount, true);
129
156
 
130
157
  let result;
131
158
  try {
@@ -146,7 +173,10 @@ const verify = async opts => {
146
173
  helo,
147
174
 
148
175
  // generate DNS handler
149
- resolver: verifyResolver
176
+ resolver: verifyResolver,
177
+
178
+ // allow to create sub resolvers
179
+ createSubResolver: () => limitedResolver(resolver, maxResolveCount, maxVoidCount)
150
180
  });
151
181
  } catch (err) {
152
182
  if (err.spfResult) {
@@ -161,8 +191,10 @@ const verify = async opts => {
161
191
 
162
192
  if (result && typeof result === 'object') {
163
193
  result.lookups = {
164
- limit: await verifyResolver(false, 'resolveLimit'),
165
- count: await verifyResolver(false, 'resolveCount')
194
+ limit: verifyResolver.getResolveLimit(),
195
+ count: verifyResolver.getResolveCount(),
196
+ void: verifyResolver.getVoidCount(),
197
+ subqueries: verifyResolver.getSubResolveCounts()
166
198
  };
167
199
  }
168
200
 
@@ -5,6 +5,9 @@ const net = require('net');
5
5
  const macro = require('./macro');
6
6
  const dns = require('dns').promises;
7
7
  const ipaddr = require('ipaddr.js');
8
+ const { getPtrHostname, formatDomain } = require('../tools');
9
+
10
+ const LIMIT_PTR_RESOLVE_RECORDS = 10;
8
11
 
9
12
  const matchIp = (addr, range) => {
10
13
  if (/\/\d+$/.test(range)) {
@@ -357,18 +360,27 @@ const spfVerify = async (domain, opts) => {
357
360
 
358
361
  let mxList = await resolver(mxDomain, 'MX');
359
362
  if (mxList) {
360
- mxList = mxList.sort((a, b) => a.priority - b.priority);
361
- for (let mx of mxList) {
362
- if (mx.exchange) {
363
- let responses = await resolver(mx.exchange, net.isIPv6(opts.ip) ? 'AAAA' : 'A');
364
- if (responses) {
365
- for (let a of responses) {
366
- if (matchIp(addr, a + cidr)) {
367
- return { type, val: mx.exchange, qualifier };
363
+ // MX resolver has separate counter
364
+ let subResolver = typeof opts.createSubResolver === 'function' ? opts.createSubResolver() : resolver;
365
+ try {
366
+ mxList = mxList.sort((a, b) => a.priority - b.priority);
367
+ for (let mx of mxList) {
368
+ if (mx.exchange) {
369
+ let responses = await subResolver(mx.exchange, net.isIPv6(opts.ip) ? 'AAAA' : 'A');
370
+ if (responses) {
371
+ for (let a of responses) {
372
+ if (matchIp(addr, a + cidr)) {
373
+ return { type, val: mx.exchange, qualifier };
374
+ }
368
375
  }
369
376
  }
370
377
  }
371
378
  }
379
+ } finally {
380
+ if (typeof resolver.updateSubQueries === 'function') {
381
+ resolver.updateSubQueries('mx', subResolver.getResolveCount());
382
+ resolver.updateSubQueries('mx:void', subResolver.getVoidCount());
383
+ }
372
384
  }
373
385
  }
374
386
  }
@@ -391,7 +403,73 @@ const spfVerify = async (domain, opts) => {
391
403
  break;
392
404
 
393
405
  case 'ptr':
394
- // ignore, not supported
406
+ {
407
+ let { cidr4, cidr6 } = parseCidrValue(val, false, type);
408
+ if (cidr4 || cidr6) {
409
+ let err = new Error('SPF failure');
410
+ err.spfResult = { error: 'permerror', text: `invalid domain-spec definition: ${val}` };
411
+ throw err;
412
+ }
413
+
414
+ let ptrDomain;
415
+ if (val) {
416
+ ptrDomain = macro(val, opts);
417
+ } else {
418
+ ptrDomain = macro('%{d}', opts);
419
+ }
420
+ ptrDomain = formatDomain(ptrDomain);
421
+
422
+ // Step 1. Resolve PTR hostnames
423
+ let ptrValues;
424
+ if (opts._resolvedPtr) {
425
+ ptrValues = opts._resolvedPtr;
426
+ } else {
427
+ let responses = await resolver(getPtrHostname(addr), 'PTR');
428
+ opts._resolvedPtr = ptrValues = responses && responses.length ? responses : [];
429
+ }
430
+
431
+ // PTR resolver has separate counter
432
+ let subResolver = typeof opts.createSubResolver === 'function' ? opts.createSubResolver() : resolver;
433
+
434
+ let resolvers = [];
435
+ for (let ptrValue of ptrValues) {
436
+ if (resolvers.length < LIMIT_PTR_RESOLVE_RECORDS) {
437
+ // resolve up to 10 PTR A/AAAA records
438
+ // https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4
439
+ resolvers.push(subResolver(ptrValue, net.isIPv6(opts.ip) ? 'AAAA' : 'A'));
440
+ }
441
+ }
442
+
443
+ // Step 2. Validate PTR hostnames by reverse resolving these
444
+ let validatedPtrRecords = [];
445
+ let results = await Promise.allSettled(resolvers);
446
+
447
+ if (typeof resolver.updateSubQueries === 'function') {
448
+ resolver.updateSubQueries('ptr', subResolver.getResolveCount());
449
+ resolver.updateSubQueries('ptr:void', subResolver.getVoidCount());
450
+ }
451
+
452
+ for (let i = 0; i < results.length; i++) {
453
+ let result = results[i];
454
+ let ptrHostname = ptrValues[i];
455
+ if (
456
+ result.status === 'fulfilled' &&
457
+ Array.isArray(result.value) &&
458
+ result.value.map(val => ipaddr.parse(val).toNormalizedString()).includes(addr.toNormalizedString())
459
+ ) {
460
+ validatedPtrRecords.push(ptrHostname);
461
+ }
462
+ }
463
+
464
+ // Step 3. Check subdomain alignment
465
+ for (let ptrRecord of validatedPtrRecords) {
466
+ let formattedPtrRecord = formatDomain(ptrRecord);
467
+
468
+ if (formattedPtrRecord === ptrDomain || formattedPtrRecord.substr(-(ptrDomain.length + 1)) === `.${ptrDomain}`) {
469
+ return { type, val: ptrRecord, qualifier };
470
+ }
471
+ }
472
+ }
395
473
  break;
396
474
  }
397
475
  }
package/lib/tools.js CHANGED
@@ -470,6 +470,21 @@ const validateAlgorithm = (algorithm, strict) => {
470
470
  }
471
471
  };
472
472
 
473
+ const getPtrHostname = parsedAddr => {
474
+ let bytes = parsedAddr.toByteArray();
475
+ if (bytes.length === 4) {
476
+ return `${bytes
477
+ .map(a => a.toString(10))
478
+ .reverse()
479
+ .join('.')}.in-addr.arpa`;
480
+ } else {
481
+ return `${bytes
482
+ .flatMap(a => a.toString(16).padStart(2, '0').split(''))
483
+ .reverse()
484
+ .join('.')}.ip6.arpa`;
485
+ }
486
+ };
487
+
473
488
  module.exports = {
474
489
  writeToStream,
475
490
  parseHeaders,
@@ -491,5 +506,7 @@ module.exports = {
491
506
  getAlignment,
492
507
 
493
508
  formatRelaxedLine,
494
- formatDomain
509
+ formatDomain,
510
+
511
+ getPtrHostname
495
512
  };
package/licenses.txt CHANGED
@@ -1,12 +1,12 @@
1
1
  name license type link installed version author
2
2
  ---- ------------ ---- ----------------- ------
3
- @postalsys/vmc MIT https://registry.npmjs.org/@postalsys/vmc/-/vmc-1.0.4.tgz 1.0.4 Postal Systems OÜ
4
- fast-xml-parser MIT git+https://github.com/NaturalIntelligence/fast-xml-parser.git 4.0.9 Amit Gupta https://amitkumargupta.work/
5
- ipaddr.js MIT git://github.com/whitequark/ipaddr.js.git 2.0.1 whitequark whitequark@whitequark.org
6
- joi BSD-3-Clause git://github.com/sideway/joi.git 17.6.0
7
- libmime MIT git://github.com/andris9/libmime.git 5.1.0 Andris Reinman andris@kreata.ee
3
+ @postalsys/vmc MIT https://registry.npmjs.org/@postalsys/vmc/-/vmc-1.0.5.tgz 1.0.5 Postal Systems OÜ
4
+ fast-xml-parser MIT git+https://github.com/NaturalIntelligence/fast-xml-parser.git 4.0.9 Amit Gupta (https://amitkumargupta.work/)
5
+ ipaddr.js MIT git://github.com/whitequark/ipaddr.js.git 2.0.1 whitequark <whitequark@whitequark.org>
6
+ joi BSD-3-Clause git://github.com/sideway/joi.git 17.6.0 n/a
7
+ libmime MIT git://github.com/andris9/libmime.git 5.1.0 Andris Reinman <andris@kreata.ee>
8
8
  node-forge (BSD-3-Clause OR GPL-2.0) git+https://github.com/digitalbazaar/forge.git 1.3.1 Digital Bazaar, Inc. support@digitalbazaar.com http://digitalbazaar.com/
9
9
  nodemailer MIT git+https://github.com/nodemailer/nodemailer.git 6.7.7 Andris Reinman
10
- psl MIT git+ssh://git@github.com/lupomontero/psl.git 1.9.0 Lupo Montero lupomontero@gmail.com https://lupomontero.com/
10
+ psl MIT git+ssh://git@github.com/lupomontero/psl.git 1.9.0 Lupo Montero <lupomontero@gmail.com> (https://lupomontero.com/)
11
11
  punycode MIT git+https://github.com/bestiejs/punycode.js.git 2.1.1 Mathias Bynens https://mathiasbynens.be/
12
- yargs MIT git+https://github.com/yargs/yargs.git 17.5.1
12
+ yargs MIT git+https://github.com/yargs/yargs.git 17.5.1 n/a
package/man/mailauth.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH "MAILAUTH" "1" "July 2022" "v3.0.2" "Mailauth Help"
1
+ .TH "MAILAUTH" "1" "August 2022" "v4.0.1" "Mailauth Help"
2
2
  .SH "NAME"
3
3
  \fBmailauth\fR
4
4
  .QP
@@ -107,7 +107,10 @@ Colon separated list of header field names to sign\. (\fBsign\fP, \fBseal\fP)
107
107
  Return signing headers only\. By default, the entire message is printed to the console\. (\fBsign\fP, \fBseal\fP, \fBspf\fP)
108
108
  .IP \(bu 2
109
109
  \fB\-\-max\-lookups\fP, \fB\-x\fP
110
- How many DNS lookups allowed for SPF validation\. Defaults to 50\. (\fBreport\fP, \fBspf\fP)
110
+ How many DNS lookups allowed for SPF validation\. Defaults to 10\. (\fBreport\fP, \fBspf\fP)
111
+ .IP \(bu 2
112
+ \fB\-\-max\-void\-lookups\fP, \fB\-z\fP
113
+ How many empty DNS lookups allowed for SPF validation\. Defaults to 2\. (\fBreport\fP, \fBspf\fP)
111
114
 
112
115
  .RE
113
116
  .SH DNS CACHE
package/man/man.md CHANGED
@@ -103,7 +103,10 @@ content is read from standard input.
103
103
  Return signing headers only. By default, the entire message is printed to the console. (`sign`, `seal`, `spf`)
104
104
 
105
105
  - `--max-lookups`, `-x`
106
- How many DNS lookups allowed for SPF validation. Defaults to 50. (`report`, `spf`)
106
+ How many DNS lookups allowed for SPF validation. Defaults to 10. (`report`, `spf`)
107
+
108
+ - `--max-void-lookups`, `-z`
109
+ How many empty DNS lookups allowed for SPF validation. Defaults to 2. (`report`, `spf`)
107
110
 
108
111
  ## DNS CACHE
109
112
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailauth",
3
- "version": "3.0.2",
3
+ "version": "4.0.1",
4
4
  "description": "Email authentication library for Node.js",
5
5
  "main": "lib/mailauth.js",
6
6
  "scripts": {
@@ -33,11 +33,11 @@
33
33
  "homepage": "https://github.com/postalsys/mailauth",
34
34
  "devDependencies": {
35
35
  "chai": "4.3.6",
36
- "eslint": "8.19.0",
36
+ "eslint": "8.22.0",
37
37
  "eslint-config-nodemailer": "1.2.0",
38
38
  "eslint-config-prettier": "8.5.0",
39
39
  "js-yaml": "4.1.0",
40
- "license-report": "5.0.2",
40
+ "license-report": "6.0.0",
41
41
  "marked": "0.7.0",
42
42
  "marked-man": "0.7.0",
43
43
  "mbox-reader": "1.1.5",
@@ -45,13 +45,13 @@
45
45
  "pkg": "5.8.0"
46
46
  },
47
47
  "dependencies": {
48
- "@postalsys/vmc": "1.0.4",
48
+ "@postalsys/vmc": "1.0.6",
49
49
  "fast-xml-parser": "4.0.9",
50
50
  "ipaddr.js": "2.0.1",
51
51
  "joi": "17.6.0",
52
52
  "libmime": "5.1.0",
53
53
  "node-forge": "1.3.1",
54
- "nodemailer": "6.7.7",
54
+ "nodemailer": "6.7.8",
55
55
  "psl": "1.9.0",
56
56
  "punycode": "2.1.1",
57
57
  "yargs": "17.5.1"