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/.ncurc.js +1 -2
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +31 -0
- package/README.md +1 -1
- package/bin/mailauth.js +261 -401
- package/cli.md +14 -0
- package/docs/dmarc.md +28 -33
- package/index.d.ts +26 -7
- package/lib/arc/index.js +7 -4
- package/lib/commands/seal.js +68 -21
- package/lib/dmarc/get-dmarc-record.js +29 -19
- package/lib/dmarc/verify.js +17 -7
- package/lib/mailauth.js +9 -0
- package/lib/parse-dkim-headers.js +28 -8
- package/lib/parse-received.js +6 -0
- package/lib/safe-keys.js +10 -0
- package/lib/tools.js +27 -10
- package/package.json +22 -22
- package/CLAUDE.md +0 -66
package/bin/mailauth.js
CHANGED
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
-
const
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
.
|
|
395
|
-
|
|
396
|
-
'
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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('
|
|
286
|
+
console.error('Included Modules');
|
|
407
287
|
console.error('================');
|
|
408
288
|
|
|
409
|
-
console.error(
|
|
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
|
-
|
|
295
|
+
program.parse(process.argv);
|