mailauth 4.7.3 → 4.8.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/CHANGELOG.md +14 -0
- package/README.md +409 -324
- package/bin/mailauth.js +68 -72
- package/cli.md +278 -244
- package/man/mailauth.1 +1 -1
- package/package.json +9 -9
package/bin/mailauth.js
CHANGED
|
@@ -20,49 +20,51 @@ const pathlib = require('node:path');
|
|
|
20
20
|
const argv = yargs(hideBin(process.argv))
|
|
21
21
|
.command(
|
|
22
22
|
['report [email]', '$0 [email]'],
|
|
23
|
-
'Validate email message and return a
|
|
23
|
+
'Validate an email message and return a detailed JSON report',
|
|
24
24
|
yargs => {
|
|
25
25
|
yargs
|
|
26
26
|
.option('client-ip', {
|
|
27
27
|
alias: 'i',
|
|
28
28
|
type: 'string',
|
|
29
|
-
description: '
|
|
29
|
+
description: 'IP address of the remote client (used for SPF checks). If not provided, it is parsed from the latest Received header.'
|
|
30
30
|
})
|
|
31
31
|
.option('mta', {
|
|
32
32
|
alias: 'm',
|
|
33
33
|
type: 'string',
|
|
34
|
-
description:
|
|
34
|
+
description:
|
|
35
|
+
'Hostname of the server performing the validation (used in the Authentication-Results header). Defaults to the local hostname.',
|
|
35
36
|
default: os.hostname()
|
|
36
37
|
})
|
|
37
38
|
.option('helo', {
|
|
38
39
|
alias: 'e',
|
|
39
40
|
type: 'string',
|
|
40
|
-
description: 'Client hostname from the EHLO
|
|
41
|
+
description: 'Client hostname from the HELO/EHLO command (used in some SPF checks).'
|
|
41
42
|
})
|
|
42
43
|
.option('sender', {
|
|
43
44
|
alias: 'f',
|
|
44
45
|
type: 'string',
|
|
45
|
-
description: 'Email address from the MAIL FROM command. If not
|
|
46
|
+
description: 'Email address from the MAIL FROM command. If not provided, the address from the latest Return-Path header is used.'
|
|
46
47
|
})
|
|
47
48
|
.option('dns-cache', {
|
|
48
49
|
alias: 'n',
|
|
49
50
|
type: 'string',
|
|
50
|
-
description:
|
|
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.'
|
|
51
53
|
})
|
|
52
54
|
.option('max-lookups', {
|
|
53
55
|
alias: 'x',
|
|
54
56
|
type: 'number',
|
|
55
|
-
description: 'Maximum allowed DNS lookups',
|
|
57
|
+
description: 'Maximum allowed DNS lookups during SPF checks. Defaults to 10.',
|
|
56
58
|
default: 10
|
|
57
59
|
})
|
|
58
60
|
.option('max-void-lookups', {
|
|
59
61
|
alias: 'z',
|
|
60
62
|
type: 'number',
|
|
61
|
-
description: 'Maximum allowed
|
|
63
|
+
description: 'Maximum allowed DNS lookups that return no data (void lookups) during SPF checks. Defaults to 2.',
|
|
62
64
|
default: 2
|
|
63
65
|
});
|
|
64
66
|
yargs.positional('email', {
|
|
65
|
-
describe: 'Path to the email message file in EML format. If not specified
|
|
67
|
+
describe: 'Path to the email message file in EML format. If not specified, the content is read from standard input.'
|
|
66
68
|
});
|
|
67
69
|
},
|
|
68
70
|
argv => {
|
|
@@ -71,7 +73,7 @@ const argv = yargs(hideBin(process.argv))
|
|
|
71
73
|
process.exit();
|
|
72
74
|
})
|
|
73
75
|
.catch(err => {
|
|
74
|
-
console.error('Failed to generate report for input message');
|
|
76
|
+
console.error('Failed to generate report for the input message.');
|
|
75
77
|
console.error(err);
|
|
76
78
|
process.exit(1);
|
|
77
79
|
});
|
|
@@ -82,59 +84,58 @@ const argv = yargs(hideBin(process.argv))
|
|
|
82
84
|
'Sign an email with a DKIM digital signature',
|
|
83
85
|
yargs => {
|
|
84
86
|
yargs
|
|
85
|
-
|
|
86
87
|
.option('private-key', {
|
|
87
88
|
alias: 'k',
|
|
88
89
|
type: 'string',
|
|
89
|
-
description: 'Path to
|
|
90
|
+
description: 'Path to the private key file used for signing.',
|
|
90
91
|
demandOption: true
|
|
91
92
|
})
|
|
92
93
|
.option('domain', {
|
|
93
94
|
alias: 'd',
|
|
94
95
|
type: 'string',
|
|
95
|
-
description: 'Domain name
|
|
96
|
+
description: 'Domain name to use in the DKIM signature (d= tag).',
|
|
96
97
|
demandOption: true
|
|
97
98
|
})
|
|
98
99
|
.option('selector', {
|
|
99
100
|
alias: 's',
|
|
100
101
|
type: 'string',
|
|
101
|
-
description: '
|
|
102
|
+
description: 'Selector to use in the DKIM signature (s= tag).',
|
|
102
103
|
demandOption: true
|
|
103
104
|
})
|
|
104
105
|
.option('algo', {
|
|
105
106
|
alias: 'a',
|
|
106
107
|
type: 'string',
|
|
107
|
-
description: 'Signing algorithm. Defaults
|
|
108
|
+
description: 'Signing algorithm. Defaults to "rsa-sha256" or "ed25519-sha256" depending on the private key type.',
|
|
108
109
|
default: 'rsa-sha256'
|
|
109
110
|
})
|
|
110
111
|
.option('canonicalization', {
|
|
111
112
|
alias: 'c',
|
|
112
113
|
type: 'string',
|
|
113
|
-
description: 'Canonicalization
|
|
114
|
+
description: 'Canonicalization method (c= tag). Defaults to "relaxed/relaxed".',
|
|
114
115
|
default: 'relaxed/relaxed'
|
|
115
116
|
})
|
|
116
117
|
.option('time', {
|
|
117
118
|
alias: 't',
|
|
118
119
|
type: 'number',
|
|
119
|
-
description: 'Signing time as a
|
|
120
|
+
description: 'Signing time as a UNIX timestamp (t= tag). Defaults to the current time.'
|
|
120
121
|
})
|
|
121
122
|
.option('body-length', {
|
|
122
123
|
alias: 'l',
|
|
123
124
|
type: 'number',
|
|
124
|
-
description: 'Maximum length of
|
|
125
|
+
description: 'Maximum length of the canonicalized body to include in the signature (l= tag). Not recommended for general use.'
|
|
125
126
|
})
|
|
126
127
|
.option('header-fields', {
|
|
127
128
|
alias: 'h',
|
|
128
129
|
type: 'string',
|
|
129
|
-
description: 'Colon
|
|
130
|
+
description: 'Colon-separated list of header field names to include in the signature (h= tag).'
|
|
130
131
|
})
|
|
131
132
|
.option('headers-only', {
|
|
132
133
|
alias: 'o',
|
|
133
134
|
type: 'boolean',
|
|
134
|
-
description: '
|
|
135
|
+
description: 'If set, outputs only the DKIM signature headers without the message body.'
|
|
135
136
|
});
|
|
136
137
|
yargs.positional('email', {
|
|
137
|
-
describe: 'Path to the email message file in EML format. If not specified
|
|
138
|
+
describe: 'Path to the email message file in EML format. If not specified, the content is read from standard input.'
|
|
138
139
|
});
|
|
139
140
|
},
|
|
140
141
|
argv => {
|
|
@@ -144,7 +145,7 @@ const argv = yargs(hideBin(process.argv))
|
|
|
144
145
|
})
|
|
145
146
|
.catch(err => {
|
|
146
147
|
if (!err.suppress) {
|
|
147
|
-
console.error('Failed to sign input message');
|
|
148
|
+
console.error('Failed to sign the input message.');
|
|
148
149
|
console.error(err);
|
|
149
150
|
}
|
|
150
151
|
process.exit(1);
|
|
@@ -153,83 +154,85 @@ const argv = yargs(hideBin(process.argv))
|
|
|
153
154
|
)
|
|
154
155
|
.command(
|
|
155
156
|
['seal [email]'],
|
|
156
|
-
'
|
|
157
|
+
'Authenticate and seal an email with an ARC digital signature',
|
|
157
158
|
yargs => {
|
|
158
159
|
yargs
|
|
159
160
|
.option('private-key', {
|
|
160
161
|
alias: 'k',
|
|
161
162
|
type: 'string',
|
|
162
|
-
description: 'Path to
|
|
163
|
+
description: 'Path to the private key file used for sealing.',
|
|
163
164
|
demandOption: true
|
|
164
165
|
})
|
|
165
166
|
.option('domain', {
|
|
166
167
|
alias: 'd',
|
|
167
168
|
type: 'string',
|
|
168
|
-
description: 'Domain name
|
|
169
|
+
description: 'Domain name to use in the ARC seal (d= tag).',
|
|
169
170
|
demandOption: true
|
|
170
171
|
})
|
|
171
172
|
.option('selector', {
|
|
172
173
|
alias: 's',
|
|
173
174
|
type: 'string',
|
|
174
|
-
description: '
|
|
175
|
+
description: 'Selector to use in the ARC seal (s= tag).',
|
|
175
176
|
demandOption: true
|
|
176
177
|
})
|
|
177
178
|
.option('algo', {
|
|
178
179
|
alias: 'a',
|
|
179
180
|
type: 'string',
|
|
180
181
|
description:
|
|
181
|
-
'Sealing algorithm. Defaults
|
|
182
|
+
'Sealing algorithm. Defaults to "rsa-sha256" or "ed25519-sha256" depending on the private key type. Note: RFC8617 only allows "rsa-sha256" (a= tag).',
|
|
182
183
|
default: 'rsa-sha256'
|
|
183
184
|
})
|
|
184
185
|
.option('canonicalization', {
|
|
185
186
|
alias: 'c',
|
|
186
187
|
type: 'string',
|
|
187
|
-
description: 'Canonicalization
|
|
188
|
+
description: 'Canonicalization method. Note: RFC8617 only allows "relaxed/relaxed" (c= tag).',
|
|
188
189
|
default: 'relaxed/relaxed'
|
|
189
190
|
})
|
|
190
191
|
.option('time', {
|
|
191
192
|
alias: 't',
|
|
192
193
|
type: 'number',
|
|
193
|
-
description: '
|
|
194
|
+
description: 'Sealing time as a UNIX timestamp (t= tag). Defaults to the current time.'
|
|
194
195
|
})
|
|
195
196
|
.option('header-fields', {
|
|
196
197
|
alias: 'h',
|
|
197
198
|
type: 'string',
|
|
198
|
-
description: 'Colon
|
|
199
|
+
description: 'Colon-separated list of header field names to include in the seal (h= tag).'
|
|
199
200
|
})
|
|
200
201
|
.option('client-ip', {
|
|
201
202
|
alias: 'i',
|
|
202
203
|
type: 'string',
|
|
203
|
-
description: '
|
|
204
|
+
description: 'IP address of the remote client (used for SPF checks). If not provided, it is parsed from the latest Received header.'
|
|
204
205
|
})
|
|
205
206
|
.option('mta', {
|
|
206
207
|
alias: 'm',
|
|
207
208
|
type: 'string',
|
|
208
|
-
description:
|
|
209
|
+
description:
|
|
210
|
+
'Hostname of the server performing the validation (used in the Authentication-Results header). Defaults to the local hostname.',
|
|
209
211
|
default: os.hostname()
|
|
210
212
|
})
|
|
211
213
|
.option('helo', {
|
|
212
214
|
alias: 'e',
|
|
213
215
|
type: 'string',
|
|
214
|
-
description: 'Client hostname from the EHLO
|
|
216
|
+
description: 'Client hostname from the HELO/EHLO command (used in some SPF checks).'
|
|
215
217
|
})
|
|
216
218
|
.option('sender', {
|
|
217
219
|
alias: 'f',
|
|
218
220
|
type: 'string',
|
|
219
|
-
description: 'Email address from the MAIL FROM command. If not
|
|
221
|
+
description: 'Email address from the MAIL FROM command. If not provided, the address from the latest Return-Path header is used.'
|
|
220
222
|
})
|
|
221
223
|
.option('dns-cache', {
|
|
222
224
|
alias: 'n',
|
|
223
225
|
type: 'string',
|
|
224
|
-
description:
|
|
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.'
|
|
225
228
|
})
|
|
226
229
|
.option('headers-only', {
|
|
227
230
|
alias: 'o',
|
|
228
231
|
type: 'boolean',
|
|
229
|
-
description: '
|
|
232
|
+
description: 'If set, outputs only the ARC seal headers without the message body.'
|
|
230
233
|
});
|
|
231
234
|
yargs.positional('email', {
|
|
232
|
-
describe: 'Path to the email message file in EML format. If not specified
|
|
235
|
+
describe: 'Path to the email message file in EML format. If not specified, the content is read from standard input.'
|
|
233
236
|
});
|
|
234
237
|
},
|
|
235
238
|
argv => {
|
|
@@ -239,7 +242,7 @@ const argv = yargs(hideBin(process.argv))
|
|
|
239
242
|
})
|
|
240
243
|
.catch(err => {
|
|
241
244
|
if (!err.suppress) {
|
|
242
|
-
console.error('Failed to
|
|
245
|
+
console.error('Failed to seal the input message.');
|
|
243
246
|
console.error(err);
|
|
244
247
|
}
|
|
245
248
|
process.exit(1);
|
|
@@ -254,46 +257,47 @@ const argv = yargs(hideBin(process.argv))
|
|
|
254
257
|
.option('sender', {
|
|
255
258
|
alias: 'f',
|
|
256
259
|
type: 'string',
|
|
257
|
-
description: 'Email address from the MAIL FROM command',
|
|
260
|
+
description: 'Email address from the MAIL FROM command.',
|
|
258
261
|
demandOption: true
|
|
259
262
|
})
|
|
260
263
|
.option('client-ip', {
|
|
261
264
|
alias: 'i',
|
|
262
265
|
type: 'string',
|
|
263
|
-
description: '
|
|
266
|
+
description: 'IP address of the remote client (used for SPF checks).',
|
|
264
267
|
demandOption: true
|
|
265
268
|
})
|
|
266
269
|
.option('helo', {
|
|
267
270
|
alias: 'e',
|
|
268
271
|
type: 'string',
|
|
269
|
-
description: 'Client hostname from the EHLO
|
|
272
|
+
description: 'Client hostname from the HELO/EHLO command (used in some SPF checks).'
|
|
270
273
|
})
|
|
271
274
|
.option('mta', {
|
|
272
275
|
alias: 'm',
|
|
273
276
|
type: 'string',
|
|
274
|
-
description: 'Hostname of
|
|
277
|
+
description: 'Hostname of the server performing the SPF check (used in the Authentication-Results header). Defaults to the local hostname.',
|
|
275
278
|
default: os.hostname()
|
|
276
279
|
})
|
|
277
280
|
.option('dns-cache', {
|
|
278
281
|
alias: 'n',
|
|
279
282
|
type: 'string',
|
|
280
|
-
description:
|
|
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.'
|
|
281
285
|
})
|
|
282
286
|
.option('headers-only', {
|
|
283
287
|
alias: 'o',
|
|
284
288
|
type: 'boolean',
|
|
285
|
-
description: '
|
|
289
|
+
description: 'If set, outputs only the SPF authentication header.'
|
|
286
290
|
})
|
|
287
291
|
.option('max-lookups', {
|
|
288
292
|
alias: 'x',
|
|
289
293
|
type: 'number',
|
|
290
|
-
description: 'Maximum allowed DNS lookups',
|
|
294
|
+
description: 'Maximum allowed DNS lookups during SPF checks. Defaults to 10.',
|
|
291
295
|
default: 10
|
|
292
296
|
})
|
|
293
297
|
.option('max-void-lookups', {
|
|
294
298
|
alias: 'z',
|
|
295
299
|
type: 'number',
|
|
296
|
-
description: 'Maximum allowed
|
|
300
|
+
description: 'Maximum allowed DNS lookups that return no data (void lookups) during SPF checks. Defaults to 2.',
|
|
297
301
|
default: 2
|
|
298
302
|
});
|
|
299
303
|
},
|
|
@@ -303,7 +307,7 @@ const argv = yargs(hideBin(process.argv))
|
|
|
303
307
|
process.exit();
|
|
304
308
|
})
|
|
305
309
|
.catch(err => {
|
|
306
|
-
console.error('Failed to verify SPF for
|
|
310
|
+
console.error('Failed to verify SPF for the email address.');
|
|
307
311
|
console.error(err);
|
|
308
312
|
process.exit(1);
|
|
309
313
|
});
|
|
@@ -311,32 +315,28 @@ const argv = yargs(hideBin(process.argv))
|
|
|
311
315
|
)
|
|
312
316
|
.command(
|
|
313
317
|
['vmc'],
|
|
314
|
-
'Validate VMC logo',
|
|
318
|
+
'Validate a Verified Mark Certificate (VMC) logo file',
|
|
315
319
|
yargs => {
|
|
316
320
|
yargs
|
|
317
321
|
.option('authorityPath', {
|
|
318
322
|
alias: 'p',
|
|
319
323
|
type: 'string',
|
|
320
|
-
description: 'Path to a VMC file'
|
|
321
|
-
demandOption: false
|
|
324
|
+
description: 'Path to a local VMC file.'
|
|
322
325
|
})
|
|
323
326
|
.option('authority', {
|
|
324
327
|
alias: 'a',
|
|
325
328
|
type: 'string',
|
|
326
|
-
description: 'URL
|
|
327
|
-
demandOption: false
|
|
329
|
+
description: 'URL of the VMC file.'
|
|
328
330
|
})
|
|
329
331
|
.option('domain', {
|
|
330
332
|
alias: 'd',
|
|
331
333
|
type: 'string',
|
|
332
|
-
description: 'Sending domain to validate'
|
|
333
|
-
demandOption: false
|
|
334
|
+
description: 'Sending domain to validate against the VMC.'
|
|
334
335
|
})
|
|
335
336
|
.option('date', {
|
|
336
337
|
alias: 't',
|
|
337
338
|
type: 'string',
|
|
338
|
-
description: 'ISO
|
|
339
|
-
demandOption: false
|
|
339
|
+
description: 'ISO-formatted timestamp to use for certificate expiration checks.'
|
|
340
340
|
});
|
|
341
341
|
},
|
|
342
342
|
argv => {
|
|
@@ -345,7 +345,7 @@ const argv = yargs(hideBin(process.argv))
|
|
|
345
345
|
process.exit();
|
|
346
346
|
})
|
|
347
347
|
.catch(err => {
|
|
348
|
-
console.error('Failed to verify VMC file');
|
|
348
|
+
console.error('Failed to verify the VMC file.');
|
|
349
349
|
console.error(err);
|
|
350
350
|
process.exit(1);
|
|
351
351
|
});
|
|
@@ -353,32 +353,28 @@ const argv = yargs(hideBin(process.argv))
|
|
|
353
353
|
)
|
|
354
354
|
.command(
|
|
355
355
|
['bodyhash [email]'],
|
|
356
|
-
'Generate a
|
|
356
|
+
'Generate a DKIM body hash for an email message',
|
|
357
357
|
yargs => {
|
|
358
358
|
yargs
|
|
359
|
-
|
|
360
359
|
.option('algo', {
|
|
361
360
|
alias: 'a',
|
|
362
361
|
type: 'string',
|
|
363
|
-
description: 'Hashing algorithm. Defaults to sha256.',
|
|
362
|
+
description: 'Hashing algorithm to use. Defaults to "sha256". Can also use DKIM-style algorithms like "rsa-sha256".',
|
|
364
363
|
default: 'sha256'
|
|
365
364
|
})
|
|
366
|
-
|
|
367
365
|
.option('canonicalization', {
|
|
368
366
|
alias: 'c',
|
|
369
367
|
type: 'string',
|
|
370
|
-
description: '
|
|
368
|
+
description: 'Body canonicalization method (c= tag). Defaults to "relaxed". Can use DKIM-style formats like "relaxed/relaxed".',
|
|
371
369
|
default: 'relaxed'
|
|
372
370
|
})
|
|
373
|
-
|
|
374
371
|
.option('body-length', {
|
|
375
372
|
alias: 'l',
|
|
376
373
|
type: 'number',
|
|
377
|
-
description: 'Maximum length of
|
|
374
|
+
description: 'Maximum length of the canonicalized body to include in the hash (l= tag).'
|
|
378
375
|
});
|
|
379
|
-
|
|
380
376
|
yargs.positional('email', {
|
|
381
|
-
describe: 'Path to the email message file in EML format. If not specified
|
|
377
|
+
describe: 'Path to the email message file in EML format. If not specified, the content is read from standard input.'
|
|
382
378
|
});
|
|
383
379
|
},
|
|
384
380
|
argv => {
|
|
@@ -388,7 +384,7 @@ const argv = yargs(hideBin(process.argv))
|
|
|
388
384
|
})
|
|
389
385
|
.catch(err => {
|
|
390
386
|
if (!err.suppress) {
|
|
391
|
-
console.error('Failed to calculate body hash for the input message');
|
|
387
|
+
console.error('Failed to calculate the body hash for the input message.');
|
|
392
388
|
console.error(err);
|
|
393
389
|
}
|
|
394
390
|
process.exit(1);
|
|
@@ -397,17 +393,17 @@ const argv = yargs(hideBin(process.argv))
|
|
|
397
393
|
)
|
|
398
394
|
.command(
|
|
399
395
|
['license'],
|
|
400
|
-
'
|
|
396
|
+
'Display license information for mailauth and included modules',
|
|
401
397
|
() => false,
|
|
402
398
|
() => {
|
|
403
399
|
fs.readFile(pathlib.join(__dirname, '..', 'LICENSE.txt'), (err, license) => {
|
|
404
400
|
if (err) {
|
|
405
|
-
console.error('Failed to load license information');
|
|
401
|
+
console.error('Failed to load license information.');
|
|
406
402
|
console.error(err);
|
|
407
403
|
return process.exit(1);
|
|
408
404
|
}
|
|
409
405
|
|
|
410
|
-
console.error('
|
|
406
|
+
console.error('mailauth License');
|
|
411
407
|
console.error('================');
|
|
412
408
|
|
|
413
409
|
console.error(license.toString().trim());
|
|
@@ -416,7 +412,7 @@ const argv = yargs(hideBin(process.argv))
|
|
|
416
412
|
|
|
417
413
|
fs.readFile(pathlib.join(__dirname, '..', 'licenses.txt'), (err, data) => {
|
|
418
414
|
if (err) {
|
|
419
|
-
console.error('Failed to load license information');
|
|
415
|
+
console.error('Failed to load included modules license information.');
|
|
420
416
|
console.error(err);
|
|
421
417
|
return process.exit(1);
|
|
422
418
|
}
|
|
@@ -433,7 +429,7 @@ const argv = yargs(hideBin(process.argv))
|
|
|
433
429
|
.option('verbose', {
|
|
434
430
|
alias: 'v',
|
|
435
431
|
type: 'boolean',
|
|
436
|
-
description: '
|
|
432
|
+
description: 'Enable verbose logging for debugging purposes.'
|
|
437
433
|
}).argv;
|
|
438
434
|
|
|
439
435
|
assert.ok(argv);
|