mailauth 4.0.0 → 4.0.2

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/bin/mailauth.js CHANGED
@@ -324,6 +324,12 @@ const argv = yargs(hideBin(process.argv))
324
324
  type: 'string',
325
325
  description: 'Sending domain to validate',
326
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
327
333
  });
328
334
  },
329
335
  argv => {
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;
@@ -52,7 +53,7 @@ const cmd = async argv => {
52
53
  let match = dnsCache?.[name]?.[rr];
53
54
 
54
55
  if (argv.verbose) {
55
- 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)`);
56
57
  }
57
58
 
58
59
  if (!match) {
@@ -63,6 +64,18 @@ const cmd = async argv => {
63
64
 
64
65
  return match;
65
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
+ };
66
79
  }
67
80
 
68
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;
@@ -45,7 +45,7 @@ const cmd = async argv => {
45
45
  let match = dnsCache?.[name]?.[rr];
46
46
 
47
47
  if (argv.verbose) {
48
- 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)`);
49
49
  }
50
50
 
51
51
  if (!match) {
@@ -56,29 +56,17 @@ const cmd = async argv => {
56
56
 
57
57
  return match;
58
58
  };
59
- } else {
59
+ } else if (argv.verbose) {
60
60
  opts.resolver = async (name, rr) => {
61
61
  let match;
62
62
  try {
63
- match = await dns.resolve(name, rr);
64
- } catch (err) {
65
- if (argv.verbose) {
66
- console.error(`DNS query for ${rr} ${name}: ${err.code || err.message}`);
67
- }
68
- throw err;
69
- }
70
-
71
- if (argv.verbose) {
63
+ match = await resolve(name, rr);
72
64
  console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
73
- }
74
-
75
- if (!match) {
76
- let err = new Error('Error');
77
- err.code = 'ENOTFOUND';
65
+ return match;
66
+ } catch (err) {
67
+ console.error(`DNS query for ${rr} ${name}: ${err.message}${err.code ? ` [${err.code}]` : ''}`);
78
68
  throw err;
79
69
  }
80
-
81
- return match;
82
70
  };
83
71
  }
84
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
 
@@ -31,7 +31,7 @@ class RelaxedHash {
31
31
  this.bodyHashedBytes = 0;
32
32
  this.maxBodyLength = maxBodyLength;
33
33
 
34
- this.maxSizeReached = false;
34
+ this.maxSizeReached = maxBodyLength === 0;
35
35
 
36
36
  this.emptyLinesQueue = [];
37
37
  }
@@ -21,12 +21,18 @@ class SimpleHash {
21
21
  this.byteLength = 0;
22
22
 
23
23
  this.bodyHashedBytes = 0;
24
+
24
25
  this.maxBodyLength = maxBodyLength;
26
+ this.maxSizeReached = maxBodyLength === 0;
25
27
 
26
28
  this.lastNewline = false;
27
29
  }
28
30
 
