mailauth 4.4.1 → 4.5.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/bin/mailauth.js CHANGED
@@ -12,6 +12,7 @@ const commandSign = require('../lib/commands/sign');
12
12
  const commandSeal = require('../lib/commands/seal');
13
13
  const commandSpf = require('../lib/commands/spf');
14
14
  const commandVmc = require('../lib/commands/vmc');
15
+ const commandBodyhash = require('../lib/commands/bodyhash');
15
16
 
16
17
  const fs = require('fs');
17
18
  const pathlib = require('path');
@@ -100,6 +101,12 @@ const argv = yargs(hideBin(process.argv))
100
101
  description: 'Key selector for signing (s= tag)',
101
102
  demandOption: true
102
103
  })
104
+ .option('algo', {
105
+ alias: 'a',
106
+ type: 'string',
107
+ description: 'Signing algorithm. Defaults either to rsa-sha256 or ed25519-sha256 depending on the private key format.',
108
+ default: 'rsa-sha256'
109
+ })
103
110
  .option('canonicalization', {
104
111
  alias: 'c',
105
112
  type: 'string',
@@ -344,6 +351,50 @@ const argv = yargs(hideBin(process.argv))
344
351
  });
345
352
  }
346
353
  )
354
+ .command(
355
+ ['bodyhash [email]'],
356
+ 'Generate a signature body hash for an email',
357
+ yargs => {
358
+ yargs
359
+
360
+ .option('algo', {
361
+ alias: 'a',
362
+ type: 'string',
363
+ description: 'Hashing algorithm. Defaults to sha256.',
364
+ default: 'sha256'
365
+ })
366
+
367
+ .option('canonicalization', {
368
+ alias: 'c',
369
+ type: 'string',
370
+ description: 'Canonicalization algorithm (c= tag)',
371
+ default: 'relaxed'
372
+ })
373
+
374
+ .option('body-length', {
375
+ alias: 'l',
376
+ type: 'number',
377
+ description: 'Maximum length of canonicalizated body to sign (l= tag)'
378
+ });
379
+
380
+ yargs.positional('email', {
381
+ describe: 'Path to the email message file in EML format. If not specified then content is read from stdin'
382
+ });
383
+ },
384
+ argv => {
385
+ commandBodyhash(argv)
386
+ .then(() => {
387
+ process.exit();
388
+ })
389
+ .catch(err => {
390
+ if (!err.suppress) {
391
+ console.error('Failed to calculate body hash for the input message');
392
+ console.error(err);
393
+ }
394
+ process.exit(1);
395
+ });
396
+ }
397
+ )
347
398
  .command(
348
399
  ['license'],
349
400
  'Show license information',
package/cli.md CHANGED
@@ -14,6 +14,7 @@ Command line utility and a [Node.js library](README.md) for email authentication
14
14
  - [seal](#seal) - to seal an email with ARC
15
15
  - [spf](#spf) - to validate SPF for an IP address and an email address
16
16
  - [vmc](#vmc) - to validate BIMI VMC logo files
17
+ - [bodyhash](#bodyhash) - to generate the signature body hash value for an email
17
18
  - [license](#license) - display licenses for `mailauth` and included modules
18
19
  - [DNS cache file](#dns-cache-file)
19
20
 
@@ -320,6 +321,35 @@ $ mailauth vmc -p /path/to/vmc-with-invalid-svg.pem
320
321
  }
321
322
  ```
322
323
 
324
+ ### bodyhash
325
+
326
+ `bodyhash` command takes an email message and calculates the body hash value for it
327
+
328
+ ```
329
+ $ mailauth bodyhash [options] [email]
330
+ ```
331
+
332
+ Where
333
+
334
+ - **options** are option flags and arguments
335
+ - **email** is the path to EML formatted email message file. If not provided then email message is read from standard input
336
+
337
+ **Options**
338
+
339
+ - `--algo sha256` or `-a sha256` is the signing algorithm. Defaults to "sha256". Can also use the a= tag format ("rsa-sha256").
340
+ - `--canonicalization algo` or `-c algo` is the body canonicalization algorithm, defaults to "relaxed". Can also use the c= tag format ("relaxed/relaxed").
341
+ - `--body-length 12345` or `-l 12345` is the maximum length of canonicalizated body to sign (l= tag)
342
+
343
+ **Example**
344
+
345
+ ```
346
+ $ mailauth bodyhash /path/message.eml -a sha1 --verbose
347
+ Hashing algorithm: sha1
348
+ Body canonicalization algorithm: relaxed
349
+ --------
350
+ j+dD7whKXS1yDmyoWtvClYSyYiQ=
351
+ ```
352
+
323
353
  ### license
324
354
 
325
355
  Display licenses for `mailauth` and included modules.
package/lib/bimi/index.js CHANGED
@@ -14,6 +14,8 @@ const http = require('http');
14
14
  const { vmc } = require('@postalsys/vmc');
15
15
  const { validateSvg } = require('./validate-svg');
16
16
 
17
+ const HTTP_REQUEST_TIMEOUT = 15 * 1000;
18
+
17
19
  const lookup = async data => {
18
20
  let { dmarc, headers, resolver, bimiWithAlignedDkim } = data;
19
21
  let headerRows = (headers && headers.parsed) || [];
@@ -197,7 +199,9 @@ const downloadPromise = (url, cachedFile) => {
197
199
  port: 443,
198
200
  path: parsedUrl.pathname,
199
201
  method: 'GET',
200
- rejectUnauthorized: true
202
+ rejectUnauthorized: true,
203
+
204
+ timeout: HTTP_REQUEST_TIMEOUT
201
205
  };
202
206
 
203
207
  return new Promise((resolve, reject) => {
@@ -240,6 +244,13 @@ const downloadPromise = (url, cachedFile) => {
240
244
  res.on('error', err => reject(err));
241
245
  });
242
246
 
247
+ req.on('timeout', () => {
248
+ req.destroy(); // cancel request
249
+ let error = new Error(`Request timeout for ${parsedUrl.href}`);
250
+ error.code = 'HTTP_SOCKET_TIMEOUT';
251
+ reject(error);
252
+ });
253
+
243
254
  req.on('error', err => {
244
255
  reject(err);
245
256
  });
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ const { DkimSigner } = require('../dkim/dkim-signer');
4
+ const { writeToStream } = require('../tools');
5
+ const fs = require('fs');
6
+
7
+ const cmd = async argv => {
8
+ let source = argv.email;
9
+ let useStdin = false;
10
+ let stream;
11
+
12
+ if (!source) {
13
+ useStdin = true;
14
+ source = 'standard input';
15
+ }
16
+
17
+ if (argv.verbose) {
18
+ console.error(`Reading email message from ${source}`);
19
+ }
20
+
21
+ if (useStdin) {
22
+ stream = process.stdin;
23
+ } else {
24
+ stream = fs.createReadStream(source);
25
+ }
26
+
27
+ if (isNaN(argv.bodyLength) || argv.bodyLength < 0) {
28
+ argv.bodyLength = null;
29
+ }
30
+
31
+ let signatureOpts = {
32
+ type: 'DKIM',
33
+ privateKey: true, // force hash calculation
34
+ canonicalization: argv.canonicalization && (argv.canonicalization.includes('/') ? argv.canonicalization : `/${argv.canonicalization}`),
35
+ algorithm: argv.algo,
36
+ maxBodyLength: argv.bodyLength
37
+ };
38
+
39
+ let dkimSigner = new DkimSigner({ signatureData: [signatureOpts] });
40
+
41
+ let { hashAlgo } = dkimSigner.getAlgorithm(signatureOpts);
42
+ let { bodyCanon } = dkimSigner.getCanonicalization(signatureOpts);
43
+
44
+ if (argv.verbose) {
45
+ if (hashAlgo) {
46
+ console.error(`Hashing algorithm: ${hashAlgo}`);
47
+ }
48
+ if (bodyCanon) {
49
+ console.error(`Body canonicalization algorithm: ${bodyCanon}`);
50
+ }
51
+ if (signatureOpts.maxBodyLength) {
52
+ console.error(`Maximum body length: ${signatureOpts.maxBodyLength}`);
53
+ }
54
+ console.error('--------');
55
+ }
56
+
57
+ await writeToStream(dkimSigner, stream);
58
+
59
+ let hashKey = `${bodyCanon}:${hashAlgo}:${typeof argv.bodyLength === 'number' ? argv.bodyLength : ''}`;
60
+ const bodyHash = dkimSigner.bodyHashes.get(hashKey)?.hash;
61
+ if (bodyHash) {
62
+ process.stdout.write(bodyHash);
63
+ }
64
+ };
65
+
66
+ module.exports = cmd;
@@ -48,12 +48,12 @@ const cmd = async argv => {
48
48
  if (signatureOpts.selector) {
49
49
  console.error(`Key selector: ${signatureOpts.selector}`);
50
50
  }
51
- if (signatureOpts.canonicalization) {
52
- console.error(`Canonicalization algorithm: ${signatureOpts.canonicalization}`);
53
- }
54
51
  if (signatureOpts.algorithm) {
55
52
  console.error(`Hashing algorithm: ${signatureOpts.algorithm}`);
56
53
  }
54
+ if (signatureOpts.canonicalization) {
55
+ console.error(`Canonicalization algorithm: ${signatureOpts.canonicalization}`);
56
+ }
57
57
  if (signatureOpts.maxBodyLength) {
58
58
  console.error(`Maximum body length: ${signatureOpts.maxBodyLength}`);
59
59
  }
@@ -10,8 +10,11 @@ const dkimBody = (canonicalization, ...options) => {
10
10
  return new SimpleHash(...options);
11
11
  case 'relaxed':
12
12
  return new RelaxedHash(...options);
13
- default:
14
- throw new Error('Unknown body canonicalization');
13
+ default: {
14
+ let error = new Error('Unknown body canonicalization');
15
+ error.canonicalization = canonicalization;
16
+ throw error;
17
+ }
15
18
  }
16
19
  };
17
20
 
@@ -96,11 +96,15 @@ class DkimSigner extends MessageParser {
96
96
  let [header, body] = canonicalization.split('/');
97
97
 
98
98
  if (!['relaxed', 'simple'].includes(header)) {
99
- throw new Error('Unknown header canonicalization: ' + header);
99
+ let error = new Error('Unknown header canonicalization');
100
+ error.canonicalization = header;
101
+ throw error;
100
102
  }
101
103
 
102
104
  if (!['relaxed', 'simple'].includes(body)) {
103
- throw new Error('Unknown header canonicalization: ' + body);
105
+ let error = new Error('Unknown body canonicalization');
106
+ error.canonicalization = body;
107
+ throw error;
104
108
  }
105
109
  } catch (err) {
106
110
  err.code = 'EINVALIDCANON';
@@ -11,8 +11,11 @@ const generateCanonicalizedHeader = (type, signingHeaderLines, options) => {
11
11
  return simpleHeaders(type, signingHeaderLines, options);
12
12
  case 'relaxed':
13
13
  return relaxedHeaders(type, signingHeaderLines, options);
14
- default:
15
- throw new Error('Unknown header canonicalization');
14
+ default: {
15
+ let error = new Error('Unknown header canonicalization');
16
+ error.canonicalization = canonicalization;
17
+ throw error;
18
+ }
16
19
  }
17
20
  };
18
21
 
package/lib/mta-sts.js CHANGED
@@ -5,6 +5,8 @@ const dns = require('dns');
5
5
  const parseDkimHeaders = require('./parse-dkim-headers');
6
6
  const https = require('https');
7
7
 
8
+ const HTTP_REQUEST_TIMEOUT = 15 * 1000;
9
+
8
10
  /**
9
11
  * Resolve MTA-STS policy ID
10
12
  * @param {String} address Either email address or a domain name
@@ -247,7 +249,9 @@ const fetchPolicy = async (domain, opts) => {
247
249
  port: 443,
248
250
  path,
249
251
  method: 'GET',
250
- rejectUnauthorized: true
252
+ rejectUnauthorized: true,
253
+
254
+ timeout: HTTP_REQUEST_TIMEOUT
251
255
  };
252
256
 
253
257
  let data = await new Promise((resolve, reject) => {
@@ -273,6 +277,13 @@ const fetchPolicy = async (domain, opts) => {
273
277
  res.on('error', err => reject(err));
274
278
  });
275
279
 
280
+ req.on('timeout', () => {
281
+ req.destroy(); // cancel request
282
+ let error = new Error(`Request timeout for https://${servername}${path}`);
283
+ error.code = 'HTTP_SOCKET_TIMEOUT';
284
+ reject(error);
285
+ });
286
+
276
287
  req.on('error', err => {
277
288
  reject(err);
278
289
  });
package/lib/tools.js CHANGED
@@ -450,11 +450,15 @@ const validateAlgorithm = (algorithm, strict) => {
450
450
  let [signAlgo, hashAlgo] = algorithm.toLowerCase().split('-');
451
451
 
452
452
  if (!['rsa', 'ed25519'].includes(signAlgo)) {
453
- throw new Error('Unknown signing algorithm: ' + signAlgo);
453
+ let error = new Error('Unknown signing algorithm');
454
+ error.signAlgo = signAlgo;
455
+ throw error;
454
456
  }
455
457
 
456
458
  if (!['sha256'].concat(!strict ? 'sha1' : []).includes(hashAlgo)) {
457
- throw new Error('Unknown hashing algorithm: ' + hashAlgo);
459
+ let error = new Error('Unknown hashing algorithm');
460
+ error.hashAlgo = hashAlgo;
461
+ throw error;
458
462
  }
459
463
  } catch (err) {
460
464
  err.code = 'EINVALIDALGO';
package/licenses.txt CHANGED
@@ -1,11 +1,11 @@
1
1
  name license type link installed version author
2
2
  ---- ------------ ---- ----------------- ------
3
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.2.4 Amit Gupta (https://amitkumargupta.work/)
4
+ fast-xml-parser MIT git+https://github.com/NaturalIntelligence/fast-xml-parser.git 4.2.6 Amit Gupta (https://amitguptagwl.github.io)
5
5
  ipaddr.js MIT git://github.com/whitequark/ipaddr.js.git 2.1.0 whitequark <whitequark@whitequark.org>
6
6
  joi BSD-3-Clause git://github.com/hapijs/joi.git 17.9.2 n/a
7
7
  libmime MIT git://github.com/andris9/libmime.git 5.2.1 Andris Reinman <andris@kreata.ee>
8
- nodemailer MIT-0 git+https://github.com/nodemailer/nodemailer.git 6.9.3 Andris Reinman
8
+ nodemailer MIT-0 git+https://github.com/nodemailer/nodemailer.git 6.9.4 Andris Reinman
9
9
  psl MIT git+ssh://git@github.com/lupomontero/psl.git 1.9.0 Lupo Montero <lupomontero@gmail.com> (https://lupomontero.com/)
10
10
  punycode MIT git+https://github.com/mathiasbynens/punycode.js.git 2.3.0 Mathias Bynens https://mathiasbynens.be/
11
11
  yargs MIT git+https://github.com/yargs/yargs.git 17.7.2 n/a
package/man/mailauth.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH "MAILAUTH" "1" "July 2023" "v4.4.1" "Mailauth Help"
1
+ .TH "MAILAUTH" "1" "July 2023" "v4.4.2" "Mailauth Help"
2
2
  .SH "NAME"
3
3
  \fBmailauth\fR
4
4
  .QP
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailauth",
3
- "version": "4.4.1",
3
+ "version": "4.5.0",
4
4
  "description": "Email authentication library for Node.js",
5
5
  "main": "lib/mailauth.js",
6
6
  "scripts": {
@@ -33,9 +33,9 @@
33
33
  "homepage": "https://github.com/postalsys/mailauth",
34
34
  "devDependencies": {
35
35
  "chai": "4.3.7",
36
- "eslint": "8.45.0",
36
+ "eslint": "8.46.0",
37
37
  "eslint-config-nodemailer": "1.2.0",
38
- "eslint-config-prettier": "8.8.0",
38
+ "eslint-config-prettier": "8.9.0",
39
39
  "js-yaml": "4.1.0",
40
40
  "license-report": "6.4.0",
41
41
  "marked": "0.7.0",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@postalsys/vmc": "1.0.6",
49
- "fast-xml-parser": "4.2.6",
49
+ "fast-xml-parser": "4.2.7",
50
50
  "ipaddr.js": "2.1.0",
51
51
  "joi": "17.9.2",
52
52
  "libmime": "5.2.1",
@@ -71,10 +71,10 @@
71
71
  "LICENSE.txt"
72
72
  ],
73
73
  "targets": [
74
- "node16-linux-x64",
75
- "node16-macos-x64",
74
+ "node18-linux-x64",
75
+ "node18-macos-x64",
76
76
  "node18-macos-arm64",
77
- "node16-win-x64"
77
+ "node18-win-x64"
78
78
  ],
79
79
  "outputPath": "ee-dist"
80
80
  }