@blamejs/core 0.7.24 → 0.7.39
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 +30 -0
- package/index.js +4 -0
- package/lib/asn1-der.js +356 -0
- package/lib/audit.js +2 -0
- package/lib/compliance.js +114 -0
- package/lib/constants.js +8 -0
- package/lib/crypto.js +92 -0
- package/lib/dora.js +347 -0
- package/lib/framework-error.js +22 -0
- package/lib/gate-contract.js +20 -4
- package/lib/mail-auth.js +661 -0
- package/lib/mail-dkim.js +309 -4
- package/lib/mail.js +8 -0
- package/lib/network-smtp-policy.js +551 -0
- package/lib/network-tls.js +1169 -0
- package/lib/network.js +7 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/lib/mail-dkim.js
CHANGED
|
@@ -63,6 +63,12 @@ var ALLOWED_CANON = [
|
|
|
63
63
|
];
|
|
64
64
|
var DEFAULT_HEADERS = ["from", "to", "subject", "date", "message-id"];
|
|
65
65
|
|
|
66
|
+
// RSA modulus bit-size thresholds per RFC 8301 §3.1 + M³AAWG hardening
|
|
67
|
+
// guidance. Anything below MIN must be considered failure; below WEAK
|
|
68
|
+
// emits a warning so operators can quarantine while transitioning.
|
|
69
|
+
var RSA_MIN_BITS = 1024; // allow:raw-byte-literal — RFC 8301 RSA bit floor
|
|
70
|
+
var RSA_WEAK_BITS = 2048; // allow:raw-byte-literal — RFC 8301 RSA bit weak threshold
|
|
71
|
+
|
|
66
72
|
// ---- Canonicalization (RFC 6376 §3.4) ----
|
|
67
73
|
|
|
68
74
|
function _canonHeaderRelaxed(name, value) {
|
|
@@ -184,11 +190,13 @@ function _foldSignatureHeader(unfolded) {
|
|
|
184
190
|
var name = "DKIM-Signature: ";
|
|
185
191
|
var rest = unfolded;
|
|
186
192
|
if ((name + rest).length <= maxLine) return name + rest;
|
|
187
|
-
// Fold on tag boundaries (`; tag=value`).
|
|
188
|
-
//
|
|
189
|
-
//
|
|
193
|
+
// Fold on tag boundaries (`; tag=value`). Each non-last chunk keeps
|
|
194
|
+
// its trailing `;` (RFC 6376 §3.2 — `;` is the tag-list separator,
|
|
195
|
+
// not a tag-end terminator; receivers' parsers expect it on the
|
|
196
|
+
// PRIOR line at fold points). Earlier shape ("v=1\r\n\ta=...;")
|
|
197
|
+
// missed the first separator and tripped strict parsers.
|
|
190
198
|
var parts = rest.split("; ");
|
|
191
|
-
var lines = [name + parts[0]];
|
|
199
|
+
var lines = [name + parts[0] + (parts.length > 1 ? ";" : "")];
|
|
192
200
|
for (var i = 1; i < parts.length; i++) {
|
|
193
201
|
lines.push("\t" + parts[i] + (i < parts.length - 1 ? ";" : ""));
|
|
194
202
|
}
|
|
@@ -379,6 +387,301 @@ function create(opts) {
|
|
|
379
387
|
// Both signers are constructed eagerly at create-time (configuration
|
|
380
388
|
// errors surface at boot, not at first send). The combined sign()
|
|
381
389
|
// applies the RSA signer first, then the Ed25519 signer on top.
|
|
390
|
+
// ---- DKIM-Signature verification (RFC 6376 §6) ----
|
|
391
|
+
//
|
|
392
|
+
// Counterpart to the signer. Walks every DKIM-Signature header in
|
|
393
|
+
// the message, parses the tag list, fetches the signing public key
|
|
394
|
+
// from DNS TXT at <selector>._domainkey.<domain>, canonicalizes the
|
|
395
|
+
// body + headers per the c= tag, and runs nodeCrypto.verify.
|
|
396
|
+
//
|
|
397
|
+
// Surface:
|
|
398
|
+
//
|
|
399
|
+
// var rv = await b.mail.dkim.verify(rfc822, {
|
|
400
|
+
// dnsLookup: async function (qname, type) { return [["v=DKIM1; k=rsa; p=BASE64..."]]; },
|
|
401
|
+
// });
|
|
402
|
+
// // → [{ d, s, alg, result, errors }, ...]
|
|
403
|
+
//
|
|
404
|
+
// One result per DKIM-Signature header (operators expect multiple
|
|
405
|
+
// when senders dual-sign with RSA + Ed25519). Each result's `result`
|
|
406
|
+
// is one of: "pass" / "fail" / "permerror" / "temperror" / "neutral".
|
|
407
|
+
//
|
|
408
|
+
// dnsLookup is operator-supplied so verify() composes with the
|
|
409
|
+
// framework's b.network.dns (DoH / DoT / system) without taking a
|
|
410
|
+
// hard dependency on it. When omitted, falls back to node:dns.
|
|
411
|
+
|
|
412
|
+
function _parseDkimTagList(value) {
|
|
413
|
+
// RFC 6376 §3.2 — tags are `key=value` separated by `;`. Whitespace
|
|
414
|
+
// around `=` and `;` is allowed and stripped. The signer folds the
|
|
415
|
+
// DKIM-Signature header across CRLF + WSP; unfold first so tag
|
|
416
|
+
// boundaries land in the right place.
|
|
417
|
+
var unfolded = String(value).replace(/\r?\n[ \t]+/g, " ");
|
|
418
|
+
var tags = {};
|
|
419
|
+
var parts = unfolded.split(";");
|
|
420
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
421
|
+
var p = parts[i].trim();
|
|
422
|
+
if (p.length === 0) continue;
|
|
423
|
+
var eq = p.indexOf("=");
|
|
424
|
+
if (eq === -1) continue;
|
|
425
|
+
var key = p.slice(0, eq).trim().toLowerCase();
|
|
426
|
+
var val = p.slice(eq + 1).trim();
|
|
427
|
+
// Whitespace inside values is unfolded — RFC 6376 §3.2 says FWS
|
|
428
|
+
// (folding whitespace) is ignored within a tag value. Strip
|
|
429
|
+
// newlines + tabs while preserving the meaningful tokens.
|
|
430
|
+
val = val.replace(/\s+/g, "");
|
|
431
|
+
tags[key] = val;
|
|
432
|
+
}
|
|
433
|
+
return tags;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function _selectorTxtToKeyTags(txtRecords) {
|
|
437
|
+
// DKIM key record is a TXT record at <selector>._domainkey.<domain>.
|
|
438
|
+
// Format: "v=DKIM1; k=rsa; p=<base64>" (chunks may be split across
|
|
439
|
+
// multi-string TXT). Returns { v, k, p, t, h } or throws.
|
|
440
|
+
var joined = "";
|
|
441
|
+
if (Array.isArray(txtRecords)) {
|
|
442
|
+
for (var i = 0; i < txtRecords.length; i += 1) {
|
|
443
|
+
var rec = txtRecords[i];
|
|
444
|
+
joined = Array.isArray(rec) ? rec.join("") : String(rec);
|
|
445
|
+
if (joined.indexOf("v=DKIM1") === 0 || joined.indexOf("p=") !== -1) break;
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
joined = String(txtRecords || "");
|
|
449
|
+
}
|
|
450
|
+
if (joined.length === 0) {
|
|
451
|
+
throw new DkimError("dkim/key-not-found", "DKIM key record is empty");
|
|
452
|
+
}
|
|
453
|
+
return _parseDkimTagList(joined);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Process-local cache for fetched DKIM key TXT records. Mailing-list
|
|
457
|
+
// fan-out and bulk-replay scenarios frequently re-fetch the same
|
|
458
|
+
// selector; without a cache the verifier hammers DNS. TTL bounded so
|
|
459
|
+
// rotated keys propagate within minutes, not hours.
|
|
460
|
+
var DKIM_KEY_CACHE = new Map();
|
|
461
|
+
var DKIM_KEY_CACHE_TTL_MS = 5 * 60 * 1000; // allow:raw-time-literal — TTL ms expression
|
|
462
|
+
var DKIM_KEY_CACHE_MAX_ENTRIES = 1024;
|
|
463
|
+
|
|
464
|
+
function _cacheGet(qname) {
|
|
465
|
+
var ent = DKIM_KEY_CACHE.get(qname);
|
|
466
|
+
if (!ent) return null;
|
|
467
|
+
if (ent.expires <= Date.now()) {
|
|
468
|
+
DKIM_KEY_CACHE.delete(qname);
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
return ent.tags;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function _cachePut(qname, tags) {
|
|
475
|
+
if (DKIM_KEY_CACHE.size >= DKIM_KEY_CACHE_MAX_ENTRIES) {
|
|
476
|
+
// Drop oldest (Map preserves insertion order). Cheap LRU-ish.
|
|
477
|
+
var oldest = DKIM_KEY_CACHE.keys().next().value;
|
|
478
|
+
if (oldest !== undefined) DKIM_KEY_CACHE.delete(oldest);
|
|
479
|
+
}
|
|
480
|
+
DKIM_KEY_CACHE.set(qname, { tags: tags, expires: Date.now() + DKIM_KEY_CACHE_TTL_MS });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function _resetDkimKeyCacheForTest() { DKIM_KEY_CACHE.clear(); }
|
|
484
|
+
|
|
485
|
+
function _pemFromB64KeyMaterial(b64) {
|
|
486
|
+
// RSA: SubjectPublicKeyInfo DER in base64. Ed25519: raw 32-byte key
|
|
487
|
+
// OR SPKI DER. Wrap in PEM markers so node:crypto.createPublicKey
|
|
488
|
+
// accepts it.
|
|
489
|
+
var pem = "-----BEGIN PUBLIC KEY-----\n";
|
|
490
|
+
// 64-char wrap (PEM convention).
|
|
491
|
+
for (var i = 0; i < b64.length; i += 64) { // allow:raw-byte-literal — PEM wrap width
|
|
492
|
+
pem += b64.slice(i, i + 64) + "\n"; // allow:raw-byte-literal — PEM wrap width
|
|
493
|
+
}
|
|
494
|
+
pem += "-----END PUBLIC KEY-----\n";
|
|
495
|
+
return pem;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function _fetchDkimKey(domain, selector, dnsLookup) {
|
|
499
|
+
var qname = selector + "._domainkey." + domain;
|
|
500
|
+
var cached = _cacheGet(qname);
|
|
501
|
+
if (cached) return cached;
|
|
502
|
+
var records;
|
|
503
|
+
try {
|
|
504
|
+
if (dnsLookup) {
|
|
505
|
+
records = await dnsLookup(qname, "TXT");
|
|
506
|
+
} else {
|
|
507
|
+
var dnsModule = require("node:dns/promises");
|
|
508
|
+
records = await dnsModule.resolveTxt(qname);
|
|
509
|
+
}
|
|
510
|
+
} catch (e) {
|
|
511
|
+
if (e && (e.code === "ENOTFOUND" || e.code === "ENODATA")) {
|
|
512
|
+
throw new DkimError("dkim/key-not-found",
|
|
513
|
+
"no DKIM TXT record at " + qname);
|
|
514
|
+
}
|
|
515
|
+
throw new DkimError("dkim/key-lookup-temperror",
|
|
516
|
+
"DKIM TXT lookup for " + qname + " failed: " +
|
|
517
|
+
((e && e.message) || String(e)));
|
|
518
|
+
}
|
|
519
|
+
var tags = _selectorTxtToKeyTags(records);
|
|
520
|
+
_cachePut(qname, tags);
|
|
521
|
+
return tags;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function _findDkimSignatureHeaders(parsedHeaders) {
|
|
525
|
+
var out = [];
|
|
526
|
+
for (var i = 0; i < parsedHeaders.length; i += 1) {
|
|
527
|
+
if (parsedHeaders[i].name.toLowerCase() === "dkim-signature") {
|
|
528
|
+
out.push({ index: i, name: parsedHeaders[i].name, value: parsedHeaders[i].value });
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return out;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function _verifySingleSignature(rfc822, parsedHeaders, sigHeader, keyTags, sigTags) {
|
|
535
|
+
// Reconstruct what the signer canonicalized, per RFC 6376 §3.7.
|
|
536
|
+
var canonicalization = sigTags.c || "simple/simple";
|
|
537
|
+
var canonHeader = canonicalization.split("/")[0];
|
|
538
|
+
var canonBody = canonicalization.split("/")[1];
|
|
539
|
+
var algorithm = sigTags.a;
|
|
540
|
+
|
|
541
|
+
var split = _splitHeadersBody(rfc822);
|
|
542
|
+
var body = split.body;
|
|
543
|
+
if (sigTags.l !== undefined) {
|
|
544
|
+
// The framework refuses l= at SIGN-time per the M3AAWG / Gmail /
|
|
545
|
+
// Microsoft 365 guidance (v0.7.18). On VERIFY, an `l=` tag on an
|
|
546
|
+
// inbound signature signals append-after-signature exposure —
|
|
547
|
+
// operators decide acceptance. Honor the cap for the body hash so
|
|
548
|
+
// the signature still validates against legitimate senders that
|
|
549
|
+
// use l=, but flag in the result.
|
|
550
|
+
var lcap = parseInt(sigTags.l, 10);
|
|
551
|
+
if (isFinite(lcap) && lcap >= 0) body = body.slice(0, lcap);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// 1. Body-hash check.
|
|
555
|
+
var expectedBh = sigTags.bh;
|
|
556
|
+
if (typeof expectedBh !== "string") {
|
|
557
|
+
return { result: "permerror", errors: ["DKIM-Signature missing bh="] };
|
|
558
|
+
}
|
|
559
|
+
var actualBh = _bodyHashB64(body, algorithm, canonBody);
|
|
560
|
+
if (actualBh !== expectedBh) {
|
|
561
|
+
return { result: "fail", errors: ["body hash mismatch"] };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// 2. Canonicalize the headers in h= order, then the DKIM-Signature
|
|
565
|
+
// header itself with the b= value emptied (per §3.7).
|
|
566
|
+
var headerNames = (sigTags.h || "").split(":").map(function (s) {
|
|
567
|
+
return s.trim().toLowerCase();
|
|
568
|
+
});
|
|
569
|
+
var lcNames = parsedHeaders.map(function (h) { return h.name.toLowerCase(); });
|
|
570
|
+
var canonicalizedHeaders = "";
|
|
571
|
+
for (var j = 0; j < headerNames.length; j += 1) {
|
|
572
|
+
var want = headerNames[j];
|
|
573
|
+
if (want.length === 0) continue;
|
|
574
|
+
var idx = lcNames.lastIndexOf(want); // last-occurrence per the DKIM spec
|
|
575
|
+
if (idx === -1) continue;
|
|
576
|
+
var h = parsedHeaders[idx];
|
|
577
|
+
canonicalizedHeaders += canonHeader === "simple"
|
|
578
|
+
? _canonHeaderSimple(h.name, h.value)
|
|
579
|
+
: _canonHeaderRelaxed(h.name, h.value);
|
|
580
|
+
}
|
|
581
|
+
// Strip the b= value from the DKIM-Signature header for the canonical
|
|
582
|
+
// form per §3.7.
|
|
583
|
+
var unsignedSigValue = sigHeader.value.replace(/(\bb=)[^;]*/i, "$1");
|
|
584
|
+
canonicalizedHeaders += canonHeader === "simple"
|
|
585
|
+
? _canonHeaderSimple("DKIM-Signature", " " + unsignedSigValue).replace(/\r\n$/, "")
|
|
586
|
+
: _canonHeaderRelaxed("DKIM-Signature", unsignedSigValue).replace(/\r\n$/, "");
|
|
587
|
+
|
|
588
|
+
// 3. Verify the signature.
|
|
589
|
+
var sigB64 = sigTags.b;
|
|
590
|
+
if (typeof sigB64 !== "string") {
|
|
591
|
+
return { result: "permerror", errors: ["DKIM-Signature missing b="] };
|
|
592
|
+
}
|
|
593
|
+
var sigBuf = Buffer.from(sigB64, "base64");
|
|
594
|
+
var pem = _pemFromB64KeyMaterial(keyTags.p);
|
|
595
|
+
var keyObj;
|
|
596
|
+
try { keyObj = nodeCrypto.createPublicKey(pem); }
|
|
597
|
+
catch (e) {
|
|
598
|
+
return { result: "permerror",
|
|
599
|
+
errors: ["DKIM key parse failed: " + ((e && e.message) || String(e))] };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
var nodeAlgo = algorithm === "rsa-sha256" ? "sha256" :
|
|
603
|
+
algorithm === "ed25519-sha256" ? null : null;
|
|
604
|
+
if (algorithm !== "rsa-sha256" && algorithm !== "ed25519-sha256") {
|
|
605
|
+
return { result: "permerror",
|
|
606
|
+
errors: ["unsupported DKIM algorithm '" + algorithm + "'"] };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Key-size enforcement (RFC 8301 §3.1, M³AAWG hardening guidance):
|
|
610
|
+
// RSA keys < 1024 bits MUST be considered failure; < 2048 is weak.
|
|
611
|
+
// The framework rejects < 1024 as a security baseline; < 2048 emits
|
|
612
|
+
// a warning in the result so operators can quarantine.
|
|
613
|
+
var warnings = [];
|
|
614
|
+
if (algorithm === "rsa-sha256" && keyObj.asymmetricKeyType === "rsa") {
|
|
615
|
+
var modBits = (keyObj.asymmetricKeyDetails && keyObj.asymmetricKeyDetails.modulusLength) || 0;
|
|
616
|
+
if (modBits > 0 && modBits < RSA_MIN_BITS) {
|
|
617
|
+
return { result: "fail",
|
|
618
|
+
errors: ["RSA key too small: " + modBits + " bits (RFC 8301 §3.1 minimum " + RSA_MIN_BITS + ")"] };
|
|
619
|
+
}
|
|
620
|
+
if (modBits > 0 && modBits < RSA_WEAK_BITS) {
|
|
621
|
+
warnings.push("rsa-key-weak: " + modBits + " bits (< " + RSA_WEAK_BITS + ")");
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (sigTags.l !== undefined) {
|
|
625
|
+
warnings.push("l-tag-present: append-after-signature exposure (RFC 6376 §8.2)");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
var verified;
|
|
629
|
+
try {
|
|
630
|
+
verified = nodeCrypto.verify(nodeAlgo,
|
|
631
|
+
Buffer.from(canonicalizedHeaders, "utf8"), keyObj, sigBuf);
|
|
632
|
+
} catch (e) {
|
|
633
|
+
return { result: "permerror",
|
|
634
|
+
errors: ["DKIM verify threw: " + ((e && e.message) || String(e))] };
|
|
635
|
+
}
|
|
636
|
+
return verified
|
|
637
|
+
? { result: "pass", errors: [], warnings: warnings }
|
|
638
|
+
: { result: "fail", errors: ["signature verification failed"], warnings: warnings };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function verify(rfc822, opts) {
|
|
642
|
+
if (typeof rfc822 !== "string" || rfc822.length === 0) {
|
|
643
|
+
throw new DkimError("dkim/bad-input",
|
|
644
|
+
"verify(): rfc822 must be a non-empty string");
|
|
645
|
+
}
|
|
646
|
+
opts = opts || {};
|
|
647
|
+
validateOpts(opts, ["dnsLookup", "audit"], "mail.dkim.verify");
|
|
648
|
+
|
|
649
|
+
var split = _splitHeadersBody(rfc822);
|
|
650
|
+
var parsedHeaders = _parseHeaders(split.headers);
|
|
651
|
+
var sigHeaders = _findDkimSignatureHeaders(parsedHeaders);
|
|
652
|
+
if (sigHeaders.length === 0) {
|
|
653
|
+
return [{ result: "none", errors: ["no DKIM-Signature headers"] }];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
var results = [];
|
|
657
|
+
for (var i = 0; i < sigHeaders.length; i += 1) {
|
|
658
|
+
var sigTags = _parseDkimTagList(sigHeaders[i].value);
|
|
659
|
+
var d = sigTags.d;
|
|
660
|
+
var s = sigTags.s;
|
|
661
|
+
var alg = sigTags.a;
|
|
662
|
+
if (!d || !s) {
|
|
663
|
+
results.push({ d: d || null, s: s || null, alg: alg || null,
|
|
664
|
+
result: "permerror", errors: ["DKIM-Signature missing d= or s="] });
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
var keyTags;
|
|
668
|
+
try { keyTags = await _fetchDkimKey(d, s, opts.dnsLookup); }
|
|
669
|
+
catch (e) {
|
|
670
|
+
var verdict = e.code === "dkim/key-lookup-temperror" ? "temperror" : "permerror";
|
|
671
|
+
results.push({ d: d, s: s, alg: alg, result: verdict, errors: [e.message] });
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (!keyTags.p) {
|
|
675
|
+
results.push({ d: d, s: s, alg: alg, result: "permerror",
|
|
676
|
+
errors: ["DKIM key record missing p="] });
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
var rv = _verifySingleSignature(rfc822, parsedHeaders, sigHeaders[i], keyTags, sigTags);
|
|
680
|
+
results.push(Object.assign({ d: d, s: s, alg: alg }, rv));
|
|
681
|
+
}
|
|
682
|
+
return results;
|
|
683
|
+
}
|
|
684
|
+
|
|
382
685
|
function dualSigner(opts) {
|
|
383
686
|
if (!opts || !opts.rsa || !opts.eddsa) {
|
|
384
687
|
throw new DkimError("dkim/dual-signer-missing",
|
|
@@ -412,6 +715,8 @@ function dualSigner(opts) {
|
|
|
412
715
|
// directly without going through a full sign() round.
|
|
413
716
|
module.exports = {
|
|
414
717
|
create: create,
|
|
718
|
+
verify: verify,
|
|
719
|
+
_resetDkimKeyCacheForTest: _resetDkimKeyCacheForTest,
|
|
415
720
|
dualSigner: dualSigner,
|
|
416
721
|
DkimError: DkimError,
|
|
417
722
|
_canonHeaderRelaxedForTest: _canonHeaderRelaxed,
|
package/lib/mail.js
CHANGED
|
@@ -70,6 +70,7 @@ var audit = lazyRequire(function () { return require("./audit"); });
|
|
|
70
70
|
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
71
71
|
var guardEmail = lazyRequire(function () { return require("./guard-email"); });
|
|
72
72
|
var mailDkim = require("./mail-dkim");
|
|
73
|
+
var mailAuth = require("./mail-auth");
|
|
73
74
|
var net = lazyRequire(function () { return require("net"); });
|
|
74
75
|
var tls = lazyRequire(function () { return require("tls"); });
|
|
75
76
|
var safeJson = require("./safe-json");
|
|
@@ -968,6 +969,13 @@ module.exports = {
|
|
|
968
969
|
// default, ed25519-sha256 opt-in). Wire it into the smtp transport
|
|
969
970
|
// via opts.dkimSigner. See lib/mail-dkim.js for the full surface.
|
|
970
971
|
dkim: mailDkim,
|
|
972
|
+
// Inbound mail authentication-results verification: SPF (RFC 7208),
|
|
973
|
+
// DMARC (RFC 7489), ARC (RFC 8617). Outbound DKIM signing lives in
|
|
974
|
+
// .dkim above; per-hop DKIM verification is deferred (composes with
|
|
975
|
+
// the existing canonicalization helpers in lib/mail-dkim.js).
|
|
976
|
+
spf: mailAuth.spf,
|
|
977
|
+
dmarc: mailAuth.dmarc,
|
|
978
|
+
arc: mailAuth.arc,
|
|
971
979
|
// Test-only export: lets unit tests inspect the wire format without
|
|
972
980
|
// standing up a TLS-capable SMTP fixture. Operators don't call this.
|
|
973
981
|
_buildRfc822ForTest: _buildRfc822,
|