mailauth 4.4.2 → 4.5.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/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
@@ -4,18 +4,19 @@ const crypto = require('crypto');
4
4
  const dns = require('dns');
5
5
  const { formatAuthHeaderRow, parseDkimHeaders, formatDomain, getAlignment } = require('../tools');
6
6
  const Joi = require('joi');
7
- const packageData = require('../../package.json');
7
+ //const packageData = require('../../package.json');
8
8
  const httpsSchema = Joi.string().uri({
9
9
  scheme: ['https']
10
10
  });
11
11
 
12
- const https = require('https');
13
- const http = require('http');
12
+ const FETCH_TIMEOUT = 5 * 1000;
13
+
14
+ const { fetch: fetchCmd, Agent } = require('undici');
15
+ const fetchAgent = new Agent({ connect: { timeout: FETCH_TIMEOUT } });
16
+
14
17
  const { vmc } = require('@postalsys/vmc');
15
18
  const { validateSvg } = require('./validate-svg');
16
19
 
17
- const HTTP_REQUEST_TIMEOUT = 15 * 1000;
18
-
19
20
  const lookup = async data => {
20
21
  let { dmarc, headers, resolver, bimiWithAlignedDkim } = data;
21
22
  let headerRows = (headers && headers.parsed) || [];
@@ -177,7 +178,7 @@ const lookup = async data => {
177
178
  return response;
178
179
  };
179
180
 
180
- const downloadPromise = (url, cachedFile) => {
181
+ const downloadPromise = async (url, cachedFile) => {
181
182
  if (cachedFile) {
182
183
  return cachedFile;
183
184
  }
@@ -186,76 +187,24 @@ const downloadPromise = (url, cachedFile) => {
186
187
  return false;
187
188
  }
188
189
 
189
- const parsedUrl = new URL(url);
190
-
191
- const options = {
192
- protocol: parsedUrl.protocol,
193
- host: parsedUrl.host,
190
+ let res = await fetchCmd(url, {
194
191
  headers: {
195
- host: parsedUrl.host,
196
- 'User-Agent': `mailauth/${packageData.version} (+${packageData.homepage}`
192
+ // Comment: AKAMAI does some strange UA based filtering that messes up the request
193
+ // 'User-Agent': `mailauth/${packageData.version} (+${packageData.homepage}`
197
194
  },
198
- servername: parsedUrl.hostname,
199
- port: 443,
200
- path: parsedUrl.pathname,
201
- method: 'GET',
202
- rejectUnauthorized: true,
203
-
204
- timeout: HTTP_REQUEST_TIMEOUT
205
- };
206
-
207
- return new Promise((resolve, reject) => {
208
- let protoHandler;
209
- switch (parsedUrl.protocol) {
210
- case 'https:':
211
- protoHandler = https;
212
- break;
213
- case 'http:':
214
- protoHandler = http;
215
- break;
216
- default:
217
- reject(new Error(`Unknown protocol ${parsedUrl.protocol}`));
218
- }
219
- const req = protoHandler.request(options, res => {
220
- let chunks = [],
221
- chunklen = 0;
222
- res.on('readable', () => {
223
- let chunk;
224
- while ((chunk = res.read()) !== null) {
225
- chunks.push(chunk);
226
- chunklen += chunk.length;
227
- }
228
- });
229
- res.on('end', () => {
230
- let data = Buffer.concat(chunks, chunklen);
231
- if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
232
- let err = new Error(`Invalid response code ${res.statusCode || '-'}`);
233
- err.code = 'http_status_' + (res.statusCode || 'na');
234
- if (res.headers.location && res.statusCode >= 300 && res.statusCode < 400) {
235
- err.redirect = {
236
- code: res.statusCode,
237
- location: res.headers.location
238
- };
239
- }
240
- return reject(err);
241
- }
242
- resolve(data);
243
- });
244
- res.on('error', err => reject(err));
245
- });
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
-
254
- req.on('error', err => {
255
- reject(err);
256
- });
257
- req.end();
195
+ dispatcher: fetchAgent
258
196
  });
197
+
198
+ if (!res.ok) {
199
+ let error = new Error(`Request failed with status ${res.status}`);
200
+ error.code = 'HTTP_REQUEST_FAILED';
201
+ throw error;
202
+ }
203
+
204
+ let ab = await res.arrayBuffer();
205
+ process.stdout.write(Buffer.from(ab));
206
+
207
+ return Buffer.from(ab);
259
208
  };
260
209
 
261
210
  const validateVMC = async (bimiData, opts) => {
@@ -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/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.7 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.5.0" "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.2",
3
+ "version": "4.5.1",
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.9.0",
38
+ "eslint-config-prettier": "8.10.0",
39
39
  "js-yaml": "4.1.0",
40
40
  "license-report": "6.4.0",
41
41
  "marked": "0.7.0",
@@ -46,13 +46,14 @@
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",
53
53
  "nodemailer": "6.9.4",
54
54
  "psl": "1.9.0",
55
55
  "punycode": "2.3.0",
56
+ "undici": "5.23.0",
56
57
  "yargs": "17.7.2"
57
58
  },
58
59
  "engines": {
@@ -71,10 +72,10 @@
71
72
  "LICENSE.txt"
72
73
  ],
73
74
  "targets": [
74
- "node16-linux-x64",
75
- "node16-macos-x64",
75
+ "node18-linux-x64",
76
+ "node18-macos-x64",
76
77
  "node18-macos-arm64",
77
- "node16-win-x64"
78
+ "node18-win-x64"
78
79
  ],
79
80
  "outputPath": "ee-dist"
80
81
  }