mailauth 4.13.3 → 5.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/bin/mailauth.js CHANGED
@@ -2,10 +2,8 @@
2
2
 
3
3
  'use strict';
4
4
 
5
- const yargs = require('yargs/yargs');
6
- const { hideBin } = require('yargs/helpers');
5
+ const { Command, InvalidArgumentError, Option } = require('commander');
7
6
  const os = require('node:os');
8
- const assert = require('node:assert');
9
7
 
10
8
  const commandReport = require('../lib/commands/report');
11
9
  const commandSign = require('../lib/commands/sign');
@@ -17,419 +15,281 @@ const commandBodyhash = require('../lib/commands/bodyhash');
17
15
  const fs = require('node:fs');
18
16
  const pathlib = require('node:path');
19
17
 
20
- const argv = yargs(hideBin(process.argv))
21
- .command(
22
- ['report [email]', '$0 [email]'],
23
- 'Validate an email message and return a detailed JSON report',
24
- yargs => {
25
- yargs
26
- .option('client-ip', {
27
- alias: 'i',
28
- type: 'string',
29
- description: 'IP address of the remote client (used for SPF checks). If not provided, it is parsed from the latest Received header.'
30
- })
31
- .option('mta', {
32
- alias: 'm',
33
- type: 'string',
34
- description:
35
- 'Hostname of the server performing the validation (used in the Authentication-Results header). Defaults to the local hostname.',
36
- default: os.hostname()
37
- })
38
- .option('helo', {
39
- alias: 'e',
40
- type: 'string',
41
- description: 'Client hostname from the HELO/EHLO command (used in some SPF checks).'
42
- })
43
- .option('sender', {
44
- alias: 'f',
45
- type: 'string',
46
- description: 'Email address from the MAIL FROM command. If not provided, the address from the latest Return-Path header is used.'
47
- })
48
- .option('dns-cache', {
49
- alias: 'n',
50
- type: 'string',
51
- description:
52
- 'Path to a JSON file with cached DNS responses. When provided, DNS queries use these cached responses instead of performing actual DNS lookups.'
53
- })
54
- .option('max-lookups', {
55
- alias: 'x',
56
- type: 'number',
57
- description: 'Maximum allowed DNS lookups during SPF checks. Defaults to 10.',
58
- default: 10
59
- })
60
- .option('max-void-lookups', {
61
- alias: 'z',
62
- type: 'number',
63
- description: 'Maximum allowed DNS lookups that return no data (void lookups) during SPF checks. Defaults to 2.',
64
- default: 2
65
- });
66
- yargs.positional('email', {
67
- describe: 'Path to the email message file in EML format. If not specified, the content is read from standard input.'
68
- });
69
- },
70
- argv => {
71
- commandReport(argv)
72
- .then(() => {
73
- process.exit();
74
- })
75
- .catch(err => {
76
- console.error('Failed to generate report for the input message.');
77
- console.error(err);
78
- process.exit(1);
79
- });
18
+ const packageData = require('../package.json');
19
+
20
+ // Commander hands every option value over as a string, so numeric options have
21
+ // to be coerced. A value that is not a number is rejected instead of being
22
+ // passed on: a non-numeric --max-lookups would disable the SPF lookup limit
23
+ // entirely, and a non-numeric --time would produce an empty t= tag.
24
+ const numberArg = value => {
25
+ let num = Number(value);
26
+ if (isNaN(num)) {
27
+ throw new InvalidArgumentError('Not a number.');
28
+ }
29
+ return num;
30
+ };
31
+
32
+ // Repeating a single-value option is a mistake worth reporting instead of
33
+ // silently keeping the last occurrence. Wraps whatever parser the option
34
+ // already has (`.choices()` installs one) so its validation still runs.
35
+ const rejectRepeated = option => {
36
+ let parseArg = option.parseArg;
37
+ return option.argParser((value, previous) => {
38
+ if (previous !== undefined) {
39
+ throw new InvalidArgumentError(`--${option.name()} can only be provided once`);
80
40
  }
81
- )
82
- .command(
83
- ['sign [email]'],
84
- 'Sign an email with a DKIM digital signature',
85
- yargs => {
86
- yargs
87
- .option('private-key', {
88
- alias: 'k',
89
- type: 'string',
90
- description: 'Path to the private key file used for signing.',
91
- demandOption: true
92
- })
93
- .option('domain', {
94
- alias: 'd',
95
- type: 'string',
96
- description: 'Domain name to use in the DKIM signature (d= tag).',
97
- demandOption: true
98
- })
99
- .option('selector', {
100
- alias: 's',
101
- type: 'string',
102
- description: 'Selector to use in the DKIM signature (s= tag).',
103
- demandOption: true
104
- })
105
- .option('algo', {
106
- alias: 'a',
107
- type: 'string',
108
- description: 'Signing algorithm. Defaults to "rsa-sha256" or "ed25519-sha256" depending on the private key type.',
109
- default: 'rsa-sha256'
110
- })
111
- .option('canonicalization', {
112
- alias: 'c',
113
- type: 'string',
114
- description: 'Canonicalization method (c= tag). Defaults to "relaxed/relaxed".',
115
- default: 'relaxed/relaxed'
116
- })
117
- .option('time', {
118
- alias: 't',
119
- type: 'number',
120
- description: 'Signing time as a UNIX timestamp (t= tag). Defaults to the current time.'
121
- })
122
- .option('body-length', {
123
- alias: 'l',
124
- type: 'number',
125
- description: 'Maximum length of the canonicalized body to include in the signature (l= tag). Not recommended for general use.'
126
- })
127
- .option('header-fields', {
128
- alias: 'h',
129
- type: 'string',
130
- description: 'Colon-separated list of header field names to include in the signature (h= tag).'
131
- })
132
- .option('headers-only', {
133
- alias: 'o',
134
- type: 'boolean',
135
- description: 'If set, outputs only the DKIM signature headers without the message body.'
136
- });
137
- yargs.positional('email', {
138
- describe: 'Path to the email message file in EML format. If not specified, the content is read from standard input.'
41
+ return parseArg ? parseArg(value, previous) : value;
42
+ });
43
+ };
44
+
45
+ // Wrap an async command implementation with the shared promise handling used by
46
+ // every subcommand: merge the positional `email` argument with the parsed
47
+ // options (including the global --verbose flag) into a single `argv` object.
48
+ const runCommand = (fn, failMessage) =>
49
+ function () {
50
+ let command = arguments[arguments.length - 1];
51
+ let positional = Array.prototype.slice.call(arguments, 0, arguments.length - 2);
52
+
53
+ let argv = Object.assign({}, command.optsWithGlobals());
54
+ if (command.registeredArguments && command.registeredArguments.length) {
55
+ command.registeredArguments.forEach((arg, i) => {
56
+ argv[arg.name()] = positional[i];
139
57
  });
140
- },
141
- argv => {
142
- commandSign(argv)
143
- .then(() => {
144
- process.exit();
145
- })
146
- .catch(err => {
147
- if (!err.suppress) {
148
- console.error('Failed to sign the input message.');
149
- console.error(err);
150
- }
151
- process.exit(1);
152
- });
153
58
  }
154
- )
155
- .command(
156
- ['seal [email]'],
157
- 'Authenticate and seal an email with an ARC digital signature',
158
- yargs => {
159
- yargs
160
- .option('private-key', {
161
- alias: 'k',
162
- type: 'string',
163
- description: 'Path to the private key file used for sealing.',
164
- demandOption: true
165
- })
166
- .option('domain', {
167
- alias: 'd',
168
- type: 'string',
169
- description: 'Domain name to use in the ARC seal (d= tag).',
170
- demandOption: true
171
- })
172
- .option('selector', {
173
- alias: 's',
174
- type: 'string',
175
- description: 'Selector to use in the ARC seal (s= tag).',
176
- demandOption: true
177
- })
178
- .option('algo', {
179
- alias: 'a',
180
- type: 'string',
181
- description:
182
- 'Sealing algorithm. Defaults to "rsa-sha256" or "ed25519-sha256" depending on the private key type. Note: RFC8617 only allows "rsa-sha256" (a= tag).',
183
- default: 'rsa-sha256'
184
- })
185
- .option('canonicalization', {
186
- alias: 'c',
187
- type: 'string',
188
- description: 'Canonicalization method. Note: RFC8617 only allows "relaxed/relaxed" (c= tag).',
189
- default: 'relaxed/relaxed'
190
- })
191
- .option('time', {
192
- alias: 't',
193
- type: 'number',
194
- description: 'Sealing time as a UNIX timestamp (t= tag). Defaults to the current time.'
195
- })
196
- .option('header-fields', {
197
- alias: 'h',
198
- type: 'string',
199
- description: 'Colon-separated list of header field names to include in the seal (h= tag).'
200
- })
201
- .option('client-ip', {
202
- alias: 'i',
203
- type: 'string',
204
- description: 'IP address of the remote client (used for SPF checks). If not provided, it is parsed from the latest Received header.'
205
- })
206
- .option('mta', {
207
- alias: 'm',
208
- type: 'string',
209
- description:
210
- 'Hostname of the server performing the validation (used in the Authentication-Results header). Defaults to the local hostname.',
211
- default: os.hostname()
212
- })
213
- .option('helo', {
214
- alias: 'e',
215
- type: 'string',
216
- description: 'Client hostname from the HELO/EHLO command (used in some SPF checks).'
217
- })
218
- .option('sender', {
219
- alias: 'f',
220
- type: 'string',
221
- description: 'Email address from the MAIL FROM command. If not provided, the address from the latest Return-Path header is used.'
222
- })
223
- .option('dns-cache', {
224
- alias: 'n',
225
- type: 'string',
226
- description:
227
- 'Path to a JSON file with cached DNS responses. When provided, DNS queries use these cached responses instead of performing actual DNS lookups.'
228
- })
229
- .option('headers-only', {
230
- alias: 'o',
231
- type: 'boolean',
232
- description: 'If set, outputs only the ARC seal headers without the message body.'
233
- });
234
- yargs.positional('email', {
235
- describe: 'Path to the email message file in EML format. If not specified, the content is read from standard input.'
59
+
60
+ fn(argv)
61
+ .then(() => {
62
+ process.exit();
63
+ })
64
+ .catch(err => {
65
+ if (!err || !err.suppress) {
66
+ console.error(failMessage);
67
+ console.error(err);
68
+ }
69
+ process.exit(1);
236
70
  });
237
- },
238
- argv => {
239
- commandSeal(argv)
240
- .then(() => {
241
- process.exit();
242
- })
243
- .catch(err => {
244
- if (!err.suppress) {
245
- console.error('Failed to seal the input message.');
246
- console.error(err);
247
- }
248
- process.exit(1);
249
- });
250
- }
71
+ };
72
+
73
+ const emailArgDescription = 'Path to the email message file in EML format. If not specified, the content is read from standard input.';
74
+
75
+ const program = new Command();
76
+
77
+ program
78
+ .name('mailauth')
79
+ .description('Email authentication tools for Node.js')
80
+ // yargs registered --version automatically, commander does not
81
+ .version(packageData.version, '--version', 'Show version number.')
82
+ .option('-v, --verbose', 'Enable verbose logging for debugging purposes.')
83
+ // sign/seal use -h as an alias for --header-fields, so free it up from the
84
+ // built-in help option and expose help via --help only.
85
+ .helpOption('--help', 'Show help.');
86
+
87
+ program
88
+ .command('report', { isDefault: true })
89
+ .description('Validate an email message and return a detailed JSON report')
90
+ .argument('[email]', emailArgDescription)
91
+ .helpOption('--help', 'Show help.')
92
+ .option('-i, --client-ip <ip>', 'IP address of the remote client (used for SPF checks). If not provided, it is parsed from the latest Received header.')
93
+ .option(
94
+ '-m, --mta <hostname>',
95
+ 'Hostname of the server performing the validation (used in the Authentication-Results header). Defaults to the local hostname.',
96
+ os.hostname()
251
97
  )
252
- .command(
253
- ['spf'],
254
- 'Validate SPF for an email address and MTA IP address',
255
- yargs => {
256
- yargs
257
- .option('sender', {
258
- alias: 'f',
259
- type: 'string',
260
- description: 'Email address from the MAIL FROM command.',
261
- demandOption: true
262
- })
263
- .option('client-ip', {
264
- alias: 'i',
265
- type: 'string',
266
- description: 'IP address of the remote client (used for SPF checks).',
267
- demandOption: true
268
- })
269
- .option('helo', {
270
- alias: 'e',
271
- type: 'string',
272
- description: 'Client hostname from the HELO/EHLO command (used in some SPF checks).'
273
- })
274
- .option('mta', {
275
- alias: 'm',
276
- type: 'string',
277
- description: 'Hostname of the server performing the SPF check (used in the Authentication-Results header). Defaults to the local hostname.',
278
- default: os.hostname()
279
- })
280
- .option('dns-cache', {
281
- alias: 'n',
282
- type: 'string',
283
- description:
284
- 'Path to a JSON file with cached DNS responses. When provided, DNS queries use these cached responses instead of performing actual DNS lookups.'
285
- })
286
- .option('headers-only', {
287
- alias: 'o',
288
- type: 'boolean',
289
- description: 'If set, outputs only the SPF authentication header.'
290
- })
291
- .option('max-lookups', {
292
- alias: 'x',
293
- type: 'number',
294
- description: 'Maximum allowed DNS lookups during SPF checks. Defaults to 10.',
295
- default: 10
296
- })
297
- .option('max-void-lookups', {
298
- alias: 'z',
299
- type: 'number',
300
- description: 'Maximum allowed DNS lookups that return no data (void lookups) during SPF checks. Defaults to 2.',
301
- default: 2
302
- });
303
- },
304
- argv => {
305
- commandSpf(argv)
306
- .then(() => {
307
- process.exit();
308
- })
309
- .catch(err => {
310
- console.error('Failed to verify SPF for the email address.');
311
- console.error(err);
312
- process.exit(1);
313
- });
314
- }
98
+ .option('-e, --helo <hostname>', 'Client hostname from the HELO/EHLO command (used in some SPF checks).')
99
+ .option('-f, --sender <address>', 'Email address from the MAIL FROM command. If not provided, the address from the latest Return-Path header is used.')
100
+ .option(
101
+ '-n, --dns-cache <file>',
102
+ 'Path to a JSON file with cached DNS responses. When provided, DNS queries use these cached responses instead of performing actual DNS lookups.'
315
103
  )
316
- .command(
317
- ['vmc'],
318
- 'Validate a Verified Mark Certificate (VMC) logo file',
319
- yargs => {
320
- yargs
321
- .option('authorityPath', {
322
- alias: 'p',
323
- type: 'string',
324
- description: 'Path to a local VMC file.'
325
- })
326
- .option('authority', {
327
- alias: 'a',
328
- type: 'string',
329
- description: 'URL of the VMC file.'
330
- })
331
- .option('domain', {
332
- alias: 'd',
333
- type: 'string',
334
- description: 'Sending domain to validate against the VMC.'
335
- })
336
- .option('date', {
337
- alias: 't',
338
- type: 'string',
339
- description: 'ISO-formatted timestamp to use for certificate expiration checks.'
340
- });
341
- },
342
- argv => {
343
- commandVmc(argv)
344
- .then(() => {
345
- process.exit();
346
- })
347
- .catch(err => {
348
- console.error('Failed to verify the VMC file.');
349
- console.error(err);
350
- process.exit(1);
351
- });
352
- }
104
+ .option('-x, --max-lookups <number>', 'Maximum allowed DNS lookups during SPF checks. Defaults to 10.', numberArg, 10)
105
+ .option('-z, --max-void-lookups <number>', 'Maximum allowed DNS lookups that return no data (void lookups) during SPF checks. Defaults to 2.', numberArg, 2)
106
+ .action(runCommand(commandReport, 'Failed to generate report for the input message.'));
107
+
108
+ program
109
+ .command('sign')
110
+ .description('Sign an email with a DKIM digital signature')
111
+ .argument('[email]', emailArgDescription)
112
+ .helpOption('--help', 'Show help.')
113
+ .requiredOption('-k, --private-key <file>', 'Path to the private key file used for signing.')
114
+ .requiredOption('-d, --domain <domain>', 'Domain name to use in the DKIM signature (d= tag).')
115
+ .requiredOption('-s, --selector <selector>', 'Selector to use in the DKIM signature (s= tag).')
116
+ .option('-a, --algo <algorithm>', 'Signing algorithm. Defaults to "rsa-sha256" or "ed25519-sha256" depending on the private key type.', 'rsa-sha256')
117
+ .option('-c, --canonicalization <method>', 'Canonicalization method (c= tag). Defaults to "relaxed/relaxed".', 'relaxed/relaxed')
118
+ .option('-t, --time <timestamp>', 'Signing time as a UNIX timestamp (t= tag). Defaults to the current time.', numberArg)
119
+ .option(
120
+ '-l, --body-length <number>',
121
+ 'Maximum length of the canonicalized body to include in the signature (l= tag). Not recommended for general use.',
122
+ numberArg
353
123
  )
354
- .command(
355
- ['bodyhash [email]'],
356
- 'Generate a DKIM body hash for an email message',
357
- yargs => {
358
- yargs
359
- .option('algo', {
360
- alias: 'a',
361
- type: 'string',
362
- description: 'Hashing algorithm to use. Defaults to "sha256". Can also use DKIM-style algorithms like "rsa-sha256".',
363
- default: 'sha256'
364
- })
365
- .option('canonicalization', {
366
- alias: 'c',
367
- type: 'string',
368
- description: 'Body canonicalization method (c= tag). Defaults to "relaxed". Can use DKIM-style formats like "relaxed/relaxed".',
369
- default: 'relaxed'
370
- })
371
- .option('body-length', {
372
- alias: 'l',
373
- type: 'number',
374
- description: 'Maximum length of the canonicalized body to include in the hash (l= tag).'
375
- });
376
- yargs.positional('email', {
377
- describe: 'Path to the email message file in EML format. If not specified, the content is read from standard input.'
378
- });
379
- },
380
- argv => {
381
- commandBodyhash(argv)
382
- .then(() => {
383
- process.exit();
384
- })
385
- .catch(err => {
386
- if (!err.suppress) {
387
- console.error('Failed to calculate the body hash for the input message.');
388
- console.error(err);
389
- }
390
- process.exit(1);
391
- });
124
+ .option('-h, --header-fields <fields>', 'Colon-separated list of header field names to include in the signature (h= tag).')
125
+ .option('-o, --headers-only', 'If set, outputs only the DKIM signature headers without the message body.')
126
+ .action(runCommand(commandSign, 'Failed to sign the input message.'));
127
+
128
+ program
129
+ .command('seal')
130
+ .description('Authenticate and seal an email with an ARC digital signature')
131
+ .argument('[email]', emailArgDescription)
132
+ .helpOption('--help', 'Show help.')
133
+ .requiredOption('-k, --private-key <file>', 'Path to the private key file used for sealing.')
134
+ .requiredOption('-d, --domain <domain>', 'Domain name to use in the ARC seal (d= tag).')
135
+ .requiredOption('-s, --selector <selector>', 'Selector to use in the ARC seal (s= tag).')
136
+ .option(
137
+ '-a, --algo <algorithm>',
138
+ 'Sealing algorithm. Defaults to "rsa-sha256" or "ed25519-sha256" depending on the private key type. Note: RFC8617 only allows "rsa-sha256" (a= tag).',
139
+ 'rsa-sha256'
140
+ )
141
+ .option('-c, --canonicalization <method>', 'Canonicalization method. Note: RFC8617 only allows "relaxed/relaxed" (c= tag).', 'relaxed/relaxed')
142
+ .option('-t, --time <timestamp>', 'Sealing time as a UNIX timestamp (t= tag). Defaults to the current time.', numberArg)
143
+ .option('-h, --header-fields <fields>', 'Colon-separated list of header field names to include in the seal (h= tag).')
144
+ .option('-i, --client-ip <ip>', 'IP address of the remote client (used for SPF checks). If not provided, it is parsed from the latest Received header.')
145
+ .option(
146
+ '-m, --mta <hostname>',
147
+ 'Hostname of the server performing the validation (used in the Authentication-Results header). Defaults to the local hostname.',
148
+ os.hostname()
149
+ )
150
+ .option('-e, --helo <hostname>', 'Client hostname from the HELO/EHLO command (used in some SPF checks).')
151
+ .option('-f, --sender <address>', 'Email address from the MAIL FROM command. If not provided, the address from the latest Return-Path header is used.')
152
+ .option(
153
+ '-n, --dns-cache <file>',
154
+ 'Path to a JSON file with cached DNS responses. When provided, DNS queries use these cached responses instead of performing actual DNS lookups.'
155
+ )
156
+ .option('-o, --headers-only', 'If set, outputs only the ARC seal headers without the message body.')
157
+ .addOption(
158
+ rejectRepeated(
159
+ new Option(
160
+ '--auth-results <value>',
161
+ 'Authentication-Results value to embed in the ARC-Authentication-Results header (the part after "i=N;"). When set, the message is sealed using this value as is and no authentication checks are performed.'
162
+ ).conflicts('authResultsFile')
163
+ )
164
+ )
165
+ .addOption(
166
+ rejectRepeated(
167
+ new Option(
168
+ '--auth-results-file <file>',
169
+ 'Path to a file containing the Authentication-Results value. Same as --auth-results, but read from a file.'
170
+ )
171
+ )
172
+ )
173
+ .addOption(
174
+ rejectRepeated(
175
+ new Option(
176
+ '--cv <status>',
177
+ 'Chain validation status for the ARC-Seal header (cv= tag). Only used together with --auth-results or --auth-results-file. Defaults to "none".'
178
+ ).choices(['none', 'pass', 'fail'])
179
+ )
180
+ )
181
+ .addOption(
182
+ rejectRepeated(
183
+ new Option(
184
+ '--instance <number>',
185
+ 'ARC instance number (i= tag). Only used together with --auth-results or --auth-results-file. Defaults to the next instance number based on the existing ARC chain, or 1.'
186
+ ).argParser(numberArg)
187
+ )
188
+ )
189
+ .hook('preAction', command => {
190
+ let opts = command.opts();
191
+ let fromCli = key => command.getOptionValueSource(key) === 'cli';
192
+
193
+ // a missing value would silently flip the command back into full
194
+ // authentication mode, the opposite of what the caller asked for
195
+ for (let [key, flag] of [
196
+ ['authResults', '--auth-results'],
197
+ ['authResultsFile', '--auth-results-file']
198
+ ]) {
199
+ if (fromCli(key) && (typeof opts[key] !== 'string' || !opts[key].trim())) {
200
+ command.error(`error: ${flag} must not be empty`);
201
+ }
392
202
  }
203
+
204
+ // .implies() cannot express "requires one of", so enforce it here
205
+ if (!opts.authResults && !opts.authResultsFile) {
206
+ for (let key of ['cv', 'instance']) {
207
+ if (fromCli(key)) {
208
+ command.error(`error: --${key} can only be used together with --auth-results or --auth-results-file`);
209
+ }
210
+ }
211
+ }
212
+ })
213
+ .action(runCommand(commandSeal, 'Failed to seal the input message.'));
214
+
215
+ program
216
+ .command('spf')
217
+ .description('Validate SPF for an email address and MTA IP address')
218
+ .helpOption('--help', 'Show help.')
219
+ .requiredOption('-f, --sender <address>', 'Email address from the MAIL FROM command.')
220
+ .requiredOption('-i, --client-ip <ip>', 'IP address of the remote client (used for SPF checks).')
221
+ .option('-e, --helo <hostname>', 'Client hostname from the HELO/EHLO command (used in some SPF checks).')
222
+ .option(
223
+ '-m, --mta <hostname>',
224
+ 'Hostname of the server performing the SPF check (used in the Authentication-Results header). Defaults to the local hostname.',
225
+ os.hostname()
393
226
  )
394
- .command(
395
- ['license'],
396
- 'Display license information for mailauth and included modules',
397
- () => false,
398
- () => {
399
- fs.readFile(pathlib.join(__dirname, '..', 'LICENSE.txt'), (err, license) => {
227
+ .option(
228
+ '-n, --dns-cache <file>',
229
+ 'Path to a JSON file with cached DNS responses. When provided, DNS queries use these cached responses instead of performing actual DNS lookups.'
230
+ )
231
+ .option('-o, --headers-only', 'If set, outputs only the SPF authentication header.')
232
+ .option('-x, --max-lookups <number>', 'Maximum allowed DNS lookups during SPF checks. Defaults to 10.', numberArg, 10)
233
+ .option('-z, --max-void-lookups <number>', 'Maximum allowed DNS lookups that return no data (void lookups) during SPF checks. Defaults to 2.', numberArg, 2)
234
+ .action(runCommand(commandSpf, 'Failed to verify SPF for the email address.'));
235
+
236
+ program
237
+ .command('vmc')
238
+ .description('Validate a Verified Mark Certificate (VMC) logo file')
239
+ .helpOption('--help', 'Show help.')
240
+ .option('-p, --authorityPath <file>', 'Path to a local VMC file.')
241
+ .option('-a, --authority <url>', 'URL of the VMC file.')
242
+ .option('-d, --domain <domain>', 'Sending domain to validate against the VMC.')
243
+ .option('-t, --date <timestamp>', 'ISO-formatted timestamp to use for certificate expiration checks.')
244
+ .action(runCommand(commandVmc, 'Failed to verify the VMC file.'));
245
+
246
+ program
247
+ .command('bodyhash')
248
+ .description('Generate a DKIM body hash for an email message')
249
+ .argument('[email]', emailArgDescription)
250
+ .helpOption('--help', 'Show help.')
251
+ .option('-a, --algo <algorithm>', 'Hashing algorithm to use. Defaults to "sha256". Can also use DKIM-style algorithms like "rsa-sha256".', 'sha256')
252
+ .option(
253
+ '-c, --canonicalization <method>',
254
+ 'Body canonicalization method (c= tag). Defaults to "relaxed". Can use DKIM-style formats like "relaxed/relaxed".',
255
+ 'relaxed'
256
+ )
257
+ .option('-l, --body-length <number>', 'Maximum length of the canonicalized body to include in the hash (l= tag).', numberArg)
258
+ .action(runCommand(commandBodyhash, 'Failed to calculate the body hash for the input message.'));
259
+
260
+ program
261
+ .command('license')
262
+ .description('Display license information for mailauth and included modules')
263
+ .helpOption('--help', 'Show help.')
264
+ .action(() => {
265
+ fs.readFile(pathlib.join(__dirname, '..', 'LICENSE.txt'), (err, license) => {
266
+ if (err) {
267
+ console.error('Failed to load license information.');
268
+ console.error(err);
269
+ return process.exit(1);
270
+ }
271
+
272
+ console.error('mailauth License');
273
+ console.error('================');
274
+
275
+ console.error(license.toString().trim());
276
+
277
+ console.error('');
278
+
279
+ fs.readFile(pathlib.join(__dirname, '..', 'licenses.txt'), (err, data) => {
400
280
  if (err) {
401
- console.error('Failed to load license information.');
281
+ console.error('Failed to load included modules license information.');
402
282
  console.error(err);
403
283
  return process.exit(1);
404
284
  }
405
285
 
406
- console.error('mailauth License');
286
+ console.error('Included Modules');
407
287
  console.error('================');
408
288
 
409
- console.error(license.toString().trim());
410
-
411
- console.error('');
412
-
413
- fs.readFile(pathlib.join(__dirname, '..', 'licenses.txt'), (err, data) => {
414
- if (err) {
415
- console.error('Failed to load included modules license information.');
416
- console.error(err);
417
- return process.exit(1);
418
- }
419
-
420
- console.error('Included Modules');
421
- console.error('================');
422
-
423
- console.error(data.toString().trim());
424
- process.exit();
425
- });
289
+ console.error(data.toString().trim());
290
+ process.exit();
426
291
  });
427
- }
428
- )
429
- .option('verbose', {
430
- alias: 'v',
431
- type: 'boolean',
432
- description: 'Enable verbose logging for debugging purposes.'
433
- }).argv;
292
+ });
293
+ });
434
294
 
435
- assert.ok(argv);
295
+ program.parse(process.argv);