@blamejs/core 0.14.19 → 0.14.21
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 +4 -0
- package/README.md +1 -1
- package/lib/auth/oauth.js +736 -1
- package/lib/auth/oid4vci.js +124 -5
- package/lib/auth/oid4vp.js +14 -4
- package/lib/auth/sd-jwt-vc-holder.js +46 -1
- package/lib/break-glass.js +1 -2
- package/lib/config.js +28 -31
- package/lib/crypto-field.js +274 -17
- package/lib/dora.js +8 -5
- package/lib/dsr.js +2 -2
- package/lib/flag-evaluation-context.js +7 -0
- package/lib/guard-html-wcag-aria.js +4 -2
- package/lib/guard-html-wcag-forms.js +4 -2
- package/lib/guard-html-wcag-tables.js +4 -2
- package/lib/guard-html-wcag-tagwalk.js +20 -0
- package/lib/guard-html-wcag.js +1 -1
- package/lib/honeytoken.js +27 -20
- package/lib/mail-auth.js +333 -0
- package/lib/mail-deploy.js +1 -1
- package/lib/mail-send-deliver.js +13 -4
- package/lib/middleware/api-encrypt.js +140 -13
- package/lib/middleware/asyncapi-serve.js +3 -0
- package/lib/middleware/csp-report.js +13 -9
- package/lib/middleware/fetch-metadata.js +115 -14
- package/lib/middleware/openapi-serve.js +3 -0
- package/lib/middleware/scim-server.js +297 -19
- package/lib/middleware/security-headers.js +47 -0
- package/lib/middleware/security-txt.js +1 -2
- package/lib/middleware/trace-log-correlation.js +1 -2
- package/lib/network-smtp-policy.js +4 -4
- package/lib/object-store/sigv4-bucket-ops.js +11 -2
- package/lib/observability-tracer.js +1 -1
- package/lib/observability.js +39 -1
- package/lib/problem-details.js +56 -11
- package/lib/pubsub-cluster.js +16 -3
- package/lib/queue-sqs.js +20 -2
- package/lib/redis-client.js +32 -4
- package/lib/safe-redirect.js +16 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mail-auth.js
CHANGED
|
@@ -50,6 +50,7 @@ var validateOpts = require("./validate-opts");
|
|
|
50
50
|
var bCrypto = require("./crypto");
|
|
51
51
|
var C = require("./constants");
|
|
52
52
|
var dkim = require("./mail-dkim");
|
|
53
|
+
var mimeParse = require("./mime-parse");
|
|
53
54
|
var safeXml = require("./parsers/safe-xml");
|
|
54
55
|
var ipUtils = require("./ip-utils");
|
|
55
56
|
var publicSuffix = require("./public-suffix");
|
|
@@ -2615,6 +2616,337 @@ async function iprevVerify(ip) {
|
|
|
2615
2616
|
};
|
|
2616
2617
|
}
|
|
2617
2618
|
|
|
2619
|
+
// ---- DMARC forensic (RUF) failure-report parser (RFC 6591 + RFC 7489 §7.3) ----
|
|
2620
|
+
//
|
|
2621
|
+
// A domain publishing a DMARC `ruf=` policy receives per-message
|
|
2622
|
+
// failure reports when an authentication check fails. RFC 7489 §7.3
|
|
2623
|
+
// specifies the Authentication Failure Reporting Format (AFRF) of
|
|
2624
|
+
// RFC 6591 for these: a multipart/report (report-type=feedback-report)
|
|
2625
|
+
// carrying a `message/feedback-report` part whose header block adds the
|
|
2626
|
+
// DMARC-specific fields (Auth-Failure, Delivery-Result, Identity-
|
|
2627
|
+
// Alignment, DKIM-*/SPF-* result fields) on top of the RFC 5965 base
|
|
2628
|
+
// fields, plus a third part (message/rfc822 or text/rfc822-headers)
|
|
2629
|
+
// with the reported message (in full or headers-only).
|
|
2630
|
+
//
|
|
2631
|
+
// Composes the shared lib/mime-parse.js substrate (the same MIME walker
|
|
2632
|
+
// the RFC 5965 ARF ingest in lib/mail-arf.js uses) for the
|
|
2633
|
+
// multipart/report bisection + message/feedback-report extraction, then
|
|
2634
|
+
// shapes the full RFC 6591 §3.1 forensic field set (which the abuse-
|
|
2635
|
+
// report profile does not model) plus the reported message's headers.
|
|
2636
|
+
//
|
|
2637
|
+
// var rv = b.mail.dmarc.parseForensicReport(rawMessageBytes);
|
|
2638
|
+
// if (!rv.ok) { /* rv.error.code / rv.error.message */ }
|
|
2639
|
+
// else { /* rv.report.feedbackType / .authFailure / … */ }
|
|
2640
|
+
//
|
|
2641
|
+
// Validation tier: DEFENSIVE READER. The input is hostile-by-default
|
|
2642
|
+
// (a per-message failure report arrives at an operator endpoint from an
|
|
2643
|
+
// arbitrary reporting peer). The parser RETURNS a typed error object on
|
|
2644
|
+
// any malformed / over-cap / wrong-shape input — it does NOT throw in
|
|
2645
|
+
// the hot path, so a crafted report can't crash the request that
|
|
2646
|
+
// ingested it. Bytes + part-count + reported-header counts are bounded
|
|
2647
|
+
// (CWE-400 resource-exhaustion class) like the sibling aggregate-report
|
|
2648
|
+
// parser.
|
|
2649
|
+
//
|
|
2650
|
+
// { ok: true, report: { … } }
|
|
2651
|
+
// { ok: false, error: { code: "<slug>", message: "<reason>" } }
|
|
2652
|
+
|
|
2653
|
+
// RFC 6591 §3.2 — the report is small in practice; cap at 8 MiB to match
|
|
2654
|
+
// the sibling DMARC aggregate-report ceiling so operators have one
|
|
2655
|
+
// mental model for "what fits".
|
|
2656
|
+
var DMARC_RUF_MAX_REPORT_BYTES = C.BYTES.mib(8);
|
|
2657
|
+
|
|
2658
|
+
// RFC 2046 §5.1 — a multipart/report failure report has a handful of
|
|
2659
|
+
// parts (text/plain + message/feedback-report + the reported message);
|
|
2660
|
+
// bound the part count so a hostile report with thousands of empty
|
|
2661
|
+
// boundary delimiters can't force unbounded walk work.
|
|
2662
|
+
var DMARC_RUF_MAX_PARTS = 64; // resource-exhaustion bound (CWE-400)
|
|
2663
|
+
|
|
2664
|
+
// RFC 6591 §3.1 — required forensic fields. Feedback-Type and Auth-
|
|
2665
|
+
// Failure are the two that make an auth-failure report a DMARC forensic
|
|
2666
|
+
// report (RFC 7489 §7.3). User-Agent / Version are advisory in practice.
|
|
2667
|
+
var DMARC_RUF_REQUIRED_FIELDS = ["feedback-type", "auth-failure"];
|
|
2668
|
+
|
|
2669
|
+
// RFC 6591 §3.1 — Auth-Failure registry values. Unknown values pass
|
|
2670
|
+
// through (the IANA "Authentication Failure" registry grows); this set
|
|
2671
|
+
// documents the launch vocabulary so operators can route on it.
|
|
2672
|
+
var DMARC_RUF_AUTH_FAILURE_TYPES = Object.freeze({
|
|
2673
|
+
adsp: 1, // RFC 6591 §3.1 (historic ADSP)
|
|
2674
|
+
"bodyhash": 1, // DKIM body-hash mismatch
|
|
2675
|
+
dkim: 1, // DKIM signature failure
|
|
2676
|
+
dmarc: 1, // RFC 7489 §7.3 — DMARC evaluation failure
|
|
2677
|
+
revoked: 1, // signing key revoked
|
|
2678
|
+
signature: 1, // DKIM signature syntactically invalid
|
|
2679
|
+
spf: 1, // SPF check failure
|
|
2680
|
+
});
|
|
2681
|
+
void DMARC_RUF_AUTH_FAILURE_TYPES;
|
|
2682
|
+
|
|
2683
|
+
// RFC 6591 §3.2 — the reported message's header section can be large but
|
|
2684
|
+
// is bounded; cap the number of reported headers we normalize so a
|
|
2685
|
+
// crafted report can't force unbounded work. The full reported message
|
|
2686
|
+
// text is still surfaced verbatim under `reportedMessage` (bounded by
|
|
2687
|
+
// the overall byte cap), but the parsed `reportedHeaders` list is
|
|
2688
|
+
// header-count-capped.
|
|
2689
|
+
var DMARC_RUF_MAX_REPORTED_HEADERS = 256; // resource-exhaustion bound (CWE-400)
|
|
2690
|
+
|
|
2691
|
+
function _rufError(code, message) {
|
|
2692
|
+
return { ok: false, error: { code: code, message: message } };
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
// Parse the reported message's headers (RFC 6591 §3.2 — the third part
|
|
2696
|
+
// of the report carries the message that failed authentication, in full
|
|
2697
|
+
// or headers-only). Returns an own-keys-only map (null-prototype) of
|
|
2698
|
+
// header-name → value (last-wins for duplicate single-valued lookups;
|
|
2699
|
+
// the full ordered list is also returned) so a header named
|
|
2700
|
+
// `__proto__` / `constructor` in a hostile report can't pollute the
|
|
2701
|
+
// prototype chain (prototype-pollution class).
|
|
2702
|
+
function _parseReportedHeaders(reportedMessage) {
|
|
2703
|
+
var ordered = [];
|
|
2704
|
+
var map = Object.create(null);
|
|
2705
|
+
if (typeof reportedMessage !== "string" || reportedMessage.length === 0) {
|
|
2706
|
+
return { headers: ordered, map: map, truncated: false };
|
|
2707
|
+
}
|
|
2708
|
+
var split;
|
|
2709
|
+
try { split = mimeParse.splitHeadersAndBody(reportedMessage); }
|
|
2710
|
+
catch (_e) { return { headers: ordered, map: map, truncated: false }; }
|
|
2711
|
+
var hdrs = Array.isArray(split.headers) ? split.headers : [];
|
|
2712
|
+
var truncated = false;
|
|
2713
|
+
for (var i = 0; i < hdrs.length; i += 1) {
|
|
2714
|
+
if (ordered.length >= DMARC_RUF_MAX_REPORTED_HEADERS) { truncated = true; break; }
|
|
2715
|
+
var h = hdrs[i];
|
|
2716
|
+
if (!h || typeof h.name !== "string") continue;
|
|
2717
|
+
var name = h.name;
|
|
2718
|
+
var value = typeof h.value === "string" ? h.value : "";
|
|
2719
|
+
ordered.push({ name: name, value: value });
|
|
2720
|
+
// Own-key assignment on a null-prototype object: a reported header
|
|
2721
|
+
// named __proto__ / constructor / prototype is stored as data, not
|
|
2722
|
+
// walked up the chain.
|
|
2723
|
+
map[name.toLowerCase()] = value;
|
|
2724
|
+
}
|
|
2725
|
+
return { headers: ordered, map: map, truncated: truncated };
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
// Reassemble a MIME part's headers + body so a reported message that
|
|
2729
|
+
// ships as text/rfc822-headers (no separate body) still round-trips its
|
|
2730
|
+
// header bytes (RFC 6591 §3.2 permits headers-only).
|
|
2731
|
+
function _reassembleRufPart(part) {
|
|
2732
|
+
var hdrs = "";
|
|
2733
|
+
var ph = Array.isArray(part.headers) ? part.headers : [];
|
|
2734
|
+
for (var i = 0; i < ph.length; i += 1) {
|
|
2735
|
+
hdrs += ph[i].name + ": " + ph[i].value + "\r\n";
|
|
2736
|
+
}
|
|
2737
|
+
return hdrs + "\r\n" + (part.body || "");
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
function dmarcParseForensicReport(input, opts) {
|
|
2741
|
+
opts = opts || {};
|
|
2742
|
+
|
|
2743
|
+
// ---- Coerce + byte-cap (defensive: typed error, never throw) ----
|
|
2744
|
+
var asString;
|
|
2745
|
+
if (typeof input === "string") asString = input;
|
|
2746
|
+
else if (Buffer.isBuffer(input)) asString = input.toString("utf8");
|
|
2747
|
+
else {
|
|
2748
|
+
return _rufError("mail-auth/dmarc-ruf-bad-input",
|
|
2749
|
+
"dmarc.parseForensicReport: input must be a string or Buffer");
|
|
2750
|
+
}
|
|
2751
|
+
var maxBytes = (typeof opts.maxBytes === "number" && isFinite(opts.maxBytes) && opts.maxBytes > 0)
|
|
2752
|
+
? opts.maxBytes
|
|
2753
|
+
: DMARC_RUF_MAX_REPORT_BYTES;
|
|
2754
|
+
if (asString.length > maxBytes) {
|
|
2755
|
+
return _rufError("mail-auth/dmarc-ruf-too-large",
|
|
2756
|
+
"dmarc.parseForensicReport: report exceeds " + maxBytes + " bytes (got " + asString.length + ")");
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
// ---- Bisect top-level headers / body; require multipart/report ----
|
|
2760
|
+
var top;
|
|
2761
|
+
try { top = mimeParse.splitHeadersAndBody(asString); }
|
|
2762
|
+
catch (e) {
|
|
2763
|
+
return _rufError("mail-auth/dmarc-ruf-bad-report",
|
|
2764
|
+
"dmarc.parseForensicReport: header/body split failed: " + ((e && e.message) || String(e)));
|
|
2765
|
+
}
|
|
2766
|
+
var ct = mimeParse.parseContentType(mimeParse.findHeader(top.headers, "Content-Type") || "");
|
|
2767
|
+
if (ct.type !== "multipart/report") {
|
|
2768
|
+
return _rufError("mail-auth/dmarc-ruf-bad-report",
|
|
2769
|
+
"dmarc.parseForensicReport: top-level Content-Type must be multipart/report (got '" + ct.type + "')");
|
|
2770
|
+
}
|
|
2771
|
+
// RFC 6591 §2 / RFC 5965 §2 — report-type=feedback-report. Tolerate an
|
|
2772
|
+
// omitted report-type (shipping reporters sometimes drop it); refuse a
|
|
2773
|
+
// mismatched value.
|
|
2774
|
+
if (ct.params["report-type"] && ct.params["report-type"].toLowerCase() !== "feedback-report") {
|
|
2775
|
+
return _rufError("mail-auth/dmarc-ruf-bad-report",
|
|
2776
|
+
"dmarc.parseForensicReport: report-type must be feedback-report (got '" +
|
|
2777
|
+
ct.params["report-type"] + "')");
|
|
2778
|
+
}
|
|
2779
|
+
if (!ct.params.boundary) {
|
|
2780
|
+
return _rufError("mail-auth/dmarc-ruf-bad-report",
|
|
2781
|
+
"dmarc.parseForensicReport: multipart/report Content-Type lacks boundary parameter");
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// ---- Walk the parts; find message/feedback-report + reported msg ----
|
|
2785
|
+
var parts = mimeParse.splitMimeParts(top.body, ct.params.boundary);
|
|
2786
|
+
if (parts.length === 0) {
|
|
2787
|
+
return _rufError("mail-auth/dmarc-ruf-bad-report",
|
|
2788
|
+
"dmarc.parseForensicReport: multipart/report body contains no parts");
|
|
2789
|
+
}
|
|
2790
|
+
if (parts.length > DMARC_RUF_MAX_PARTS) {
|
|
2791
|
+
return _rufError("mail-auth/dmarc-ruf-too-many-parts",
|
|
2792
|
+
"dmarc.parseForensicReport: report has " + parts.length + " parts (cap " +
|
|
2793
|
+
DMARC_RUF_MAX_PARTS + ")");
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
var feedbackPart = null;
|
|
2797
|
+
var reportedPart = null;
|
|
2798
|
+
for (var pi = 0; pi < parts.length; pi += 1) {
|
|
2799
|
+
var split;
|
|
2800
|
+
try { split = mimeParse.splitHeadersAndBody(parts[pi]); }
|
|
2801
|
+
catch (_e) { continue; }
|
|
2802
|
+
var partCt = mimeParse.parseContentType(
|
|
2803
|
+
mimeParse.findHeader(split.headers, "Content-Type") || "");
|
|
2804
|
+
if (partCt.type === "message/feedback-report" && !feedbackPart) {
|
|
2805
|
+
feedbackPart = split;
|
|
2806
|
+
} else if ((partCt.type === "message/rfc822" ||
|
|
2807
|
+
partCt.type === "text/rfc822-headers") && !reportedPart) {
|
|
2808
|
+
reportedPart = split;
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
if (!feedbackPart) {
|
|
2812
|
+
return _rufError("mail-auth/dmarc-ruf-no-feedback-report",
|
|
2813
|
+
"dmarc.parseForensicReport: missing message/feedback-report subpart (RFC 6591 §3)");
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
// ---- Parse the feedback-report header block (RFC 6591 §3.1) ----
|
|
2817
|
+
// Field names are stored own-key on a null-prototype map so a hostile
|
|
2818
|
+
// field named __proto__ / constructor can't pollute the prototype.
|
|
2819
|
+
var fields;
|
|
2820
|
+
try { fields = mimeParse.parseHeaderBlock(feedbackPart.body); }
|
|
2821
|
+
catch (e) {
|
|
2822
|
+
return _rufError("mail-auth/dmarc-ruf-bad-report",
|
|
2823
|
+
"dmarc.parseForensicReport: feedback-report field parse failed: " + ((e && e.message) || String(e)));
|
|
2824
|
+
}
|
|
2825
|
+
var fieldMap = Object.create(null);
|
|
2826
|
+
var rcptToList = [];
|
|
2827
|
+
for (var fi = 0; fi < fields.length; fi += 1) {
|
|
2828
|
+
var f = fields[fi];
|
|
2829
|
+
if (!f || typeof f.name !== "string") continue;
|
|
2830
|
+
var lc = f.name.toLowerCase();
|
|
2831
|
+
var val = typeof f.value === "string" ? f.value : "";
|
|
2832
|
+
fieldMap[lc] = val;
|
|
2833
|
+
if (lc === "original-rcpt-to") rcptToList.push(val);
|
|
2834
|
+
}
|
|
2835
|
+
function _field(name) {
|
|
2836
|
+
return Object.prototype.hasOwnProperty.call(fieldMap, name) ? fieldMap[name] : null;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// ---- Required fields (RFC 6591 §3.1 / RFC 7489 §7.3) ----
|
|
2840
|
+
for (var ri = 0; ri < DMARC_RUF_REQUIRED_FIELDS.length; ri += 1) {
|
|
2841
|
+
var req = DMARC_RUF_REQUIRED_FIELDS[ri];
|
|
2842
|
+
var rv = _field(req);
|
|
2843
|
+
if (typeof rv !== "string" || rv.length === 0) {
|
|
2844
|
+
if (req === "auth-failure") {
|
|
2845
|
+
return _rufError("mail-auth/dmarc-ruf-missing-auth-failure",
|
|
2846
|
+
"dmarc.parseForensicReport: required field 'Auth-Failure' is missing (RFC 6591 §3.1)");
|
|
2847
|
+
}
|
|
2848
|
+
return _rufError("mail-auth/dmarc-ruf-missing-field",
|
|
2849
|
+
"dmarc.parseForensicReport: required field '" + req + "' is missing (RFC 6591 §3.1)");
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// RFC 7489 §7.3 — a DMARC forensic report carries Feedback-Type:
|
|
2854
|
+
// auth-failure (the AFRF profile of RFC 6591). A report whose Feedback-
|
|
2855
|
+
// Type is another ARF class (e.g. plain "abuse") is a valid feedback
|
|
2856
|
+
// report but NOT a DMARC forensic report; surface the mismatch rather
|
|
2857
|
+
// than mislabeling it. Field values are case-insensitive tokens.
|
|
2858
|
+
var feedbackType = String(_field("feedback-type")).toLowerCase();
|
|
2859
|
+
if (feedbackType !== "auth-failure") {
|
|
2860
|
+
return _rufError("mail-auth/dmarc-ruf-not-auth-failure",
|
|
2861
|
+
"dmarc.parseForensicReport: Feedback-Type must be 'auth-failure' for a " +
|
|
2862
|
+
"DMARC forensic report (RFC 7489 §7.3 / RFC 6591), got " +
|
|
2863
|
+
JSON.stringify(_field("feedback-type")));
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
// ---- RFC 6591 §3.2 reported message ----
|
|
2867
|
+
var reportedMessage = null;
|
|
2868
|
+
if (reportedPart) {
|
|
2869
|
+
reportedMessage = (reportedPart.body && reportedPart.body.length > 0)
|
|
2870
|
+
? reportedPart.body
|
|
2871
|
+
: _reassembleRufPart(reportedPart);
|
|
2872
|
+
}
|
|
2873
|
+
var reported = _parseReportedHeaders(reportedMessage);
|
|
2874
|
+
|
|
2875
|
+
// ---- Normalize Arrival-Date / Incidents (RFC 5965 §3.1) ----
|
|
2876
|
+
var arrivalRaw = _field("arrival-date") || _field("received-date") || null;
|
|
2877
|
+
var arrivalIso = null;
|
|
2878
|
+
if (arrivalRaw) {
|
|
2879
|
+
var d = new Date(arrivalRaw);
|
|
2880
|
+
if (!isNaN(d.getTime())) arrivalIso = d.toISOString();
|
|
2881
|
+
}
|
|
2882
|
+
var incidentsRaw = _field("incidents");
|
|
2883
|
+
var incidents = null;
|
|
2884
|
+
if (typeof incidentsRaw === "string") {
|
|
2885
|
+
var inc = parseInt(incidentsRaw, 10);
|
|
2886
|
+
if (isFinite(inc) && inc >= 0) incidents = inc;
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// Surface unmodeled fields under extraFields for operator visibility
|
|
2890
|
+
// (vendor X-* tags). Own-key copy off the null-prototype fieldMap onto
|
|
2891
|
+
// a null-prototype target so a field named __proto__ / constructor in a
|
|
2892
|
+
// hostile report is stored as data, not as a prototype mutation.
|
|
2893
|
+
var KNOWN = Object.create(null);
|
|
2894
|
+
["feedback-type", "user-agent", "version", "auth-failure",
|
|
2895
|
+
"delivery-result", "identity-alignment", "dkim-domain",
|
|
2896
|
+
"dkim-identity", "dkim-selector", "dkim-canonicalized-header",
|
|
2897
|
+
"dkim-canonicalized-body", "spf-dns", "original-mail-from",
|
|
2898
|
+
"original-rcpt-to", "arrival-date", "received-date",
|
|
2899
|
+
"reported-domain", "source-ip", "authentication-results",
|
|
2900
|
+
"reported-uri", "incidents", "original-envelope-id"
|
|
2901
|
+
].forEach(function (k) { KNOWN[k] = 1; });
|
|
2902
|
+
var extraFields = Object.create(null);
|
|
2903
|
+
Object.keys(fieldMap).forEach(function (k) {
|
|
2904
|
+
if (!Object.prototype.hasOwnProperty.call(KNOWN, k)) extraFields[k] = fieldMap[k];
|
|
2905
|
+
});
|
|
2906
|
+
|
|
2907
|
+
var report = {
|
|
2908
|
+
// ---- RFC 5965 base fields ----
|
|
2909
|
+
feedbackType: _field("feedback-type"),
|
|
2910
|
+
userAgent: _field("user-agent"),
|
|
2911
|
+
version: _field("version") || "1",
|
|
2912
|
+
arrivalDate: arrivalIso || arrivalRaw,
|
|
2913
|
+
reportedDomain: _field("reported-domain"),
|
|
2914
|
+
sourceIp: _field("source-ip"),
|
|
2915
|
+
originalFrom: _field("original-mail-from"),
|
|
2916
|
+
originalRcptTo: rcptToList,
|
|
2917
|
+
originalEnvelopeId: _field("original-envelope-id"),
|
|
2918
|
+
authenticationResults: _field("authentication-results"),
|
|
2919
|
+
incidents: incidents,
|
|
2920
|
+
reportedUri: _field("reported-uri"),
|
|
2921
|
+
|
|
2922
|
+
// ---- RFC 6591 §3.1 / RFC 7489 §7.3 forensic-specific fields ----
|
|
2923
|
+
authFailure: _field("auth-failure"), // RFC 6591 §3.1 — "dkim" | "spf" | "dmarc" | "bodyhash" | …
|
|
2924
|
+
deliveryResult: _field("delivery-result"), // RFC 6591 §3.1 — "delivered" | "spam" | "policy" | "reject" | "other"
|
|
2925
|
+
identityAlignment: _field("identity-alignment"), // RFC 7489 §7.3 — "none" | "spf" | "dkim" | "dkim spf"
|
|
2926
|
+
dkim: {
|
|
2927
|
+
domain: _field("dkim-domain"), // RFC 6591 §3.1
|
|
2928
|
+
identity: _field("dkim-identity"),
|
|
2929
|
+
selector: _field("dkim-selector"),
|
|
2930
|
+
canonicalizedHeader: _field("dkim-canonicalized-header"),
|
|
2931
|
+
canonicalizedBody: _field("dkim-canonicalized-body"),
|
|
2932
|
+
},
|
|
2933
|
+
spf: {
|
|
2934
|
+
dns: _field("spf-dns"), // RFC 6591 §3.1 — the SPF DNS record at evaluation time
|
|
2935
|
+
},
|
|
2936
|
+
|
|
2937
|
+
// ---- RFC 6591 §3.2 reported message ----
|
|
2938
|
+
reportedMessage: reportedMessage,
|
|
2939
|
+
reportedHeaders: reported.headers, // [ { name, value }, … ] — order preserved
|
|
2940
|
+
reportedHeaderMap: reported.map, // null-prototype lower-cased-name → value
|
|
2941
|
+
reportedHeadersTruncated: reported.truncated, // true when the §3.2 header cap clipped the list
|
|
2942
|
+
|
|
2943
|
+
// ---- operator-visible passthrough for unmodeled fields ----
|
|
2944
|
+
extraFields: extraFields,
|
|
2945
|
+
};
|
|
2946
|
+
|
|
2947
|
+
return { ok: true, report: report };
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2618
2950
|
module.exports = {
|
|
2619
2951
|
spf: Object.freeze({
|
|
2620
2952
|
verify: spfVerify,
|
|
@@ -2625,6 +2957,7 @@ module.exports = {
|
|
|
2625
2957
|
parseRecord: _parseDmarcRecord,
|
|
2626
2958
|
parseAggregateReport: dmarcParseAggregateReport,
|
|
2627
2959
|
buildAggregateReport: dmarcBuildAggregateReport,
|
|
2960
|
+
parseForensicReport: dmarcParseForensicReport,
|
|
2628
2961
|
}),
|
|
2629
2962
|
arc: Object.freeze({
|
|
2630
2963
|
verify: arcVerify,
|
package/lib/mail-deploy.js
CHANGED
|
@@ -919,7 +919,7 @@ function tlsRptIngestHttp(opts) {
|
|
|
919
919
|
opts = opts || {};
|
|
920
920
|
validateOpts(opts, ["authenticate", "trustedReporters", "maxCompressedBytes",
|
|
921
921
|
"maxDecompressedBytes", "maxRatio", "onAccept", "onRefuse",
|
|
922
|
-
"audit"
|
|
922
|
+
"audit"],
|
|
923
923
|
"mail.deploy.tlsRptIngestHttp");
|
|
924
924
|
validateOpts.optionalFunction(opts.authenticate, "tlsRptIngestHttp: opts.authenticate",
|
|
925
925
|
MailDeployError, "mail-tlsrpt/bad-opts");
|
package/lib/mail-send-deliver.js
CHANGED
|
@@ -468,16 +468,25 @@ function create(opts) {
|
|
|
468
468
|
|
|
469
469
|
var retryOpts = opts.retry || {};
|
|
470
470
|
validateOpts(retryOpts, ["maxAttempts", "backoffMs"], "mail.send.deliver.create.retry");
|
|
471
|
-
|
|
472
|
-
|
|
471
|
+
// Config-time entry-point opts: a typo (maxAttempts:"5", mxLookupMs:-1)
|
|
472
|
+
// must fail at create(), not silently fall back to the default. Absent
|
|
473
|
+
// keeps the default; present-but-bad throws. Matches opts.port above.
|
|
474
|
+
validateOpts.optionalPositiveInt(retryOpts.maxAttempts,
|
|
475
|
+
"mail.send.deliver.create.retry.maxAttempts", DeliverError, "deliver/bad-retry-maxAttempts");
|
|
476
|
+
var maxAttempts = retryOpts.maxAttempts !== undefined
|
|
477
|
+
? retryOpts.maxAttempts : DEFAULT_RETRY_BACKOFF_MS.length;
|
|
473
478
|
var backoffMs = Array.isArray(retryOpts.backoffMs) && retryOpts.backoffMs.length > 0
|
|
474
479
|
? retryOpts.backoffMs.slice() : DEFAULT_RETRY_BACKOFF_MS.slice();
|
|
475
480
|
|
|
476
481
|
var timeouts = opts.timeouts || {};
|
|
477
482
|
validateOpts(timeouts, ["mxLookupMs", "perHostMs"], "mail.send.deliver.create.timeouts");
|
|
478
|
-
|
|
483
|
+
validateOpts.optionalPositiveInt(timeouts.mxLookupMs,
|
|
484
|
+
"mail.send.deliver.create.timeouts.mxLookupMs", DeliverError, "deliver/bad-timeout-mxLookupMs");
|
|
485
|
+
validateOpts.optionalPositiveInt(timeouts.perHostMs,
|
|
486
|
+
"mail.send.deliver.create.timeouts.perHostMs", DeliverError, "deliver/bad-timeout-perHostMs");
|
|
487
|
+
var mxLookupTimeoutMs = timeouts.mxLookupMs !== undefined
|
|
479
488
|
? timeouts.mxLookupMs : DEFAULT_MX_LOOKUP_TIMEOUT_MS;
|
|
480
|
-
var perHostTimeoutMs =
|
|
489
|
+
var perHostTimeoutMs = timeouts.perHostMs !== undefined
|
|
481
490
|
? timeouts.perHostMs : DEFAULT_PER_HOST_TIMEOUT_MS;
|
|
482
491
|
|
|
483
492
|
var dsnOpts = opts.dsn || null;
|