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 +2 -2
- package/bin/mailauth.js +20 -2
- package/cli.md +5 -3
- package/lib/arc/index.js +0 -1
- package/lib/bimi/index.js +3 -2
- package/lib/commands/report.js +18 -1
- package/lib/commands/seal.js +14 -1
- package/lib/commands/spf.js +11 -19
- package/lib/commands/vmc.js +14 -1
- package/lib/dkim/body/relaxed.js +0 -24
- package/lib/dkim/dkim-verifier.js +1 -1
- package/lib/spf/index.js +52 -20
- package/lib/spf/spf-verify.js +87 -9
- package/lib/tools.js +18 -1
- package/licenses.txt +7 -7
- package/man/mailauth.1 +5 -2
- package/man/man.md +4 -1
- package/package.json +5 -5
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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');
|
package/lib/commands/report.js
CHANGED
|
@@ -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);
|
package/lib/commands/seal.js
CHANGED
|
@@ -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);
|
package/lib/commands/spf.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { spf } = require('../spf');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
package/lib/commands/vmc.js
CHANGED
|
@@ -18,7 +18,20 @@ const cmd = async argv => {
|
|
|
18
18
|
bimiData.status = { header: { d: argv.domain } };
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
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
|
|
package/lib/dkim/body/relaxed.js
CHANGED
|
@@ -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
|
-
*/
|
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 =
|
|
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
|
-
|
|
25
|
+
let voidCount = 0;
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// special condition to get the counter
|
|
29
|
-
return resolveCount;
|
|
30
|
-
}
|
|
27
|
+
let subResolveCounts = {};
|
|
28
|
+
let firstCounted = !ignoreFirst;
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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=
|
|
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:
|
|
165
|
-
count:
|
|
194
|
+
limit: verifyResolver.getResolveLimit(),
|
|
195
|
+
count: verifyResolver.getResolveCount(),
|
|
196
|
+
void: verifyResolver.getVoidCount(),
|
|
197
|
+
subqueries: verifyResolver.getSubResolveCounts()
|
|
166
198
|
};
|
|
167
199
|
}
|
|
168
200
|
|
package/lib/spf/spf-verify.js
CHANGED
|
@@ -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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
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
|
-
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" "
|
|
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
|
|
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
|
|
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
|
+
"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.
|
|
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": "
|
|
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.
|
|
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.
|
|
54
|
+
"nodemailer": "6.7.8",
|
|
55
55
|
"psl": "1.9.0",
|
|
56
56
|
"punycode": "2.1.1",
|
|
57
57
|
"yargs": "17.5.1"
|