29
31
  _updateBodyHash(chunk) {
32
+ if (this.maxSizeReached) {
33
+ return;
34
+ }
35
+
30
36
  // the following is needed for l= option
31
37
  if (
32
38
  typeof this.maxBodyLength === 'number' &&
@@ -34,10 +40,12 @@ class SimpleHash {
34
40
  this.maxBodyLength >= 0 &&
35
41
  this.bodyHashedBytes + chunk.length > this.maxBodyLength
36
42
  ) {
43
+ this.maxSizeReached = true;
37
44
  if (this.bodyHashedBytes >= this.maxBodyLength) {
38
45
  // nothing to do here, skip entire chunk
39
46
  return;
40
47
  }
48
+
41
49
  // only use allowed size of bytes
42
50
  chunk = chunk.slice(0, this.maxBodyLength - this.bodyHashedBytes);
43
51
  }
@@ -49,6 +57,11 @@ class SimpleHash {
49
57
  }
50
58
 
51
59
  update(chunk) {
60
+ this.byteLength += (chunk && chunk.length) || 0;
61
+ if (this.maxSizeReached) {
62
+ return;
63
+ }
64
+
52
65
  if (this.remainder.length) {
53
66
  // see if we can release the last remainder
54
67
  for (let i = 0; i < chunk.length; i++) {
@@ -10,14 +10,16 @@ class DkimSigner extends MessageParser {
10
10
  constructor(options) {
11
11
  super();
12
12
 
13
- let { canonicalization, algorithm, signTime, headerList, signatureData, arc, bodyHash, headers, getARChain } = options || {};
13
+ let { canonicalization, algorithm, signTime, headerList, signatureData, arc, bodyHash, headers, getARChain, expires } = options || {};
14
14
 
15
15
  this.algorithm = algorithm || false;
16
16
  this.canonicalization = canonicalization || 'relaxed/relaxed';
17
17
 
18
18
  this.errors = [];
19
19
 
20
+ this.expires = expires;
20
21
  this.signTime = signTime;
22
+
21
23
  this.headerList = headerList;
22
24
 
23
25
  this.signatureData = [].concat(signatureData || []).map(entry => {
@@ -243,7 +245,10 @@ class DkimSigner extends MessageParser {
243
245
  instance: this.arc?.instance, // ARC only
244
246
  algorithm,
245
247
  canonicalization: this.getCanonicalization(signatureData).canonicalization,
248
+
246
249
  signTime: this.signTime,
250
+ expires: this.expires,
251
+
247
252
  bodyHash: this.bodyHashes.has(hashKey) ? this.bodyHashes.get(hashKey).hash : null
248
253
  },
249
254
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { getSigningHeaderLines, getPublicKey, parseDkimHeaders, formatAuthHeaderRow, getAlignment } = require('../../lib/tools');
3
+ const { getSigningHeaderLines, getPublicKey, parseDkimHeaders, formatAuthHeaderRow, getAlignment, getCurTime } = require('../../lib/tools');
4
4
  const { MessageParser } = require('./message-parser');
5
5
  const { dkimBody } = require('./body');
6
6
  const { generateCanonicalizedHeader } = require('./header');
@@ -16,6 +16,8 @@ class DkimVerifier extends MessageParser {
16
16
  this.resolver = this.options.resolver;
17
17
  this.minBitLength = this.options.minBitLength;
18
18
 
19
+ this.curTime = getCurTime(this.options.curTime);
20
+
19
21
  this.results = [];
20
22
 
21
23
  this.signatureHeaders = [];
@@ -114,6 +116,12 @@ class DkimVerifier extends MessageParser {
114
116
  signatureHeader.signingDomain = signatureHeader.parsed?.d?.value || '';
115
117
  signatureHeader.selector = signatureHeader.parsed?.s?.value || '';
116
118
 
119
+ signatureHeader.timestamp =
120
+ signatureHeader.parsed?.t && !isNaN(signatureHeader.parsed?.t?.value) ? new Date(signatureHeader.parsed?.t?.value * 1000) : null;
121
+
122
+ signatureHeader.expiration =
123
+ signatureHeader.parsed?.x && !isNaN(signatureHeader.parsed?.x?.value) ? new Date(signatureHeader.parsed?.x?.value * 1000) : null;
124
+
117
125
  signatureHeader.maxBodyLength =
118
126
  signatureHeader.parsed?.l?.value && !isNaN(signatureHeader.parsed?.l?.value) ? signatureHeader.parsed?.l?.value : '';
119
127
 
@@ -225,9 +233,21 @@ class DkimVerifier extends MessageParser {
225
233
  ? 'pass'
226
234
  : 'fail';
227
235
 
228
- if (status === 'fail') {
236
+ if (status.result === 'fail') {
229
237
  status.comment = 'bad signature';
230
238
  }
239
+
240
+ if (status.result === 'pass') {
241
+ if (signatureHeader.expiration && signatureHeader.timestamp && signatureHeader.expiration < signatureHeader.timestamp) {
242
+ status.result = 'neutral';
243
+ status.comment = 'invalid expiration';
244
+ }
245
+
246
+ if (signatureHeader.expiration && signatureHeader.expiration < this.curTime) {
247
+ status.result = 'neutral';
248
+ status.comment = 'expired';
249
+ }
250
+ }
231
251
  } catch (err) {
232
252
  status.result = 'neutral';
233
253
  status.comment = err.message;
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const { formatSignatureHeaderLine, formatRelaxedLine } = require('../../../lib/tools');
3
+ const { formatSignatureHeaderLine, formatRelaxedLine, getCurTime } = require('../../../lib/tools');
4
4
 
5
5
  // generate headers for signing
6
6
  const relaxedHeaders = (type, signingHeaderLines, options) => {
7
- let { signatureHeaderLine, signingDomain, selector, algorithm, canonicalization, bodyHash, signTime, signature, instance, bodyHashedBytes } = options || {};
7
+ let { signatureHeaderLine, signingDomain, selector, algorithm, canonicalization, bodyHash, signTime, signature, instance, bodyHashedBytes, expires } =
8
+ options || {};
8
9
  let chunks = [];
9
10
 
10
11
  for (let signedHeaderLine of signingHeaderLines.headers) {
@@ -33,15 +34,11 @@ const relaxedHeaders = (type, signingHeaderLines, options) => {
33
34
  }
34
35
 
35
36
  if (signTime) {
36
- if (typeof signTime === 'string' || typeof signTime === 'number') {
37
- signTime = new Date(signTime);
38
- }
37
+ opts.t = Math.floor(getCurTime(signTime).getTime() / 1000);
38
+ }
39
39
 
40
- if (Object.prototype.toString.call(signTime) === '[object Date]' && signTime.toString() !== 'Invalid Date') {
41
- // we need a unix timestamp value
42
- signTime = Math.round(signTime.getTime() / 1000);
43
- opts.t = signTime;
44
- }
40
+ if (expires) {
41
+ opts.x = Math.floor(getCurTime(expires).getTime() / 1000);
45
42
  }
46
43
 
47
44
  signatureHeaderLine = formatSignatureHeaderLine(
@@ -1,12 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const { formatSignatureHeaderLine } = require('../../../lib/tools');
3
+ const { formatSignatureHeaderLine, getCurTime } = require('../../../lib/tools');
4
4
 
5
5
  const formatSimpleLine = (line, suffix) => Buffer.from(line.toString('binary') + (suffix ? suffix : ''), 'binary');
6
6
 
7
7
  // generate headers for signing
8
8
  const simpleHeaders = (type, signingHeaderLines, options) => {
9
- let { signatureHeaderLine, signingDomain, selector, algorithm, canonicalization, bodyHash, signTime, signature, instance, bodyHashedBytes } = options || {};
9
+ let { signatureHeaderLine, signingDomain, selector, algorithm, canonicalization, bodyHash, signTime, signature, instance, bodyHashedBytes, expires } =
10
+ options || {};
10
11
  let chunks = [];
11
12
 
12
13
  for (let signedHeaderLine of signingHeaderLines.headers) {
@@ -35,15 +36,11 @@ const simpleHeaders = (type, signingHeaderLines, options) => {
35
36
  }
36
37
 
37
38
  if (signTime) {
38
- if (typeof signTime === 'string' || typeof signTime === 'number') {
39
- signTime = new Date(signTime);
40
- }
39
+ opts.t = Math.floor(getCurTime(signTime).getTime() / 1000);
40
+ }
41
41
 
42
- if (Object.prototype.toString.call(signTime) === '[object Date]' && signTime.toString() !== 'Invalid Date') {
43
- // we need a unix timestamp value
44
- signTime = Math.round(signTime.getTime() / 1000);
45
- opts.t = signTime;
46
- }
42
+ if (expires) {
43
+ opts.x = Math.floor(getCurTime(expires).getTime() / 1000);
47
44
  }
48
45
 
49
46
  signatureHeaderLine = formatSignatureHeaderLine(
@@ -245,7 +245,7 @@ const headerParser = buf => {
245
245
  } else if (['bh', 'b', 'p', 'h'].includes(parts[i].key)) {
246
246
  // remove unneeded whitespace
247
247
  parts[i].value = parts[i].value.replace(/\s+/g, '');
248
- } else if (['l', 'v', 't'].includes(parts[i].key) && !isNaN(parts[i].value)) {
248
+ } else if (['l', 'v', 't', 'x'].includes(parts[i].key) && !isNaN(parts[i].value)) {
249
249
  parts[i].value = Number(parts[i].value);
250
250
  } else if (parts[i].key === 'i' && /^arc-/i.test(headerKey)) {
251
251
  parts[i].value = Number(parts[i].value);
package/lib/tools.js CHANGED
@@ -485,6 +485,29 @@ const getPtrHostname = parsedAddr => {
485
485
  }
486
486
  };
487
487
 
488
+ function getCurTime(timeValue) {
489
+ if (timeValue) {
490
+ if (typeof timeValue === 'object' && typeof timeValue.toISOString === 'function') {
491
+ return timeValue;
492
+ }
493
+
494
+ if (typeof timeValue === 'number' || !isNaN(timeValue)) {
495
+ let timestamp = Number(timeValue);
496
+ let curTime = new Date(timestamp);
497
+ if (curTime.toString !== 'Invalid Date') {
498
+ return curTime;
499
+ }
500
+ } else if (typeof timeValue === 'string') {
501
+ let curTime = new Date(timeValue);
502
+ if (curTime.toString !== 'Invalid Date') {
503
+ return curTime;
504
+ }
505
+ }
506
+ }
507
+
508
+ return new Date();
509
+ }
510
+
488
511
  module.exports = {
489
512
  writeToStream,
490
513
  parseHeaders,
@@ -508,5 +531,7 @@ module.exports = {
508
531
  formatRelaxedLine,
509
532
  formatDomain,
510
533
 
511
- getPtrHostname
534
+ getPtrHostname,
535
+
536
+ getCurTime
512
537
  };
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.5.tgz 1.0.5 Postal Systems OÜ
3
+ @postalsys/vmc MIT https://registry.npmjs.org/@postalsys/vmc/-/vmc-1.0.6.tgz 1.0.6 Postal Systems OÜ
4
4
  fast-xml-parser MIT git+https://github.com/NaturalIntelligence/fast-xml-parser.git 4.0.9 Amit Gupta (https://amitkumargupta.work/)
5
5
  ipaddr.js MIT git://github.com/whitequark/ipaddr.js.git 2.0.1 whitequark <whitequark@whitequark.org>
6
6
  joi BSD-3-Clause git://github.com/sideway/joi.git 17.6.0 n/a
7
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
- nodemailer MIT git+https://github.com/nodemailer/nodemailer.git 6.7.7 Andris Reinman
9
+ nodemailer MIT git+https://github.com/nodemailer/nodemailer.git 6.7.8 Andris Reinman
10
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
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.3" "Mailauth Help"
1
+ .TH "MAILAUTH" "1" "September 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailauth",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
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.20.0",
36
+ "eslint": "8.24.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": "6.0.0",
40
+ "license-report": "6.1.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.5",
49
- "fast-xml-parser": "4.0.9",
48
+ "@postalsys/vmc": "1.0.6",
49
+ "fast-xml-parser": "4.0.10",
50
50
  "ipaddr.js": "2.0.1",
51
- "joi": "17.6.0",
51
+ "joi": "17.6.1",
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"