mailauth 4.1.0 → 4.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/README.md +38 -3
- package/lib/dmarc/get-dmarc-record.js +75 -0
- package/lib/dmarc/verify.js +1 -67
- package/lib/mta-sts.js +23 -5
- package/licenses.txt +11 -12
- package/man/mailauth.1 +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -294,6 +294,41 @@ process.stdout.write(headers); // authentication results
|
|
|
294
294
|
process.stdout.write(message);
|
|
295
295
|
```
|
|
296
296
|
|
|
297
|
+
## DMARC
|
|
298
|
+
|
|
299
|
+
DMARC is verified as part of the authentication process and even as the `dmarc` handler is exported, it requires input from previous steps.
|
|
300
|
+
|
|
301
|
+
### Helpers
|
|
302
|
+
|
|
303
|
+
#### getDmarcRecord(domain [,resolver])
|
|
304
|
+
|
|
305
|
+
Returns parsed DMARC DNS record for a domain or a subdomain or `false` is no record exists.
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
const getDmarcRecord = require('mailauth/lib/dmarc/get-dmarc-record');
|
|
309
|
+
const dmarcRecord = await getDmarcRecord("ethereal.email");
|
|
310
|
+
console.log(dmarcRecord);
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Output**
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
{
|
|
317
|
+
v: 'DMARC1',
|
|
318
|
+
p: 'none',
|
|
319
|
+
pct: 100,
|
|
320
|
+
rua: 'mailto:re+joqy8fpatm3@dmarc.postmarkapp.com',
|
|
321
|
+
sp: 'none',
|
|
322
|
+
aspf: 'r',
|
|
323
|
+
rr: 'v=DMARC1; p=none; pct=100; rua=mailto:re+joqy8fpatm3@dmarc.postmarkapp.com; sp=none; aspf=r;',
|
|
324
|
+
isOrgRecord: false
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`isOrgRecord` is `true` for sudomains, where organizational domain's DMARC policy applies, so use the `sp`, not `p` policy.
|
|
329
|
+
|
|
330
|
+
Optionally set `resolver` argument with custom resolver (uses `dns.resolve` by default).
|
|
331
|
+
|
|
297
332
|
## BIMI
|
|
298
333
|
|
|
299
334
|
Brand Indicators for Message Identification (BIMI) support is based on [draft-blank-ietf-bimi-01](https://tools.ietf.org/html/draft-blank-ietf-bimi-01).
|
|
@@ -349,7 +384,7 @@ if (policy.mode === 'enforce') {
|
|
|
349
384
|
// must use TLS
|
|
350
385
|
}
|
|
351
386
|
|
|
352
|
-
if (policy.mx && !policyMatch) {
|
|
387
|
+
if (policy.mx && !policyMatch.valid) {
|
|
353
388
|
// can't connect, unlisted MX
|
|
354
389
|
}
|
|
355
390
|
```
|
|
@@ -386,7 +421,7 @@ The function returns an object with the following properties:
|
|
|
386
421
|
Check if a resolved MX hostname is valid by MTA-STS policy or not.
|
|
387
422
|
|
|
388
423
|
```
|
|
389
|
-
validateMx(mx, policy) ->
|
|
424
|
+
validateMx(mx, policy) -> Object
|
|
390
425
|
```
|
|
391
426
|
|
|
392
427
|
Where
|
|
@@ -394,7 +429,7 @@ Where
|
|
|
394
429
|
- **mx** is the resolved MX hostname (eg. "gmail-smtp-in.l.google.com")
|
|
395
430
|
- **policy** is the policy object returned by `getPolicy()`
|
|
396
431
|
|
|
397
|
-
The function returns
|
|
432
|
+
The function returns an object. If `{valid}` is `true`, then MX hostname is allowed to be used.
|
|
398
433
|
|
|
399
434
|
## Testing
|
|
400
435
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const psl = require('psl');
|
|
4
|
+
const dns = require('dns').promises;
|
|
5
|
+
|
|
6
|
+
const resolveTxt = async (domain, resolver) => {
|
|
7
|
+
try {
|
|
8
|
+
let txt = await resolver(`_dmarc.${domain}`, 'TXT');
|
|
9
|
+
if (!txt || !txt.length) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
txt = txt.map(row => row.join('').trim()).filter(row => /^v=DMARC1\b/i.test(row));
|
|
14
|
+
|
|
15
|
+
if (txt.length !== 1) {
|
|
16
|
+
//no records or multiple records yield in no policy
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return txt[0];
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err.code === 'ENOTFOUND' || err.code === 'ENODATA') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getDmarcRecord = async (domain, resolver) => {
|
|
30
|
+
resolver = resolver || dns.resolve;
|
|
31
|
+
|
|
32
|
+
let txt = await resolveTxt(domain, resolver);
|
|
33
|
+
let isOrgRecord = false;
|
|
34
|
+
|
|
35
|
+
if (!txt) {
|
|
36
|
+
let orgDomain = psl.get(domain);
|
|
37
|
+
if (orgDomain !== domain) {
|
|
38
|
+
// try org domain as well
|
|
39
|
+
txt = await resolveTxt(orgDomain, resolver);
|
|
40
|
+
isOrgRecord = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!txt) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let parsed = Object.fromEntries(
|
|
49
|
+
txt
|
|
50
|
+
.split(';')
|
|
51
|
+
.map(e => e.trim())
|
|
52
|
+
.filter(e => e)
|
|
53
|
+
.map(e => {
|
|
54
|
+
let splitPos = e.indexOf('=');
|
|
55
|
+
if (splitPos < 0) {
|
|
56
|
+
return [e.toLowerCase().trim(), false];
|
|
57
|
+
} else if (splitPos === 0) {
|
|
58
|
+
return [false, e];
|
|
59
|
+
}
|
|
60
|
+
let key = e.substr(0, splitPos).toLowerCase().trim();
|
|
61
|
+
let val = e.substr(splitPos + 1);
|
|
62
|
+
if (['pct', 'ri'].includes(key)) {
|
|
63
|
+
val = parseInt(val, 10) || 0;
|
|
64
|
+
}
|
|
65
|
+
return [key, val];
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
parsed.rr = txt;
|
|
70
|
+
parsed.isOrgRecord = isOrgRecord;
|
|
71
|
+
|
|
72
|
+
return parsed;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
module.exports = getDmarcRecord;
|
package/lib/dmarc/verify.js
CHANGED
|
@@ -4,73 +4,7 @@ const dns = require('dns').promises;
|
|
|
4
4
|
const punycode = require('punycode/');
|
|
5
5
|
const psl = require('psl');
|
|
6
6
|
const { formatAuthHeaderRow, getAlignment } = require('../tools');
|
|
7
|
-
|
|
8
|
-
const resolveTxt = async (domain, resolver) => {
|
|
9
|
-
try {
|
|
10
|
-
let txt = await resolver(`_dmarc.${domain}`, 'TXT');
|
|
11
|
-
if (!txt || !txt.length) {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
txt = txt.map(row => row.join('').trim()).filter(row => /^v=DMARC1\b/i.test(row));
|
|
16
|
-
|
|
17
|
-
if (txt.length !== 1) {
|
|
18
|
-
//no records or multiple records yield in no policy
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return txt[0];
|
|
23
|
-
} catch (err) {
|
|
24
|
-
if (err.code === 'ENOTFOUND' || err.code === 'ENODATA') {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
throw err;
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const getDmarcRecord = async (domain, resolver) => {
|
|
32
|
-
let txt = await resolveTxt(domain, resolver);
|
|
33
|
-
let isOrgRecord = false;
|
|
34
|
-
|
|
35
|
-
if (!txt) {
|
|
36
|
-
let orgDomain = psl.get(domain);
|
|
37
|
-
if (orgDomain !== domain) {
|
|
38
|
-
// try org domain as well
|
|
39
|
-
txt = await resolveTxt(orgDomain, resolver);
|
|
40
|
-
isOrgRecord = true;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (!txt) {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let parsed = Object.fromEntries(
|
|
49
|
-
txt
|
|
50
|
-
.split(';')
|
|
51
|
-
.map(e => e.trim())
|
|
52
|
-
.filter(e => e)
|
|
53
|
-
.map(e => {
|
|
54
|
-
let splitPos = e.indexOf('=');
|
|
55
|
-
if (splitPos < 0) {
|
|
56
|
-
return [e.toLowerCase().trim(), false];
|
|
57
|
-
} else if (splitPos === 0) {
|
|
58
|
-
return [false, e];
|
|
59
|
-
}
|
|
60
|
-
let key = e.substr(0, splitPos).toLowerCase().trim();
|
|
61
|
-
let val = e.substr(splitPos + 1);
|
|
62
|
-
if (['pct', 'ri'].includes(key)) {
|
|
63
|
-
val = parseInt(val, 10) || 0;
|
|
64
|
-
}
|
|
65
|
-
return [key, val];
|
|
66
|
-
})
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
parsed.rr = txt;
|
|
70
|
-
parsed.isOrgRecord = isOrgRecord;
|
|
71
|
-
|
|
72
|
-
return parsed;
|
|
73
|
-
};
|
|
7
|
+
const getDmarcRecord = require('./get-dmarc-record');
|
|
74
8
|
|
|
75
9
|
const verifyDmarc = async opts => {
|
|
76
10
|
let { headerFrom, spfDomains, dkimDomains, resolver, arcResult } = opts;
|
package/lib/mta-sts.js
CHANGED
|
@@ -139,9 +139,13 @@ const parsePolicy = file => {
|
|
|
139
139
|
*/
|
|
140
140
|
const validateMx = (mx, policy) => {
|
|
141
141
|
policy = policy || { mode: 'none' };
|
|
142
|
-
if (policy.mode === 'none' || policy.mode
|
|
142
|
+
if (policy.mode === 'none' || !policy.mode) {
|
|
143
143
|
// nothing to check for
|
|
144
|
-
return
|
|
144
|
+
return {
|
|
145
|
+
valid: true,
|
|
146
|
+
mode: policy.mode || 'none',
|
|
147
|
+
testing: policy.mode === 'testing'
|
|
148
|
+
};
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
mx = (mx || '').toString().trim().toLowerCase();
|
|
@@ -161,15 +165,29 @@ const validateMx = (mx, policy) => {
|
|
|
161
165
|
// remove wildcard
|
|
162
166
|
allowed = allowed.substr(1);
|
|
163
167
|
if (mx.substr(-allowed.length) === allowed) {
|
|
164
|
-
return
|
|
168
|
+
return {
|
|
169
|
+
valid: true,
|
|
170
|
+
mode: policy.mode || 'none',
|
|
171
|
+
match: allowed,
|
|
172
|
+
testing: policy.mode === 'testing'
|
|
173
|
+
};
|
|
165
174
|
}
|
|
166
175
|
} else if (allowed === mx) {
|
|
167
|
-
return
|
|
176
|
+
return {
|
|
177
|
+
valid: true,
|
|
178
|
+
mode: policy.mode || 'none',
|
|
179
|
+
match: allowed,
|
|
180
|
+
testing: policy.mode === 'testing'
|
|
181
|
+
};
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
|
|
171
185
|
// no match found
|
|
172
|
-
return
|
|
186
|
+
return {
|
|
187
|
+
valid: false,
|
|
188
|
+
mode: policy.mode || 'none',
|
|
189
|
+
testing: policy.mode === 'testing'
|
|
190
|
+
};
|
|
173
191
|
};
|
|
174
192
|
|
|
175
193
|
/**
|
package/licenses.txt
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
name license type
|
|
2
|
-
---- ------------
|
|
3
|
-
@postalsys/vmc MIT
|
|
4
|
-
fast-xml-parser MIT
|
|
5
|
-
ipaddr.js MIT
|
|
6
|
-
joi BSD-3-Clause
|
|
7
|
-
libmime MIT
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
yargs MIT git+https://github.com/yargs/yargs.git 17.5.1 n/a
|
|
1
|
+
name license type link installed version author
|
|
2
|
+
---- ------------ ---- ----------------- ------
|
|
3
|
+
@postalsys/vmc MIT https://registry.npmjs.org/@postalsys/vmc/-/vmc-1.0.6.tgz 1.0.6 Postal Systems OÜ
|
|
4
|
+
fast-xml-parser MIT git+https://github.com/NaturalIntelligence/fast-xml-parser.git 4.1.1 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/hapijs/joi.git 17.7.0 n/a
|
|
7
|
+
libmime MIT git://github.com/andris9/libmime.git 5.2.1 Andris Reinman <andris@kreata.ee>
|
|
8
|
+
nodemailer MIT git+https://github.com/nodemailer/nodemailer.git 6.9.1 Andris Reinman
|
|
9
|
+
psl MIT git+ssh://git@github.com/lupomontero/psl.git 1.9.0 Lupo Montero <lupomontero@gmail.com> (https://lupomontero.com/)
|
|
10
|
+
punycode MIT git+https://github.com/mathiasbynens/punycode.js.git 2.3.0 Mathias Bynens https://mathiasbynens.be/
|
|
11
|
+
yargs MIT git+https://github.com/yargs/yargs.git 17.6.2 n/a
|
package/man/mailauth.1
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailauth",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "Email authentication library for Node.js",
|
|
5
5
|
"main": "lib/mailauth.js",
|
|
6
6
|
"scripts": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"homepage": "https://github.com/postalsys/mailauth",
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"chai": "4.3.7",
|
|
36
|
-
"eslint": "8.
|
|
36
|
+
"eslint": "8.35.0",
|
|
37
37
|
"eslint-config-nodemailer": "1.2.0",
|
|
38
38
|
"eslint-config-prettier": "8.6.0",
|
|
39
39
|
"js-yaml": "4.1.0",
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@postalsys/vmc": "1.0.6",
|
|
49
|
-
"fast-xml-parser": "4.
|
|
49
|
+
"fast-xml-parser": "4.1.3",
|
|
50
50
|
"ipaddr.js": "2.0.1",
|
|
51
|
-
"joi": "17.
|
|
52
|
-
"libmime": "5.2.
|
|
53
|
-
"nodemailer": "6.9.
|
|
51
|
+
"joi": "17.8.3",
|
|
52
|
+
"libmime": "5.2.1",
|
|
53
|
+
"nodemailer": "6.9.1",
|
|
54
54
|
"psl": "1.9.0",
|
|
55
55
|
"punycode": "2.3.0",
|
|
56
|
-
"yargs": "17.
|
|
56
|
+
"yargs": "17.7.1"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">=16.0.0"
|