mailauth 2.3.2 → 3.0.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/.gitattributes +1 -0
- package/README.md +1 -10
- package/bin/mailauth.js +32 -0
- package/cli.md +81 -0
- package/lib/bimi/index.js +174 -1
- package/lib/commands/vmc.js +20 -0
- package/lib/dkim/body/relaxed.js +219 -85
- package/lib/dkim/body/simple.js +6 -1
- package/lib/dkim/dkim-verifier.js +2 -2
- package/lib/dmarc/verify.js +9 -4
- package/lib/mailauth.js +3 -2
- package/lib/tools.js +3 -47
- package/licenses.txt +11 -0
- package/man/mailauth.1 +1 -1
- package/package.json +16 -15
package/.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.js text eol=lf
|
package/README.md
CHANGED
|
@@ -261,7 +261,7 @@ process.stdout.write(message);
|
|
|
261
261
|
If you want to modify the message before sealing, you have to authenticate the message first and then use authentication results as input for the sealing step.
|
|
262
262
|
|
|
263
263
|
```js
|
|
264
|
-
const { authenticate, sealMessage } = require('
|
|
264
|
+
const { authenticate, sealMessage } = require('mailauth');
|
|
265
265
|
|
|
266
266
|
// 1. authenticate the message
|
|
267
267
|
const { arc, headers } = await authenticate(
|
|
@@ -327,15 +327,6 @@ Some example authority evidence documents:
|
|
|
327
327
|
- [from default.\_bimi.cnn.com](https://amplify.valimail.com/bimi/time-warner/LysAFUdG-Hw-cnn_vmc.pem)
|
|
328
328
|
- [from default.\_bimi.entrustdatacard.com](https://www.entrustdatacard.com/-/media/certificate/Entrust%20VMC%20July%2014%202020.pem)
|
|
329
329
|
|
|
330
|
-
You can parse logos from these certificate files using the `parseLogoFromX509` function.
|
|
331
|
-
|
|
332
|
-
```js
|
|
333
|
-
const { parseLogoFromX509 } = require('mailauth/lib/tools');
|
|
334
|
-
let { altnNames, svg } = await parseLogoFromX509(fs.readFileSync('vmc.pem'));
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
> **NB!** `parseLogoFromX509` does not verify the validity of the VMC certificate. It could be self-signed or expired and still be processed.
|
|
338
|
-
|
|
339
330
|
## MTA-STS
|
|
340
331
|
|
|
341
332
|
`mailauth` allows you to fetch MTA-STS information for a domain name.
|
package/bin/mailauth.js
CHANGED
|
@@ -6,10 +6,13 @@ const yargs = require('yargs/yargs');
|
|
|
6
6
|
const { hideBin } = require('yargs/helpers');
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const assert = require('assert');
|
|
9
|
+
|
|
9
10
|
const commandReport = require('../lib/commands/report');
|
|
10
11
|
const commandSign = require('../lib/commands/sign');
|
|
11
12
|
const commandSeal = require('../lib/commands/seal');
|
|
12
13
|
const commandSpf = require('../lib/commands/spf');
|
|
14
|
+
const commandVmc = require('../lib/commands/vmc');
|
|
15
|
+
|
|
13
16
|
const fs = require('fs');
|
|
14
17
|
const pathlib = require('path');
|
|
15
18
|
|
|
@@ -287,6 +290,35 @@ const argv = yargs(hideBin(process.argv))
|
|
|
287
290
|
});
|
|
288
291
|
}
|
|
289
292
|
)
|
|
293
|
+
.command(
|
|
294
|
+
['vmc'],
|
|
295
|
+
'Validate VMC logo',
|
|
296
|
+
yargs => {
|
|
297
|
+
yargs.option('authorityFile', {
|
|
298
|
+
alias: 'f',
|
|
299
|
+
type: 'string',
|
|
300
|
+
description: 'Path to a VMC file',
|
|
301
|
+
demandOption: false
|
|
302
|
+
});
|
|
303
|
+
yargs.option('authority', {
|
|
304
|
+
alias: 'a',
|
|
305
|
+
type: 'string',
|
|
306
|
+
description: 'URL to a VMC file',
|
|
307
|
+
demandOption: false
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
argv => {
|
|
311
|
+
commandVmc(argv)
|
|
312
|
+
.then(() => {
|
|
313
|
+
process.exit();
|
|
314
|
+
})
|
|
315
|
+
.catch(err => {
|
|
316
|
+
console.error('Failed to verify VMC file');
|
|
317
|
+
console.error(err);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
)
|
|
290
322
|
.command(
|
|
291
323
|
['license'],
|
|
292
324
|
'Show license information',
|
package/cli.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- [sign](#sign) - to sign an email with DKIM
|
|
10
10
|
- [seal](#seal) - to seal an email with ARC
|
|
11
11
|
- [spf](#spf) - to validate SPF for an IP address and an email address
|
|
12
|
+
- [vmc](#vmc) - to validate BIMI VMC logo files
|
|
12
13
|
- [license](#license) - display licenses for `mailauth` and included modules
|
|
13
14
|
- [DNS cache file](#dns-cache-file)
|
|
14
15
|
|
|
@@ -208,6 +209,86 @@ DNS query for A mail.wildduck.email: ["217.146.76.20"]
|
|
|
208
209
|
...
|
|
209
210
|
```
|
|
210
211
|
|
|
212
|
+
### vmc
|
|
213
|
+
|
|
214
|
+
`vmc` command takes either the URL for a VMC file or a file path or both. It then verifies if the VMC resource is a valid file or not and exposes its contents.
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
$ mailauth vmc [options]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Where
|
|
221
|
+
|
|
222
|
+
- **options** are option flags and arguments
|
|
223
|
+
|
|
224
|
+
**Options**
|
|
225
|
+
|
|
226
|
+
- `--authority <url>` or `-a <url>` is the URL for the VMC resource
|
|
227
|
+
- `--authorityFile <path>` or `-f <path>` is the cached file for the authority URL to avoid network requests
|
|
228
|
+
|
|
229
|
+
**Example**
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
$ mailauth vmc -a https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.pem
|
|
233
|
+
{
|
|
234
|
+
"url": "https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.pem",
|
|
235
|
+
"success": true,
|
|
236
|
+
"vmc": {
|
|
237
|
+
"mediaType": "image/svg+xml",
|
|
238
|
+
"hashAlgo": "sha1",
|
|
239
|
+
"hashValue": "ea8c81da633c66a16262134a78576cdf067638e9",
|
|
240
|
+
"logoFile": "PD94bWwgdmVyc...",
|
|
241
|
+
"validHash": true,
|
|
242
|
+
"certificate": {
|
|
243
|
+
"subjectAltName": [
|
|
244
|
+
"cnn.com"
|
|
245
|
+
],
|
|
246
|
+
"subject": {
|
|
247
|
+
"businessCategory": "Private Organization",
|
|
248
|
+
"jurisdictionCountryName": "US",
|
|
249
|
+
"jurisdictionStateOrProvinceName": "Delaware",
|
|
250
|
+
"serialNumber": "2976730",
|
|
251
|
+
"countryName": "US",
|
|
252
|
+
"stateOrProvinceName": "Georgia",
|
|
253
|
+
"localityName": "Atlanta",
|
|
254
|
+
"street": "190 Marietta St NW",
|
|
255
|
+
"organizationName": "Cable News Network, Inc.",
|
|
256
|
+
"commonName": "Cable News Network, Inc.",
|
|
257
|
+
"trademarkCountryOrRegionName": "US",
|
|
258
|
+
"trademarkRegistration": "5817930"
|
|
259
|
+
},
|
|
260
|
+
"fingerprint": "17:B3:94:97:E6:6B:C8:6B:33:B8:0A:D2:F0:79:6B:08:A2:A6:84:BD",
|
|
261
|
+
"serialNumber": "0821B8FE0A9CBC3BAC10DA08C088EEF4",
|
|
262
|
+
"issuer": {
|
|
263
|
+
"countryName": "US",
|
|
264
|
+
"organizationName": "DigiCert, Inc.",
|
|
265
|
+
"commonName": "DigiCert Verified Mark RSA4096 SHA256 2021 CA1"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
If the certificate verification fails, then the contents are not returned.
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
$ mailauth vmc -f /path/to/random/cert-bundle.pem
|
|
276
|
+
{
|
|
277
|
+
"success": false,
|
|
278
|
+
"error": {
|
|
279
|
+
"message": "Self signed certificate in certificate chain",
|
|
280
|
+
"details": {
|
|
281
|
+
"subject": "CN=catchall.delivery",
|
|
282
|
+
"fingerprint": "35:EF:C9:9A:52:D5:A9:94:00:68:C6:D4:17:F1:26:61:01:0F:70:6D",
|
|
283
|
+
"fingerprint235": "09:AB:0F:6B:F5:4F:16:58:F8:94:80:DE:E2:1A:D1:47:CC:64:F2:BF:63:E7:73:E4:02:F9:D3:C3:F6:9E:CC:86",
|
|
284
|
+
"validFrom": "Jul 6 23:10:49 2022 GMT",
|
|
285
|
+
"validTo": "Oct 4 23:10:48 2022 GMT"
|
|
286
|
+
},
|
|
287
|
+
"code": "SELF_SIGNED_CERT_IN_CHAIN"
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
211
292
|
### license
|
|
212
293
|
|
|
213
294
|
Display licenses for `mailauth` and included modules.
|
package/lib/bimi/index.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const dns = require('dns');
|
|
4
5
|
const { formatAuthHeaderRow, parseDkimHeaders } = require('../tools');
|
|
5
6
|
const Joi = require('joi');
|
|
7
|
+
const packageData = require('../../package.json');
|
|
6
8
|
const httpsSchema = Joi.string().uri({
|
|
7
9
|
scheme: ['https']
|
|
8
10
|
});
|
|
9
11
|
|
|
12
|
+
const https = require('https');
|
|
13
|
+
const http = require('http');
|
|
14
|
+
const { vmc } = require('@postalsys/vmc');
|
|
15
|
+
|
|
10
16
|
const lookup = async data => {
|
|
11
17
|
let { dmarc, headers, resolver } = data;
|
|
12
18
|
let headerRows = (headers && headers.parsed) || [];
|
|
@@ -161,4 +167,171 @@ const lookup = async data => {
|
|
|
161
167
|
return response;
|
|
162
168
|
};
|
|
163
169
|
|
|
164
|
-
|
|
170
|
+
const downloadPromise = (url, cachedFile) => {
|
|
171
|
+
if (cachedFile) {
|
|
172
|
+
return cachedFile;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!url) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const parsedUrl = new URL(url);
|
|
180
|
+
|
|
181
|
+
const options = {
|
|
182
|
+
protocol: parsedUrl.protocol,
|
|
183
|
+
host: parsedUrl.host,
|
|
184
|
+
headers: {
|
|
185
|
+
host: parsedUrl.host,
|
|
186
|
+
'User-Agent': `mailauth/${packageData.version} (+${packageData.homepage}`
|
|
187
|
+
},
|
|
188
|
+
servername: parsedUrl.hostname,
|
|
189
|
+
port: 443,
|
|
190
|
+
path: parsedUrl.pathname,
|
|
191
|
+
method: 'GET',
|
|
192
|
+
rejectUnauthorized: true
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
let protoHandler;
|
|
197
|
+
switch (parsedUrl.protocol) {
|
|
198
|
+
case 'https:':
|
|
199
|
+
protoHandler = https;
|
|
200
|
+
break;
|
|
201
|
+
case 'http:':
|
|
202
|
+
protoHandler = http;
|
|
203
|
+
break;
|
|
204
|
+
default:
|
|
205
|
+
reject(new Error(`Unknown protocol ${parsedUrl.protocol}`));
|
|
206
|
+
}
|
|
207
|
+
const req = protoHandler.request(options, res => {
|
|
208
|
+
let chunks = [],
|
|
209
|
+
chunklen = 0;
|
|
210
|
+
res.on('readable', () => {
|
|
211
|
+
let chunk;
|
|
212
|
+
while ((chunk = res.read()) !== null) {
|
|
213
|
+
chunks.push(chunk);
|
|
214
|
+
chunklen += chunk.length;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
res.on('end', () => {
|
|
218
|
+
let data = Buffer.concat(chunks, chunklen);
|
|
219
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
220
|
+
let err = new Error(`Invalid response code ${res.statusCode || '-'}`);
|
|
221
|
+
err.code = 'http_status_' + (res.statusCode || 'na');
|
|
222
|
+
if (res.headers.location && res.statusCode >= 300 && res.statusCode < 400) {
|
|
223
|
+
err.redirect = {
|
|
224
|
+
code: res.statusCode,
|
|
225
|
+
location: res.headers.location
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return reject(err);
|
|
229
|
+
}
|
|
230
|
+
resolve(data);
|
|
231
|
+
});
|
|
232
|
+
res.on('error', err => reject(err));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
req.on('error', err => {
|
|
236
|
+
reject(err);
|
|
237
|
+
});
|
|
238
|
+
req.end();
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const validateVMC = async bimiData => {
|
|
243
|
+
if (!bimiData) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let promises = [];
|
|
248
|
+
|
|
249
|
+
promises.push(downloadPromise(bimiData.location, bimiData.locationFile));
|
|
250
|
+
promises.push(downloadPromise(bimiData.authority, bimiData.authorityFile));
|
|
251
|
+
|
|
252
|
+
if (!promises.length) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let results = await Promise.allSettled(promises);
|
|
257
|
+
|
|
258
|
+
let result = {};
|
|
259
|
+
if (results[0].value || results[0].reason) {
|
|
260
|
+
result.location = {
|
|
261
|
+
url: bimiData.location,
|
|
262
|
+
success: results[0].status === 'fulfilled'
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (results[0].reason) {
|
|
266
|
+
let err = results[0].reason;
|
|
267
|
+
result.location.error = { message: err.message };
|
|
268
|
+
if (err.redirect) {
|
|
269
|
+
result.location.error.redirect = err.redirect;
|
|
270
|
+
}
|
|
271
|
+
if (err.code) {
|
|
272
|
+
result.location.error.code = err.code;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (result.location.success) {
|
|
277
|
+
result.location.logoFile = results[0].value.toString('base64');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (results[1].value || results[1].reason) {
|
|
282
|
+
result.authority = {
|
|
283
|
+
url: bimiData.authority,
|
|
284
|
+
success: results[1].status === 'fulfilled'
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
if (results[1].reason) {
|
|
288
|
+
let err = results[1].reason;
|
|
289
|
+
result.authority.error = { message: err.message };
|
|
290
|
+
if (err.redirect) {
|
|
291
|
+
result.authority.error.redirect = err.redirect;
|
|
292
|
+
}
|
|
293
|
+
if (err.code) {
|
|
294
|
+
result.authority.error.code = err.code;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (results[1].value) {
|
|
299
|
+
try {
|
|
300
|
+
result.authority.vmc = await vmc(results[1].value);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
result.authority.success = false;
|
|
303
|
+
result.authority.error = { message: err.message };
|
|
304
|
+
if (err.details) {
|
|
305
|
+
result.authority.error.details = err.details;
|
|
306
|
+
}
|
|
307
|
+
if (err.code) {
|
|
308
|
+
result.authority.error.code = err.code;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (result.location && result.location.success && result.authority.success) {
|
|
314
|
+
try {
|
|
315
|
+
if (result.location.success && result.authority.vmc.hashAlgo && result.authority.vmc.validHash) {
|
|
316
|
+
let hash = crypto.createHash(result.authority.vmc.hashAlgo).update(results[0].value).digest('hex');
|
|
317
|
+
result.location.hashAlgo = result.authority.vmc.hashAlgo;
|
|
318
|
+
result.location.hashValue = hash;
|
|
319
|
+
result.authority.hashMatch = hash === result.authority.vmc.hashValue;
|
|
320
|
+
}
|
|
321
|
+
} catch (err) {
|
|
322
|
+
result.authority.success = false;
|
|
323
|
+
result.authority.error = { message: err.message };
|
|
324
|
+
if (err.details) {
|
|
325
|
+
result.authority.error.details = err.details;
|
|
326
|
+
}
|
|
327
|
+
if (err.code) {
|
|
328
|
+
result.authority.error.code = err.code;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return result;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
module.exports = { bimi: lookup, validateVMC };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { validateVMC } = require('../bimi');
|
|
4
|
+
|
|
5
|
+
const fs = require('fs').promises;
|
|
6
|
+
|
|
7
|
+
const cmd = async argv => {
|
|
8
|
+
let bimiData = {};
|
|
9
|
+
if (argv.authorityFile) {
|
|
10
|
+
bimiData.authorityFile = await fs.readFile(argv.authorityFile);
|
|
11
|
+
}
|
|
12
|
+
if (argv.authority) {
|
|
13
|
+
bimiData.authority = argv.authority;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result = await validateVMC(bimiData);
|
|
17
|
+
process.stdout.write(JSON.stringify(result.authority, false, 2) + '\n');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
module.exports = cmd;
|
package/lib/dkim/body/relaxed.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint no-control-regex: 0 */
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
'use strict';
|
|
4
4
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
|
|
7
|
+
const CHAR_CR = 0x0d;
|
|
8
|
+
const CHAR_LF = 0x0a;
|
|
9
|
+
const CHAR_SPACE = 0x20;
|
|
10
|
+
const CHAR_TAB = 0x09;
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* Class for calculating body hash of an email message body stream
|
|
9
14
|
* using the "relaxed" canonicalization
|
|
@@ -16,139 +21,244 @@ class RelaxedHash {
|
|
|
16
21
|
* @param {Number} [maxBodyLength] Allowed body length count, the value from the l= parameter
|
|
17
22
|
*/
|
|
18
23
|
constructor(algorithm, maxBodyLength) {
|
|
19
|
-
algorithm = (algorithm || 'sha256').split('-').pop();
|
|
24
|
+
algorithm = (algorithm || 'sha256').split('-').pop().toLowerCase();
|
|
25
|
+
|
|
20
26
|
this.bodyHash = crypto.createHash(algorithm);
|
|
21
27
|
|
|
22
|
-
this.remainder =
|
|
28
|
+
this.remainder = false;
|
|
23
29
|
this.byteLength = 0;
|
|
24
30
|
|
|
25
31
|
this.bodyHashedBytes = 0;
|
|
26
32
|
this.maxBodyLength = maxBodyLength;
|
|
33
|
+
|
|
34
|
+
this.maxSizeReached = false;
|
|
35
|
+
|
|
36
|
+
this.emptyLinesQueue = [];
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
_updateBodyHash(chunk) {
|
|
30
|
-
|
|
40
|
+
if (this.maxSizeReached) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// the following is needed for the l= option
|
|
31
45
|
if (
|
|
32
46
|
typeof this.maxBodyLength === 'number' &&
|
|
33
47
|
!isNaN(this.maxBodyLength) &&
|
|
34
48
|
this.maxBodyLength >= 0 &&
|
|
35
49
|
this.bodyHashedBytes + chunk.length > this.maxBodyLength
|
|
36
50
|
) {
|
|
51
|
+
this.maxSizeReached = true;
|
|
37
52
|
if (this.bodyHashedBytes >= this.maxBodyLength) {
|
|
38
53
|
// nothing to do here, skip entire chunk
|
|
39
54
|
return;
|
|
40
55
|
}
|
|
56
|
+
|
|
41
57
|
// only use allowed size of bytes
|
|
42
|
-
chunk = chunk.
|
|
58
|
+
chunk = chunk.subarray(0, this.maxBodyLength - this.bodyHashedBytes);
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
this.bodyHashedBytes += chunk.length;
|
|
46
62
|
this.bodyHash.update(chunk);
|
|
63
|
+
|
|
64
|
+
//process.stdout.write(chunk);
|
|
47
65
|
}
|
|
48
66
|
|
|
49
|
-
|
|
50
|
-
this.
|
|
67
|
+
_drainPendingEmptyLines() {
|
|
68
|
+
if (this.emptyLinesQueue.length) {
|
|
69
|
+
for (let emptyLine of this.emptyLinesQueue) {
|
|
70
|
+
this._updateBodyHash(emptyLine);
|
|
71
|
+
}
|
|
72
|
+
this.emptyLinesQueue = [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
51
75
|
|
|
52
|
-
|
|
76
|
+
_pushBodyHash(chunk) {
|
|
77
|
+
if (!chunk || !chunk.length) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
53
80
|
|
|
54
|
-
//
|
|
55
|
-
let
|
|
81
|
+
// remove line endings
|
|
82
|
+
let foundNonLn = false;
|
|
56
83
|
|
|
57
|
-
//
|
|
58
|
-
// If we get another chunk that does not match this description then we can restore the previously processed data
|
|
59
|
-
let state = 'file';
|
|
84
|
+
// buffer line endings and empty lines
|
|
60
85
|
for (let i = chunk.length - 1; i >= 0; i--) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// switch to line ending mode, this is the last non-empty line
|
|
67
|
-
state = 'line';
|
|
68
|
-
} else if (state === 'line' && (c === 0x09 || c === 0x20)) {
|
|
69
|
-
// do nothing, found ' ' or \t at the end of line, keep processing the last non-empty line
|
|
70
|
-
} else if (state === 'file' || state === 'line') {
|
|
71
|
-
// non line/file ending character found, switch to body mode
|
|
72
|
-
state = 'body';
|
|
73
|
-
if (i === chunk.length - 1) {
|
|
74
|
-
// final char is not part of line end or file end, so do nothing
|
|
75
|
-
break;
|
|
86
|
+
if (chunk[i] !== CHAR_LF && chunk[i] !== CHAR_CR) {
|
|
87
|
+
this._drainPendingEmptyLines();
|
|
88
|
+
if (i < chunk.length - 1) {
|
|
89
|
+
this.emptyLinesQueue.push(chunk.subarray(i + 1));
|
|
90
|
+
chunk = chunk.subarray(0, i + 1);
|
|
76
91
|
}
|
|
92
|
+
foundNonLn = true;
|
|
93
|
+
break;
|
|
77
94
|
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!foundNonLn) {
|
|
98
|
+
this.emptyLinesQueue.push(chunk);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
this._updateBodyHash(chunk);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fixLineBuffer(line) {
|
|
106
|
+
let resultLine = [];
|
|
107
|
+
|
|
108
|
+
let nonWspFound = false;
|
|
109
|
+
let prevWsp = false;
|
|
110
|
+
|
|
111
|
+
for (let i = line.length - 1; i >= 0; i--) {
|
|
112
|
+
if (line[i] === CHAR_LF) {
|
|
113
|
+
resultLine.unshift(line[i]);
|
|
114
|
+
if (i === 0 || line[i - 1] !== CHAR_CR) {
|
|
115
|
+
// add missing carriage return
|
|
116
|
+
resultLine.unshift(CHAR_CR);
|
|
94
117
|
}
|
|
118
|
+
continue;
|
|
95
119
|
}
|
|
96
120
|
|
|
97
|
-
if (
|
|
121
|
+
if (line[i] === CHAR_CR) {
|
|
122
|
+
resultLine.unshift(line[i]);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (line[i] === CHAR_SPACE || line[i] === CHAR_TAB) {
|
|
127
|
+
if (nonWspFound) {
|
|
128
|
+
prevWsp = true;
|
|
129
|
+
}
|
|
98
130
|
continue;
|
|
99
131
|
}
|
|
100
132
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
133
|
+
if (prevWsp) {
|
|
134
|
+
resultLine.unshift(CHAR_SPACE);
|
|
135
|
+
prevWsp = false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
nonWspFound = true;
|
|
139
|
+
resultLine.unshift(line[i]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (prevWsp && nonWspFound) {
|
|
143
|
+
resultLine.unshift(CHAR_SPACE);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return Buffer.from(resultLine);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
update(chunk, final) {
|
|
150
|
+
this.byteLength += (chunk && chunk.length) || 0;
|
|
151
|
+
if (this.maxSizeReached) {
|
|
152
|
+
return;
|
|
105
153
|
}
|
|
106
154
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
155
|
+
// Canonicalize content by applying a and b in order:
|
|
156
|
+
// a.1. Ignore all whitespace at the end of lines.
|
|
157
|
+
// a.2. Reduce all sequences of WSP within a line to a single SP character.
|
|
158
|
+
|
|
159
|
+
// b.1. Ignore all empty lines at the end of the message body.
|
|
160
|
+
// b.2. If the body is non-empty but does not end with a CRLF, a CRLF is added.
|
|
161
|
+
|
|
162
|
+
let lineEndPos = -1;
|
|
163
|
+
let lineNeedsFixing = false;
|
|
164
|
+
let cursorPos = 0;
|
|
165
|
+
|
|
166
|
+
if (this.remainder && this.remainder.length) {
|
|
167
|
+
if (chunk) {
|
|
168
|
+
// concatting chunks might be bad for performance :S
|
|
169
|
+
chunk = Buffer.concat([this.remainder, chunk]);
|
|
170
|
+
} else {
|
|
171
|
+
chunk = this.remainder;
|
|
172
|
+
}
|
|
173
|
+
this.remainder = false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (chunk && chunk.length) {
|
|
177
|
+
for (let pos = 0; pos < chunk.length; pos++) {
|
|
178
|
+
switch (chunk[pos]) {
|
|
179
|
+
case CHAR_LF:
|
|
180
|
+
if (
|
|
181
|
+
!lineNeedsFixing &&
|
|
182
|
+
// previous character is not <CR>
|
|
183
|
+
((pos >= 1 && chunk[pos - 1] !== CHAR_CR) ||
|
|
184
|
+
// LF is the first byte on the line
|
|
185
|
+
pos === 0 ||
|
|
186
|
+
// there's a space before line break
|
|
187
|
+
(pos >= 2 && chunk[pos - 1] === CHAR_CR && chunk[pos - 2] === CHAR_SPACE))
|
|
188
|
+
) {
|
|
189
|
+
lineNeedsFixing = true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// line break
|
|
193
|
+
if (lineNeedsFixing) {
|
|
194
|
+
// emit pending bytes up to the last line break before current line
|
|
195
|
+
if (lineEndPos >= 0 && lineEndPos >= cursorPos) {
|
|
196
|
+
let chunkPart = chunk.subarray(cursorPos, lineEndPos + 1);
|
|
197
|
+
this._pushBodyHash(chunkPart);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let line = chunk.subarray(lineEndPos + 1, pos + 1);
|
|
201
|
+
this._pushBodyHash(this.fixLineBuffer(line));
|
|
202
|
+
|
|
203
|
+
lineNeedsFixing = false;
|
|
204
|
+
|
|
205
|
+
// move cursor to the start of next line
|
|
206
|
+
cursorPos = pos + 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
lineEndPos = pos;
|
|
210
|
+
|
|
211
|
+
break;
|
|
212
|
+
|
|
213
|
+
case CHAR_SPACE:
|
|
214
|
+
if (!lineNeedsFixing && pos && chunk[pos - 1] === CHAR_SPACE) {
|
|
215
|
+
lineNeedsFixing = true;
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
|
|
219
|
+
case CHAR_TAB:
|
|
220
|
+
// non-space WSP always needs replacing
|
|
221
|
+
lineNeedsFixing = true;
|
|
222
|
+
break;
|
|
223
|
+
|
|
224
|
+
default:
|
|
127
225
|
}
|
|
128
226
|
}
|
|
129
227
|
}
|
|
130
228
|
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
229
|
+
if (chunk && cursorPos < chunk.length && cursorPos !== lineEndPos) {
|
|
230
|
+
// emit data from chunk
|
|
231
|
+
|
|
232
|
+
let chunkPart = chunk.subarray(cursorPos, lineEndPos + 1);
|
|
233
|
+
|
|
234
|
+
if (chunkPart.length) {
|
|
235
|
+
this._pushBodyHash(lineNeedsFixing ? this.fixLineBuffer(chunkPart) : chunkPart);
|
|
236
|
+
lineNeedsFixing = false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
cursorPos = lineEndPos + 1;
|
|
142
240
|
}
|
|
143
241
|
|
|
144
|
-
|
|
242
|
+
if (chunk && !final && cursorPos < chunk.length) {
|
|
243
|
+
this.remainder = chunk.subarray(cursorPos);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (final) {
|
|
247
|
+
let chunkPart = (cursorPos && chunk && chunk.subarray(cursorPos)) || chunk;
|
|
248
|
+
if (chunkPart && chunkPart.length) {
|
|
249
|
+
this._pushBodyHash(lineNeedsFixing ? this.fixLineBuffer(chunkPart) : chunkPart);
|
|
250
|
+
lineNeedsFixing = false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (this.bodyHashedBytes) {
|
|
254
|
+
// terminating line break for non-empty messages
|
|
255
|
+
this._updateBodyHash(Buffer.from([CHAR_CR, CHAR_LF]));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
145
258
|
}
|
|
146
259
|
|
|
147
260
|
digest(encoding) {
|
|
148
|
-
|
|
149
|
-
// add terminating line end
|
|
150
|
-
this._updateBodyHash(Buffer.from('\r\n'));
|
|
151
|
-
}
|
|
261
|
+
this.update(null, true);
|
|
152
262
|
|
|
153
263
|
// finalize
|
|
154
264
|
return this.bodyHash.digest(encoding);
|
|
@@ -156,3 +266,27 @@ class RelaxedHash {
|
|
|
156
266
|
}
|
|
157
267
|
|
|
158
268
|
module.exports = { RelaxedHash };
|
|
269
|
+
|
|
270
|
+
/*
|
|
271
|
+
let fs = require('fs');
|
|
272
|
+
|
|
273
|
+
const getBody = message => {
|
|
274
|
+
message = message.toString('binary');
|
|
275
|
+
let match = message.match(/\r?\n\r?\n/);
|
|
276
|
+
if (match) {
|
|
277
|
+
message = message.substr(match.index + match[0].length);
|
|
278
|
+
}
|
|
279
|
+
return Buffer.from(message, 'binary');
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
let s = fs.readFileSync(process.argv[2]);
|
|
283
|
+
|
|
284
|
+
let k = new RelaxedHash('rsa-sha256', -1);
|
|
285
|
+
|
|
286
|
+
for (let byte of getBody(s)) {
|
|
287
|
+
k.update(Buffer.from([byte]));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.error(k.digest('base64'));
|
|
291
|
+
console.error(k.byteLength, k.bodyHashedBytes);
|
|
292
|
+
*/
|
package/lib/dkim/body/simple.js
CHANGED
|
@@ -22,6 +22,8 @@ class SimpleHash {
|
|
|
22
22
|
|
|
23
23
|
this.bodyHashedBytes = 0;
|
|
24
24
|
this.maxBodyLength = maxBodyLength;
|
|
25
|
+
|
|
26
|
+
this.lastNewline = false;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
_updateBodyHash(chunk) {
|
|
@@ -42,6 +44,8 @@ class SimpleHash {
|
|
|
42
44
|
|
|
43
45
|
this.bodyHashedBytes += chunk.length;
|
|
44
46
|
this.bodyHash.update(chunk);
|
|
47
|
+
|
|
48
|
+
//process.stdout.write(chunk);
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
update(chunk) {
|
|
@@ -81,10 +85,11 @@ class SimpleHash {
|
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
this._updateBodyHash(chunk);
|
|
88
|
+
this.lastNewline = chunk[chunk.length - 1] === 0x0a;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
digest(encoding) {
|
|
87
|
-
if (this.
|
|
92
|
+
if (!this.lastNewline || !this.bodyHashedBytes) {
|
|
88
93
|
// emit empty line buffer to keep the stream flowing
|
|
89
94
|
this._updateBodyHash(Buffer.from('\r\n'));
|
|
90
95
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { getSigningHeaderLines, getPublicKey, parseDkimHeaders, formatAuthHeaderRow,
|
|
3
|
+
const { getSigningHeaderLines, getPublicKey, parseDkimHeaders, formatAuthHeaderRow, getAlignment } = require('../../lib/tools');
|
|
4
4
|
const { MessageParser } = require('./message-parser');
|
|
5
5
|
const { dkimBody } = require('./body');
|
|
6
6
|
const { generateCanonicalizedHeader } = require('./header');
|
|
@@ -195,7 +195,7 @@ class DkimVerifier extends MessageParser {
|
|
|
195
195
|
};
|
|
196
196
|
|
|
197
197
|
if (signatureHeader.type === 'DKIM' && this.headerFrom?.length) {
|
|
198
|
-
status.aligned = this.headerFrom?.length ?
|
|
198
|
+
status.aligned = this.headerFrom?.length ? getAlignment(this.headerFrom[0].split('@').pop(), [signatureHeader.signingDomain]) : false;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
let bodyHash = this.bodyHashes.get(signatureHeader.bodyHashKey)?.hash;
|
package/lib/dmarc/verify.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const dns = require('dns').promises;
|
|
4
4
|
const punycode = require('punycode/');
|
|
5
5
|
const psl = require('psl');
|
|
6
|
-
const { formatAuthHeaderRow,
|
|
6
|
+
const { formatAuthHeaderRow, getAlignment } = require('../tools');
|
|
7
7
|
|
|
8
8
|
const resolveTxt = async (domain, resolver) => {
|
|
9
9
|
try {
|
|
@@ -146,8 +146,8 @@ const verifyDmarc = async opts => {
|
|
|
146
146
|
// use "sp" if this is a subdomain of an org domain and "sp" is set, otherwise use "p"
|
|
147
147
|
const policy = dmarcRecord.isOrgRecord && dmarcRecord.sp ? dmarcRecord.sp : dmarcRecord.p;
|
|
148
148
|
|
|
149
|
-
const dkimAlignment =
|
|
150
|
-
const spfAlignment =
|
|
149
|
+
const dkimAlignment = getAlignment(domain, dkimDomains, { strict: dmarcRecord.adkim === 's' });
|
|
150
|
+
const spfAlignment = getAlignment(domain, spfDomains, { strict: dmarcRecord.aspf === 's' });
|
|
151
151
|
|
|
152
152
|
if (dkimAlignment || spfAlignment) {
|
|
153
153
|
// pass
|
|
@@ -164,7 +164,12 @@ const verifyDmarc = async opts => {
|
|
|
164
164
|
p: dmarcRecord.p,
|
|
165
165
|
sp: dmarcRecord.sp || dmarcRecord.p,
|
|
166
166
|
pct: dmarcRecord.pct,
|
|
167
|
-
rr: dmarcRecord.rr
|
|
167
|
+
rr: dmarcRecord.rr,
|
|
168
|
+
|
|
169
|
+
alignment: {
|
|
170
|
+
spf: { result: spfAlignment, strict: dmarcRecord.aspf === 's' },
|
|
171
|
+
dkim: { result: dkimAlignment, strict: dmarcRecord.adkim === 's' }
|
|
172
|
+
}
|
|
168
173
|
});
|
|
169
174
|
};
|
|
170
175
|
|
package/lib/mailauth.js
CHANGED
|
@@ -4,8 +4,9 @@ const { dkimVerify } = require('./dkim/verify');
|
|
|
4
4
|
const { spf } = require('./spf');
|
|
5
5
|
const { dmarc } = require('./dmarc');
|
|
6
6
|
const { arc, createSeal } = require('./arc');
|
|
7
|
-
const { bimi } = require('./bimi');
|
|
7
|
+
const { bimi, validateVMC: validateBimiVmc } = require('./bimi');
|
|
8
8
|
const { parseReceived } = require('./parse-received');
|
|
9
|
+
const { sealMessage } = require('./arc');
|
|
9
10
|
const libmime = require('libmime');
|
|
10
11
|
const os = require('os');
|
|
11
12
|
const { isIP } = require('net');
|
|
@@ -179,4 +180,4 @@ const authenticate = async (input, opts) => {
|
|
|
179
180
|
};
|
|
180
181
|
};
|
|
181
182
|
|
|
182
|
-
module.exports = { authenticate };
|
|
183
|
+
module.exports = { authenticate, sealMessage, validateBimiVmc };
|
package/lib/tools.js
CHANGED
|
@@ -10,10 +10,6 @@ const https = require('https');
|
|
|
10
10
|
const packageData = require('../package');
|
|
11
11
|
const parseDkimHeaders = require('./parse-dkim-headers');
|
|
12
12
|
const psl = require('psl');
|
|
13
|
-
const { Certificate } = require('@fidm/x509');
|
|
14
|
-
const zlib = require('zlib');
|
|
15
|
-
const util = require('util');
|
|
16
|
-
const gunzip = util.promisify(zlib.gunzip);
|
|
17
13
|
const pki = require('node-forge').pki;
|
|
18
14
|
const Joi = require('joi');
|
|
19
15
|
const base64Schema = Joi.string().base64({ paddingRequired: false });
|
|
@@ -429,7 +425,7 @@ const formatDomain = domain => {
|
|
|
429
425
|
return domain;
|
|
430
426
|
};
|
|
431
427
|
|
|
432
|
-
const
|
|
428
|
+
const getAlignment = (fromDomain, domainList, strict) => {
|
|
433
429
|
domainList = [].concat(domainList || []);
|
|
434
430
|
if (strict) {
|
|
435
431
|
fromDomain = formatDomain(fromDomain);
|
|
@@ -474,44 +470,6 @@ const validateAlgorithm = (algorithm, strict) => {
|
|
|
474
470
|
}
|
|
475
471
|
};
|
|
476
472
|
|
|
477
|
-
/**
|
|
478
|
-
* Function takes Verified Mark Certificate file and parses domain names and SVG file
|
|
479
|
-
* NB! Certificate is not verified in any way. If there are altNames and SVG content
|
|
480
|
-
* available then these are returned even if the certificate is self signed or expired.
|
|
481
|
-
* @param {Buffer} pem VMC file
|
|
482
|
-
* @returns {Object|Boolean} Either an object with {altNames[], svg} or false if required data was missing from the certificate
|
|
483
|
-
*/
|
|
484
|
-
const parseLogoFromX509 = async pem => {
|
|
485
|
-
const cert = Certificate.fromPEM(pem);
|
|
486
|
-
|
|
487
|
-
const altNames = cert.extensions
|
|
488
|
-
.filter(e => e.oid === '2.5.29.17')
|
|
489
|
-
.flatMap(d => d?.altNames?.map(an => an?.dnsName?.trim()))
|
|
490
|
-
.filter(an => an);
|
|
491
|
-
if (!altNames.length) {
|
|
492
|
-
return false;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
let logo = cert.extensions.find(e => e.oid === '1.3.6.1.5.5.7.1.12');
|
|
496
|
-
if (!logo?.value?.length) {
|
|
497
|
-
return false;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
let str = logo.value.toString();
|
|
501
|
-
// No idea what is that binary stuff before the data uri block
|
|
502
|
-
let dataMatch = /\bdata:/.test(str) && str.match(/\bbase64,/);
|
|
503
|
-
if (dataMatch) {
|
|
504
|
-
let b64 = str.substr(dataMatch.index + dataMatch[0].length);
|
|
505
|
-
let svg = await gunzip(Buffer.from(b64, 'base64'));
|
|
506
|
-
return {
|
|
507
|
-
pem,
|
|
508
|
-
altNames,
|
|
509
|
-
svg: svg.toString()
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
return false;
|
|
513
|
-
};
|
|
514
|
-
|
|
515
473
|
module.exports = {
|
|
516
474
|
writeToStream,
|
|
517
475
|
parseHeaders,
|
|
@@ -530,9 +488,7 @@ module.exports = {
|
|
|
530
488
|
|
|
531
489
|
validateAlgorithm,
|
|
532
490
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
formatRelaxedLine,
|
|
491
|
+
getAlignment,
|
|
536
492
|
|
|
537
|
-
|
|
493
|
+
formatRelaxedLine
|
|
538
494
|
};
|
package/licenses.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
name license type link installed version author
|
|
2
|
+
---- ------------ ---- ----------------- ------
|
|
3
|
+
@fidm/x509 MIT git+ssh://git@github.com/fidm/x509.git 1.2.1
|
|
4
|
+
ipaddr.js MIT git://github.com/whitequark/ipaddr.js.git 2.0.1 whitequark whitequark@whitequark.org
|
|
5
|
+
joi BSD-3-Clause git://github.com/sideway/joi.git 17.6.0
|
|
6
|
+
libmime MIT git://github.com/andris9/libmime.git 5.1.0 Andris Reinman andris@kreata.ee
|
|
7
|
+
node-forge (BSD-3-Clause OR GPL-2.0) git+https://github.com/digitalbazaar/forge.git 1.3.1 Digital Bazaar, Inc. support@digitalbazaar.com http://digitalbazaar.com/
|
|
8
|
+
nodemailer MIT git+https://github.com/nodemailer/nodemailer.git 6.7.5 Andris Reinman
|
|
9
|
+
psl MIT git+ssh://git@github.com/lupomontero/psl.git 1.8.0 Lupo Montero lupomontero@gmail.com https://lupomontero.com/
|
|
10
|
+
punycode MIT git+https://github.com/bestiejs/punycode.js.git 2.1.1 Mathias Bynens https://mathiasbynens.be/
|
|
11
|
+
yargs MIT git+https://github.com/yargs/yargs.git 17.5.1
|
package/man/mailauth.1
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailauth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Email authentication library for Node.js",
|
|
5
5
|
"main": "lib/mailauth.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "eslint \"lib/**/*.js\" \"test/**/*.js\" && mocha --recursive \"./test/**/*.js\" --reporter spec",
|
|
8
8
|
"prepublish": "npm run man || true",
|
|
9
9
|
"man": "cd man && marked-man --version `node -e \"console.log('v'+require('../package.json').version)\"` --manual 'Mailauth Help' --section 1 man.md > mailauth.1",
|
|
10
|
-
"build-
|
|
10
|
+
"build-source": "rm -rf node_modules package-lock.json && npm install && npm run man && npm run licenses && rm -rf node_modules package-lock.json && npm install --production && rm -rf package-lock.json",
|
|
11
|
+
"build-dist": "npx pkg --compress Brotli package.json && rm -rf package-lock.json && npm install",
|
|
11
12
|
"licenses": "license-report --only=prod --output=table --config license-report-config.json > licenses.txt"
|
|
12
13
|
},
|
|
13
14
|
"repository": {
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"homepage": "https://github.com/postalsys/mailauth",
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"chai": "4.3.6",
|
|
35
|
-
"eslint": "8.
|
|
36
|
+
"eslint": "8.19.0",
|
|
36
37
|
"eslint-config-nodemailer": "1.2.0",
|
|
37
38
|
"eslint-config-prettier": "8.5.0",
|
|
38
39
|
"js-yaml": "4.1.0",
|
|
@@ -40,22 +41,23 @@
|
|
|
40
41
|
"marked": "0.7.0",
|
|
41
42
|
"marked-man": "0.7.0",
|
|
42
43
|
"mbox-reader": "1.1.5",
|
|
43
|
-
"mocha": "
|
|
44
|
-
"pkg": "5.
|
|
44
|
+
"mocha": "10.0.0",
|
|
45
|
+
"pkg": "5.7.0"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@fidm/x509": "1.2.1",
|
|
49
|
+
"@postalsys/vmc": "1.0.1",
|
|
48
50
|
"ipaddr.js": "2.0.1",
|
|
49
51
|
"joi": "17.6.0",
|
|
50
|
-
"libmime": "5.
|
|
52
|
+
"libmime": "5.1.0",
|
|
51
53
|
"node-forge": "1.3.1",
|
|
52
|
-
"nodemailer": "6.7.
|
|
53
|
-
"psl": "1.
|
|
54
|
+
"nodemailer": "6.7.7",
|
|
55
|
+
"psl": "1.9.0",
|
|
54
56
|
"punycode": "2.1.1",
|
|
55
|
-
"yargs": "17.
|
|
57
|
+
"yargs": "17.5.1"
|
|
56
58
|
},
|
|
57
59
|
"engines": {
|
|
58
|
-
"node": ">=
|
|
60
|
+
"node": ">=16.0.0"
|
|
59
61
|
},
|
|
60
62
|
"bin": {
|
|
61
63
|
"mailauth": "bin/mailauth.js"
|
|
@@ -64,16 +66,15 @@
|
|
|
64
66
|
"man/mailauth.1"
|
|
65
67
|
],
|
|
66
68
|
"pkg": {
|
|
67
|
-
"scripts": [
|
|
68
|
-
"workers/**/*.js"
|
|
69
|
-
],
|
|
70
69
|
"assets": [
|
|
71
70
|
"man/**/*",
|
|
72
71
|
"licenses.txt",
|
|
73
72
|
"LICENSE.txt"
|
|
74
73
|
],
|
|
75
|
-
"
|
|
76
|
-
"node16-
|
|
74
|
+
"targets": [
|
|
75
|
+
"node16-linux-x64",
|
|
76
|
+
"node16-macos-x64",
|
|
77
|
+
"node16-win-x64"
|
|
77
78
|
],
|
|
78
79
|
"outputPath": "ee-dist"
|
|
79
80
|
}
|