mailauth 7.2.0 → 7.3.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/.prettierignore +3 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +99 -0
- package/bin/mailauth.js +62 -0
- package/cli.md +149 -7
- package/docs/dkim2.md +154 -0
- package/index.d.ts +359 -0
- package/lib/commands/dkim2-hash.js +25 -0
- package/lib/commands/dkim2-sign.js +77 -0
- package/lib/commands/dkim2-verify.js +59 -0
- package/lib/commands/report.js +8 -33
- package/lib/commands/resolver.js +51 -0
- package/lib/commands/seal.js +2 -32
- package/lib/commands/spf.js +2 -33
- package/lib/dkim/sign.js +7 -2
- package/lib/dkim2/dkim2-parser.js +52 -0
- package/lib/dkim2/dkim2-signer.js +334 -0
- package/lib/dkim2/dkim2-verifier.js +572 -0
- package/lib/dkim2/fields.js +570 -0
- package/lib/dkim2/hash.d.ts +3 -0
- package/lib/dkim2/hash.js +51 -0
- package/lib/dkim2/key.js +203 -0
- package/lib/dkim2/message-state.js +181 -0
- package/lib/dkim2/recipe.js +134 -0
- package/lib/dkim2/sign.d.ts +3 -0
- package/lib/dkim2/sign.js +38 -0
- package/lib/dkim2/verify.d.ts +3 -0
- package/lib/dkim2/verify.js +19 -0
- package/lib/mailauth.js +40 -0
- package/lib/tools.js +1 -0
- package/package.json +3 -3
- package/stryker.config.json +2 -2
package/index.d.ts
CHANGED
|
@@ -109,6 +109,24 @@ export interface AuthenticateOptions {
|
|
|
109
109
|
* `warnings` array of the affected result. It is never written into the headers
|
|
110
110
|
*/
|
|
111
111
|
strict?: boolean;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Verify DKIM2 header fields as well (default: false). Experimental, built against
|
|
115
|
+
* draft-ietf-dkim-dkim2-spec-06. The result is added to Authentication-Results with the
|
|
116
|
+
* `dkim2` method of draft-gondwana-dkim2-authres-00 and does not take part in DMARC
|
|
117
|
+
*/
|
|
118
|
+
dkim2?: boolean;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* RCPT TO addresses of the delivery, checked against the rt= tag of the highest
|
|
122
|
+
* DKIM2-Signature. `sender` is checked against its mf= tag
|
|
123
|
+
*/
|
|
124
|
+
rcptTo?: string | string[];
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Extra DKIM2 verifier options
|
|
128
|
+
*/
|
|
129
|
+
dkim2Options?: Pick<DKIM2VerifyOptions, 'curTime' | 'maxSignatureAge' | 'maxFutureTime' | 'maxInstances' | 'checkReplay'>;
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
/**
|
|
@@ -852,6 +870,11 @@ export interface AuthenticateResult {
|
|
|
852
870
|
*/
|
|
853
871
|
dkim: DKIMVerifyResult;
|
|
854
872
|
|
|
873
|
+
/**
|
|
874
|
+
* DKIM2 verification result, false unless the `dkim2` option is set
|
|
875
|
+
*/
|
|
876
|
+
dkim2: DKIM2VerifyResult | false;
|
|
877
|
+
|
|
855
878
|
/**
|
|
856
879
|
* SPF verification result
|
|
857
880
|
*/
|
|
@@ -1318,6 +1341,342 @@ export interface DKIMVerifyOptions {
|
|
|
1318
1341
|
*/
|
|
1319
1342
|
export function dkimVerify(input: MessageInput, options?: DKIMVerifyOptions): Promise<DKIMVerifyResult>;
|
|
1320
1343
|
|
|
1344
|
+
// ============================================================================
|
|
1345
|
+
// DKIM2 (experimental, draft-ietf-dkim-dkim2-spec-06, draft-ietf-dkim-dkim2-dns-00)
|
|
1346
|
+
// ============================================================================
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* A DKIM2 Recipe (draft-ietf-dkim-dkim2-spec-06 section 5). Each step either copies a range of
|
|
1350
|
+
* header fields (numbered from the bottom) or body lines (numbered from the top), or emits data
|
|
1351
|
+
*/
|
|
1352
|
+
export type DKIM2RecipeStep = { c: [number, number] } | { d: string[] };
|
|
1353
|
+
|
|
1354
|
+
export interface DKIM2Recipe {
|
|
1355
|
+
/**
|
|
1356
|
+
* Steps per lower case header field name. An empty array removes every field of that name
|
|
1357
|
+
*/
|
|
1358
|
+
h?: Record<string, DKIM2RecipeStep[]>;
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Steps for the body, or null when the previous body can not be recreated
|
|
1362
|
+
*/
|
|
1363
|
+
b?: DKIM2RecipeStep[] | null;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
export interface DKIM2SignatureKey {
|
|
1367
|
+
/**
|
|
1368
|
+
* Key selector
|
|
1369
|
+
*/
|
|
1370
|
+
selector: string;
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Private key, PEM or a raw 32 byte Ed25519 key
|
|
1374
|
+
*/
|
|
1375
|
+
privateKey: string | Buffer;
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* "rsa-sha256" or "ed25519-sha256", follows the key type when not set
|
|
1379
|
+
*/
|
|
1380
|
+
algorithm?: 'rsa-sha256' | 'ed25519-sha256';
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* DKIM2 signing options
|
|
1385
|
+
*/
|
|
1386
|
+
export interface DKIM2SignOptions {
|
|
1387
|
+
/**
|
|
1388
|
+
* Signing domain (d= tag)
|
|
1389
|
+
*/
|
|
1390
|
+
signingDomain: string;
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Signing keys. At most two keys per algorithm, each with its own selector
|
|
1394
|
+
*/
|
|
1395
|
+
signatureData?: DKIM2SignatureKey[];
|
|
1396
|
+
|
|
1397
|
+
/**
|
|
1398
|
+
* Selector of a single key, used with `privateKey` when `signatureData` is not set
|
|
1399
|
+
*/
|
|
1400
|
+
selector?: string;
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* A single private key, used with `selector` when `signatureData` is not set
|
|
1404
|
+
*/
|
|
1405
|
+
privateKey?: string | Buffer;
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Algorithm of the single key
|
|
1409
|
+
*/
|
|
1410
|
+
algorithm?: 'rsa-sha256' | 'ed25519-sha256';
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* SMTP MAIL FROM the message is sent with (mf= tag), "" or "<>" for the null sender.
|
|
1414
|
+
* Required unless `nextDomain` is set
|
|
1415
|
+
*/
|
|
1416
|
+
mailFrom?: string;
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* SMTP RCPT TO addresses the message is sent to (rt= tag). Required unless `nextDomain` is set
|
|
1420
|
+
*/
|
|
1421
|
+
rcptTo?: string | string[];
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* Domain of the next DKIM2-Signature for an imaginary hop (nd= tag), instead of mailFrom and rcptTo
|
|
1425
|
+
*/
|
|
1426
|
+
nextDomain?: string;
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* Nonce (n= tag), at most 64 printable ASCII characters without ";"
|
|
1430
|
+
*/
|
|
1431
|
+
nonce?: string;
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* Flags (f= tag), such as "donotmodify", "donotexplode", "exploded", "feedback" or "feedhere"
|
|
1435
|
+
*/
|
|
1436
|
+
flags?: string[];
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Hash algorithms of a new Message-Instance (default: ["sha256"])
|
|
1440
|
+
*/
|
|
1441
|
+
hashAlgorithms?: ('sha256' | 'sha512')[];
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* Recipe that recreates the previous instance of a message that was changed since its
|
|
1445
|
+
* highest Message-Instance. It is checked against that instance before signing
|
|
1446
|
+
*/
|
|
1447
|
+
recipe?: DKIM2Recipe;
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Signing time (t= tag), defaults to now
|
|
1451
|
+
*/
|
|
1452
|
+
signTime?: Date | string | number;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* DKIM2 signing result
|
|
1457
|
+
*/
|
|
1458
|
+
export interface DKIM2SignResult {
|
|
1459
|
+
/**
|
|
1460
|
+
* Header fields to prepend to the message, each ending with CRLF
|
|
1461
|
+
*/
|
|
1462
|
+
signatures: string;
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* The new DKIM2-Signature header field
|
|
1466
|
+
*/
|
|
1467
|
+
signature: string;
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* The new Message-Instance header field, null when the message did not change
|
|
1471
|
+
*/
|
|
1472
|
+
messageInstance: string | null;
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* i= of the new signature
|
|
1476
|
+
*/
|
|
1477
|
+
i: number;
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* m= of the new signature
|
|
1481
|
+
*/
|
|
1482
|
+
m: number;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* Signs a message with DKIM2. Rejects for invalid options, an invalid DKIM2 chain on the
|
|
1487
|
+
* message, or a recipe that does not recreate the previous instance
|
|
1488
|
+
*/
|
|
1489
|
+
export function dkim2Sign(input: MessageInput, options: DKIM2SignOptions): Promise<DKIM2SignResult>;
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* Transform stream that prepends the DKIM2 header fields to the message
|
|
1493
|
+
*/
|
|
1494
|
+
export class Dkim2SignStream extends Transform {
|
|
1495
|
+
constructor(options: DKIM2SignOptions);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* DKIM2 verification options
|
|
1500
|
+
*/
|
|
1501
|
+
export interface DKIM2VerifyOptions {
|
|
1502
|
+
/**
|
|
1503
|
+
* Custom DNS resolver function
|
|
1504
|
+
*/
|
|
1505
|
+
resolver?: DNSResolver;
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* SMTP MAIL FROM of the delivery, "" for the null sender. Compared with mf= of the highest
|
|
1509
|
+
* DKIM2-Signature when set
|
|
1510
|
+
*/
|
|
1511
|
+
mailFrom?: string;
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* SMTP RCPT TO addresses of the delivery, each of which has to be listed in rt= of the highest
|
|
1515
|
+
* DKIM2-Signature
|
|
1516
|
+
*/
|
|
1517
|
+
rcptTo?: string | string[];
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Seconds after t= when a signature expires (default: 1209600, 14 days), false to not check
|
|
1521
|
+
*/
|
|
1522
|
+
maxSignatureAge?: number | false;
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Seconds a t= may be ahead of the current time (default: 300), false to not check. Section 8.4
|
|
1526
|
+
* allows ignoring signatures from the future
|
|
1527
|
+
*/
|
|
1528
|
+
maxFutureTime?: number | false;
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* Time to verify against (default: now)
|
|
1532
|
+
*/
|
|
1533
|
+
curTime?: Date | string | number;
|
|
1534
|
+
|
|
1535
|
+
/**
|
|
1536
|
+
* Most Message-Instance, and most DKIM2-Signature, header fields processed (default: 20)
|
|
1537
|
+
*/
|
|
1538
|
+
maxInstances?: number;
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* Called with the replay key (the m=1 hashes) of the message. Return true when a message with
|
|
1542
|
+
* the same key was seen before; without an exploded flag the result is then FAIL
|
|
1543
|
+
*/
|
|
1544
|
+
checkReplay?: (replay: { key: string; exploded: boolean }) => Promise<boolean> | boolean;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
export type DKIM2Result = 'pass' | 'fail' | 'permerror' | 'temperror';
|
|
1548
|
+
|
|
1549
|
+
export type DKIM2Warning = 'mail-from-not-checked' | 'rcpt-to-not-checked';
|
|
1550
|
+
|
|
1551
|
+
export interface DKIM2Error {
|
|
1552
|
+
result: DKIM2Result;
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Human-readable string of draft-ietf-dkim-dkim2-spec-06 section 11
|
|
1556
|
+
*/
|
|
1557
|
+
message: string;
|
|
1558
|
+
|
|
1559
|
+
/**
|
|
1560
|
+
* i= of the DKIM2-Signature the error is reported against
|
|
1561
|
+
*/
|
|
1562
|
+
i?: number;
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* m= of the Message-Instance the error is about
|
|
1566
|
+
*/
|
|
1567
|
+
m?: number;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
export interface DKIM2InstanceResult {
|
|
1571
|
+
m: number;
|
|
1572
|
+
hashes: { algorithm: string; header: 'pass' | 'fail'; body: 'pass' | 'fail' | 'unknown' }[];
|
|
1573
|
+
recipe?: { headers: string[]; body: 'unchanged' | 'recipe' | 'unrecoverable' };
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
export interface DKIM2SignatureValueResult {
|
|
1577
|
+
selector: string;
|
|
1578
|
+
algorithm: string;
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* "none" for an algorithm that is not supported and ignored
|
|
1582
|
+
*/
|
|
1583
|
+
result?: DKIM2Result | 'none';
|
|
1584
|
+
comment?: string;
|
|
1585
|
+
rr?: string;
|
|
1586
|
+
modulusLength?: number;
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* The key record has the t=y testing flag
|
|
1590
|
+
*/
|
|
1591
|
+
testing?: boolean;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
export interface DKIM2SignatureResult {
|
|
1595
|
+
i: number;
|
|
1596
|
+
m: number;
|
|
1597
|
+
signingDomain: string;
|
|
1598
|
+
timestamp: number;
|
|
1599
|
+
signTime: string;
|
|
1600
|
+
flags: string[];
|
|
1601
|
+
mailFrom?: string;
|
|
1602
|
+
rcptTo?: string[];
|
|
1603
|
+
nextDomain?: string;
|
|
1604
|
+
nonce?: string;
|
|
1605
|
+
status: { result: DKIM2Result | 'skipped'; comment?: string };
|
|
1606
|
+
values: DKIM2SignatureValueResult[];
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* DKIM2 verification result
|
|
1611
|
+
*/
|
|
1612
|
+
export interface DKIM2VerifyResult {
|
|
1613
|
+
status: {
|
|
1614
|
+
result: DKIM2Result | 'none';
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* The human-readable string of the reported failure
|
|
1618
|
+
*/
|
|
1619
|
+
comment?: string;
|
|
1620
|
+
header?: { d?: string; i?: number };
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* The parts of the SMTP envelope that were not given, so the highest DKIM2-Signature was
|
|
1624
|
+
* not compared with them and an unchanged copy sent to someone else would pass as well.
|
|
1625
|
+
* Only present when there is something to report, never written into `info`
|
|
1626
|
+
*/
|
|
1627
|
+
warnings?: DKIM2Warning[];
|
|
1628
|
+
};
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* Authentication-Results resinfo
|
|
1632
|
+
*/
|
|
1633
|
+
info: string;
|
|
1634
|
+
errors: DKIM2Error[];
|
|
1635
|
+
instances: DKIM2InstanceResult[];
|
|
1636
|
+
signatures: DKIM2SignatureResult[];
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* The key for replay detection and whether a signature allows copies
|
|
1640
|
+
*/
|
|
1641
|
+
replay?: { key: string; exploded: boolean };
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* DKIM2 hashing options
|
|
1646
|
+
*/
|
|
1647
|
+
export interface DKIM2HashOptions {
|
|
1648
|
+
/**
|
|
1649
|
+
* Hash algorithms (default: ["sha256"])
|
|
1650
|
+
*/
|
|
1651
|
+
algorithms?: ('sha256' | 'sha512')[];
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
/**
|
|
1655
|
+
* DKIM2 hashes of a message
|
|
1656
|
+
*/
|
|
1657
|
+
export interface DKIM2HashResult {
|
|
1658
|
+
/**
|
|
1659
|
+
* Names of the hashed header fields, in the order they are hashed
|
|
1660
|
+
*/
|
|
1661
|
+
headers: string[];
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* The hash-sets of the h= tag of a Message-Instance
|
|
1665
|
+
*/
|
|
1666
|
+
hashes: { algorithm: 'sha256' | 'sha512'; headerHash: string; bodyHash: string }[];
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* Computes the DKIM2 header and body hashes of a message, as they go into the h= tag of a
|
|
1671
|
+
* Message-Instance header field. Rejects for an unsupported hash algorithm
|
|
1672
|
+
*/
|
|
1673
|
+
export function dkim2Hash(input: MessageInput, options?: DKIM2HashOptions): Promise<DKIM2HashResult>;
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* Verifies the DKIM2 header fields of a message
|
|
1677
|
+
*/
|
|
1678
|
+
export function dkim2Verify(input: MessageInput, options?: DKIM2VerifyOptions): Promise<DKIM2VerifyResult>;
|
|
1679
|
+
|
|
1321
1680
|
// ============================================================================
|
|
1322
1681
|
// SPF
|
|
1323
1682
|
// ============================================================================
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { dkim2Hash } = require('../dkim2/hash');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
|
|
6
|
+
const cmd = async argv => {
|
|
7
|
+
let source = argv.email;
|
|
8
|
+
if (argv.verbose) {
|
|
9
|
+
console.error(`Reading email message from ${source || 'standard input'}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { headers, hashes } = await dkim2Hash(source ? fs.createReadStream(source) : process.stdin, { algorithms: argv.algo });
|
|
13
|
+
|
|
14
|
+
if (argv.verbose) {
|
|
15
|
+
console.error(`Hashed header fields: ${headers.join(', ') || '(none)'}`);
|
|
16
|
+
console.error('--------');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// the hash-sets of the h= tag of a Message-Instance (section 7.3)
|
|
20
|
+
for (let { algorithm, headerHash, bodyHash } of hashes) {
|
|
21
|
+
process.stdout.write(`${algorithm}:${headerHash}:${bodyHash}\n`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
module.exports = cmd;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { dkim2Sign } = require('../dkim2/sign');
|
|
4
|
+
const { GathererStream } = require('../gatherer-stream');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
|
|
7
|
+
const cmd = async argv => {
|
|
8
|
+
let source = argv.email;
|
|
9
|
+
let stream;
|
|
10
|
+
|
|
11
|
+
if (argv.verbose) {
|
|
12
|
+
console.error(`Reading email message from ${source || 'standard input'}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
stream = source ? fs.createReadStream(source) : process.stdin;
|
|
16
|
+
|
|
17
|
+
let privateKeys = argv.privateKey || [];
|
|
18
|
+
let selectors = argv.selector || [];
|
|
19
|
+
if (privateKeys.length !== selectors.length) {
|
|
20
|
+
let err = new Error('Every --private-key needs a --selector, in the same order');
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let gatherer = new GathererStream({ gather: !argv.headersOnly });
|
|
25
|
+
stream.pipe(gatherer);
|
|
26
|
+
stream.on('error', err => gatherer.emit('error', err));
|
|
27
|
+
|
|
28
|
+
let signatureData = [];
|
|
29
|
+
for (let i = 0; i < privateKeys.length; i++) {
|
|
30
|
+
signatureData.push({ selector: selectors[i], privateKey: await fs.promises.readFile(privateKeys[i], 'utf-8') });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let options = {
|
|
34
|
+
signingDomain: argv.domain,
|
|
35
|
+
signatureData,
|
|
36
|
+
signTime: argv.time ? new Date(argv.time * 1000) : new Date(),
|
|
37
|
+
flags: argv.flag,
|
|
38
|
+
nonce: argv.nonce
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (argv.hash?.length) {
|
|
42
|
+
options.hashAlgorithms = argv.hash;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (argv.nextDomain) {
|
|
46
|
+
options.nextDomain = argv.nextDomain;
|
|
47
|
+
} else {
|
|
48
|
+
options.mailFrom = argv.mailFrom;
|
|
49
|
+
options.rcptTo = argv.rcptTo;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (argv.recipe) {
|
|
53
|
+
options.recipe = JSON.parse(await fs.promises.readFile(argv.recipe, 'utf-8'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (argv.verbose) {
|
|
57
|
+
console.error(`Signing domain: ${options.signingDomain}`);
|
|
58
|
+
console.error(`Selectors: ${selectors.join(', ')}`);
|
|
59
|
+
console.error(`Signing time: ${options.signTime.toISOString()}`);
|
|
60
|
+
console.error('--------');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let signResult = await dkim2Sign(gatherer, options);
|
|
64
|
+
|
|
65
|
+
process.stdout.write(signResult.signatures);
|
|
66
|
+
if (!argv.headersOnly) {
|
|
67
|
+
// print the full message as well
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
let msgStream = gatherer.replay();
|
|
70
|
+
msgStream.pipe(process.stdout, { end: false });
|
|
71
|
+
msgStream.on('end', resolve);
|
|
72
|
+
msgStream.on('error', reject);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
module.exports = cmd;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { dkim2Verify } = require('../dkim2/verify');
|
|
4
|
+
const { createResolver } = require('./resolver');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
|
|
7
|
+
const cmd = async argv => {
|
|
8
|
+
let source = argv.email;
|
|
9
|
+
|
|
10
|
+
if (argv.verbose) {
|
|
11
|
+
console.error(`Reading email message from ${source || 'standard input'}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let stream = source ? fs.createReadStream(source) : process.stdin;
|
|
15
|
+
|
|
16
|
+
let opts = {
|
|
17
|
+
resolver: await createResolver(argv),
|
|
18
|
+
rcptTo: argv.rcptTo
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (typeof argv.mailFrom === 'string') {
|
|
22
|
+
opts.mailFrom = argv.mailFrom;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof argv.time === 'number') {
|
|
26
|
+
opts.curTime = new Date(argv.time * 1000);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof argv.maxAge === 'number') {
|
|
30
|
+
// 0 turns the expiration check off
|
|
31
|
+
opts.maxSignatureAge = argv.maxAge || false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof argv.maxFuture === 'number') {
|
|
35
|
+
// 0 turns the check off, like --max-age
|
|
36
|
+
opts.maxFutureTime = argv.maxFuture || false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof argv.maxInstances === 'number') {
|
|
40
|
+
opts.maxInstances = argv.maxInstances;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let result = await dkim2Verify(stream, opts);
|
|
44
|
+
|
|
45
|
+
if (argv.verbose) {
|
|
46
|
+
for (let warning of result.status.warnings || []) {
|
|
47
|
+
console.error(`Warning: ${warning}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (argv.headersOnly) {
|
|
52
|
+
process.stdout.write(result.info + '\n');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
process.stdout.write(JSON.stringify(result, false, 2) + '\n');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports = cmd;
|
package/lib/commands/report.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { authenticate } = require('../mailauth');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
|
-
const {
|
|
5
|
+
const { createResolver } = require('./resolver');
|
|
6
6
|
|
|
7
7
|
const cmd = async argv => {
|
|
8
8
|
let source = argv.email;
|
|
@@ -27,9 +27,14 @@ const cmd = async argv => {
|
|
|
27
27
|
const opts = {
|
|
28
28
|
trustReceived: true,
|
|
29
29
|
strict: !!argv.strict,
|
|
30
|
-
rejectRsaSha1: !!argv.rejectRsaSha1
|
|
30
|
+
rejectRsaSha1: !!argv.rejectRsaSha1,
|
|
31
|
+
dkim2: !!argv.dkim2
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
if (argv.rcptTo) {
|
|
35
|
+
opts.rcptTo = argv.rcptTo;
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
if (argv.clientIp) {
|
|
34
39
|
opts.ip = argv.clientIp;
|
|
35
40
|
}
|
|
@@ -48,37 +53,7 @@ const cmd = async argv => {
|
|
|
48
53
|
}
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
let dnsCache = JSON.parse(await fs.promises.readFile(argv.dnsCache, 'utf-8'));
|
|
53
|
-
|
|
54
|
-
opts.resolver = async (name, rr) => {
|
|
55
|
-
let match = dnsCache?.[name]?.[rr];
|
|
56
|
-
|
|
57
|
-
if (argv.verbose) {
|
|
58
|
-
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'} (using cache)`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!match) {
|
|
62
|
-
let err = new Error('Error');
|
|
63
|
-
err.code = 'ENOTFOUND';
|
|
64
|
-
throw err;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return match;
|
|
68
|
-
};
|
|
69
|
-
} else if (argv.verbose) {
|
|
70
|
-
opts.resolver = async (name, rr) => {
|
|
71
|
-
let match;
|
|
72
|
-
try {
|
|
73
|
-
match = await resolve(name, rr);
|
|
74
|
-
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
|
|
75
|
-
return match;
|
|
76
|
-
} catch (err) {
|
|
77
|
-
console.error(`DNS query for ${rr} ${name}: ${err.message}${err.code ? ` [${err.code}]` : ''}`);
|
|
78
|
-
throw err;
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
}
|
|
56
|
+
opts.resolver = await createResolver(argv);
|
|
82
57
|
|
|
83
58
|
let result = await authenticate(stream, opts);
|
|
84
59
|
process.stdout.write(JSON.stringify(result, false, 2) + '\n');
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const { resolve } = require('node:dns').promises;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The DNS resolver for a command: answers from the --dns-cache file, or real DNS queries that are
|
|
8
|
+
* logged with --verbose. Undefined when neither is set, so the library uses its default resolver
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} argv Command options
|
|
11
|
+
* @returns {Function|undefined} async (name, rrtype) resolver
|
|
12
|
+
*/
|
|
13
|
+
const createResolver = async argv => {
|
|
14
|
+
if (argv.dnsCache) {
|
|
15
|
+
let dnsCache = JSON.parse(await fs.promises.readFile(argv.dnsCache, 'utf-8'));
|
|
16
|
+
|
|
17
|
+
return async (name, rr) => {
|
|
18
|
+
let match = dnsCache?.[name]?.[rr];
|
|
19
|
+
|
|
20
|
+
if (argv.verbose) {
|
|
21
|
+
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'} (using cache)`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!match) {
|
|
25
|
+
let err = new Error('Error');
|
|
26
|
+
err.code = 'ENOTFOUND';
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return match;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (argv.verbose) {
|
|
35
|
+
return async (name, rr) => {
|
|
36
|
+
let match;
|
|
37
|
+
try {
|
|
38
|
+
match = await resolve(name, rr);
|
|
39
|
+
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
|
|
40
|
+
return match;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(`DNS query for ${rr} ${name}: ${err.message}${err.code ? ` [${err.code}]` : ''}`);
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = { createResolver };
|
package/lib/commands/seal.js
CHANGED
|
@@ -5,7 +5,7 @@ const { authenticate } = require('../mailauth');
|
|
|
5
5
|
const { createSeal } = require('../arc');
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const { GathererStream } = require('../gatherer-stream');
|
|
8
|
-
const {
|
|
8
|
+
const { createResolver } = require('./resolver');
|
|
9
9
|
|
|
10
10
|
const writeMessageBody = async gatherer =>
|
|
11
11
|
new Promise((resolve, reject) => {
|
|
@@ -161,37 +161,7 @@ const cmd = async argv => {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
let dnsCache = JSON.parse(await fs.promises.readFile(argv.dnsCache, 'utf-8'));
|
|
166
|
-
|
|
167
|
-
opts.resolver = async (name, rr) => {
|
|
168
|
-
let match = dnsCache?.[name]?.[rr];
|
|
169
|
-
|
|
170
|
-
if (argv.verbose) {
|
|
171
|
-
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'} (using cache)`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (!match) {
|
|
175
|
-
let err = new Error('Error');
|
|
176
|
-
err.code = 'ENOTFOUND';
|
|
177
|
-
throw err;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return match;
|
|
181
|
-
};
|
|
182
|
-
} else if (argv.verbose) {
|
|
183
|
-
opts.resolver = async (name, rr) => {
|
|
184
|
-
let match;
|
|
185
|
-
try {
|
|
186
|
-
match = await resolve(name, rr);
|
|
187
|
-
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
|
|
188
|
-
return match;
|
|
189
|
-
} catch (err) {
|
|
190
|
-
console.error(`DNS query for ${rr} ${name}: ${err.message}${err.code ? ` [${err.code}]` : ''}`);
|
|
191
|
-
throw err;
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
}
|
|
164
|
+
opts.resolver = await createResolver(argv);
|
|
195
165
|
|
|
196
166
|
let result = await authenticate(gatherer, opts);
|
|
197
167
|
|