@blamejs/core 0.8.52 → 0.8.58
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 +6 -0
- package/index.js +8 -0
- package/lib/audit.js +4 -0
- package/lib/auth/fido-mds3.js +624 -0
- package/lib/auth/passkey.js +214 -2
- package/lib/auth-bot-challenge.js +1 -1
- package/lib/credential-hash.js +2 -2
- package/lib/db-collection.js +290 -0
- package/lib/db-query.js +245 -0
- package/lib/db.js +173 -67
- package/lib/framework-error.js +55 -0
- package/lib/guard-cidr.js +2 -1
- package/lib/guard-jwt.js +2 -2
- package/lib/guard-oauth.js +2 -2
- package/lib/http-client-cache.js +916 -0
- package/lib/http-client.js +242 -0
- package/lib/mail-arf.js +343 -0
- package/lib/mail-auth.js +265 -40
- package/lib/mail-bimi.js +948 -33
- package/lib/mail-bounce.js +386 -4
- package/lib/mail-mdn.js +424 -0
- package/lib/mail-unsubscribe.js +265 -25
- package/lib/mail.js +403 -21
- package/lib/middleware/bearer-auth.js +1 -1
- package/lib/middleware/clear-site-data.js +122 -0
- package/lib/middleware/dpop.js +1 -1
- package/lib/middleware/index.js +9 -0
- package/lib/middleware/nel.js +214 -0
- package/lib/middleware/security-headers.js +56 -4
- package/lib/middleware/speculation-rules.js +323 -0
- package/lib/mime-parse.js +198 -0
- package/lib/mtls-ca.js +15 -5
- package/lib/network-dns.js +890 -27
- package/lib/network-tls.js +745 -0
- package/lib/object-store/sigv4.js +54 -0
- package/lib/public-suffix.js +414 -0
- package/lib/safe-buffer.js +7 -0
- package/lib/safe-json.js +1 -1
- package/lib/static.js +120 -0
- package/lib/storage.js +11 -0
- package/lib/vendor/MANIFEST.json +33 -0
- package/lib/vendor/bimi-trust-anchors.pem +33 -0
- package/lib/vendor/public-suffix-list.dat +16376 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/lib/mail-bounce.js
CHANGED
|
@@ -27,22 +27,28 @@
|
|
|
27
27
|
* `onBounce(event)` so the suppression list can be updated before
|
|
28
28
|
* the 200 response goes back.
|
|
29
29
|
*
|
|
30
|
-
* Generic RFC 3464 DSN is
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* Generic RFC 3464 / RFC 3461 / RFC 6533 DSN is wired in as
|
|
31
|
+
* `b.mailBounce.dsn.parse` / `b.mailBounce.dsn.build` — a parser for
|
|
32
|
+
* raw multipart/report message/delivery-status MIME bounces (the
|
|
33
|
+
* shape any spec-conforming MTA returns) and a generator that builds
|
|
34
|
+
* the same shape for operators that need to issue bounces from their
|
|
35
|
+
* own MTA. Operators with bespoke vendor inflow can still supply
|
|
36
|
+
* `{ parser }` to plug a custom normalizer onto `parse` / `handler`.
|
|
34
37
|
*
|
|
35
38
|
* @card
|
|
36
39
|
* Inbound mail bounce-handler — parse the vendor's webhook DSN / complaint / delivery payload, normalize it into one event shape, classify hard vs soft bounces, and feed an operator-supplied suppression-list hook.
|
|
37
40
|
*/
|
|
38
41
|
|
|
42
|
+
var crypto = require("./crypto");
|
|
39
43
|
var lazyRequire = require("./lazy-require");
|
|
44
|
+
var mimeParse = require("./mime-parse");
|
|
40
45
|
var numericBounds = require("./numeric-bounds");
|
|
41
46
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
42
47
|
var safeBuffer = require("./safe-buffer");
|
|
43
48
|
var safeJson = require("./safe-json");
|
|
44
49
|
var C = require("./constants");
|
|
45
50
|
var requestHelpers = require("./request-helpers");
|
|
51
|
+
var validateOpts = require("./validate-opts");
|
|
46
52
|
var { defineClass } = require("./framework-error");
|
|
47
53
|
|
|
48
54
|
var HTTP = requestHelpers.HTTP_STATUS;
|
|
@@ -558,6 +564,372 @@ function handler(opts) {
|
|
|
558
564
|
};
|
|
559
565
|
}
|
|
560
566
|
|
|
567
|
+
// ---- Generic RFC 3464 / RFC 3461 / RFC 6533 DSN ----
|
|
568
|
+
//
|
|
569
|
+
// A delivery status notification is a multipart/report MIME body:
|
|
570
|
+
//
|
|
571
|
+
// Content-Type: multipart/report;
|
|
572
|
+
// report-type=delivery-status;
|
|
573
|
+
// boundary="boundary-string"
|
|
574
|
+
//
|
|
575
|
+
// --boundary-string
|
|
576
|
+
// Content-Type: text/plain; charset=us-ascii
|
|
577
|
+
//
|
|
578
|
+
// <human-readable description of the failure>
|
|
579
|
+
//
|
|
580
|
+
// --boundary-string
|
|
581
|
+
// Content-Type: message/delivery-status
|
|
582
|
+
//
|
|
583
|
+
// Reporting-MTA: dns; mta.example.com
|
|
584
|
+
// Arrival-Date: Mon, 28 Apr 2026 12:00:00 +0000
|
|
585
|
+
//
|
|
586
|
+
// Original-Recipient: rfc822;user@example.com
|
|
587
|
+
// Final-Recipient: rfc822;user@example.com
|
|
588
|
+
// Action: failed
|
|
589
|
+
// Status: 5.1.1
|
|
590
|
+
// Remote-MTA: dns; mx.example.com
|
|
591
|
+
// Diagnostic-Code: smtp; 550 5.1.1 No such user
|
|
592
|
+
//
|
|
593
|
+
// --boundary-string
|
|
594
|
+
// Content-Type: message/rfc822
|
|
595
|
+
//
|
|
596
|
+
// <original message headers + body>
|
|
597
|
+
//
|
|
598
|
+
// --boundary-string--
|
|
599
|
+
//
|
|
600
|
+
// RFC 3461 adds the SMTP NOTIFY=SUCCESS,FAILURE,DELAY and RET=FULL,HDRS
|
|
601
|
+
// extensions — they're carried inside the SMTP envelope, but the DSN
|
|
602
|
+
// the framework generates / parses ends up reflecting that operator
|
|
603
|
+
// choice via the `originalMessage` decision (full body vs headers
|
|
604
|
+
// only) and the per-recipient `Action` field (delivered / failed /
|
|
605
|
+
// delayed / relayed / expanded).
|
|
606
|
+
//
|
|
607
|
+
// RFC 6533 (SMTPUTF8 / EAI) extends the address-type tag from rfc822
|
|
608
|
+
// to utf-8 so internationalized mailbox names ride through. The
|
|
609
|
+
// parser accepts both; the generator picks utf-8 when the recipient
|
|
610
|
+
// contains non-ASCII bytes.
|
|
611
|
+
|
|
612
|
+
// Per-message DSN fields (RFC 3464 §2.2). Listed for validation +
|
|
613
|
+
// canonical-case re-emission.
|
|
614
|
+
var DSN_PER_MESSAGE_FIELDS = {
|
|
615
|
+
"original-envelope-id": "Original-Envelope-Id",
|
|
616
|
+
"reporting-mta": "Reporting-MTA",
|
|
617
|
+
"dsn-gateway": "DSN-Gateway",
|
|
618
|
+
"received-from-mta": "Received-From-MTA",
|
|
619
|
+
"arrival-date": "Arrival-Date",
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// Per-recipient DSN fields (RFC 3464 §2.3).
|
|
623
|
+
var DSN_PER_RECIPIENT_FIELDS = {
|
|
624
|
+
"original-recipient": "Original-Recipient",
|
|
625
|
+
"final-recipient": "Final-Recipient",
|
|
626
|
+
"action": "Action",
|
|
627
|
+
"status": "Status",
|
|
628
|
+
"remote-mta": "Remote-MTA",
|
|
629
|
+
"diagnostic-code": "Diagnostic-Code",
|
|
630
|
+
"last-attempt-date": "Last-Attempt-Date",
|
|
631
|
+
"final-log-id": "Final-Log-ID",
|
|
632
|
+
"will-retry-until": "Will-Retry-Until",
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
// Action token allowlist — RFC 3464 §2.3.3.
|
|
636
|
+
var DSN_ACTIONS = {
|
|
637
|
+
"failed": true,
|
|
638
|
+
"delayed": true,
|
|
639
|
+
"delivered": true,
|
|
640
|
+
"relayed": true,
|
|
641
|
+
"expanded": true,
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
// Body cap for the DSN parser. The parser walks the raw bytes once;
|
|
645
|
+
// any payload above 1 MiB is pathological — no spec-conforming DSN
|
|
646
|
+
// approaches that, and uncapped parsing would let a hostile peer pin
|
|
647
|
+
// CPU on regex backtracking inside the header decoder.
|
|
648
|
+
var DSN_MAX_BYTES = C.BYTES.mib(1);
|
|
649
|
+
|
|
650
|
+
function _parseDeliveryStatusBody(body) {
|
|
651
|
+
// RFC 3464 §2.1 — message/delivery-status is per-message field group
|
|
652
|
+
// followed by ONE OR MORE per-recipient groups, each separated by an
|
|
653
|
+
// empty line.
|
|
654
|
+
var groups = body.split(/\r?\n\r?\n/).map(function (g) { return g.trim(); }).filter(Boolean);
|
|
655
|
+
if (groups.length === 0) return { perMessage: {}, perRecipients: [] };
|
|
656
|
+
var perMessage = {};
|
|
657
|
+
var msgHeaders = mimeParse.parseHeaderBlock(groups[0]);
|
|
658
|
+
for (var i = 0; i < msgHeaders.length; i += 1) {
|
|
659
|
+
perMessage[msgHeaders[i].name.toLowerCase()] = msgHeaders[i].value;
|
|
660
|
+
}
|
|
661
|
+
var perRecipients = [];
|
|
662
|
+
for (var g = 1; g < groups.length; g += 1) {
|
|
663
|
+
var headers = mimeParse.parseHeaderBlock(groups[g]);
|
|
664
|
+
if (headers.length === 0) continue;
|
|
665
|
+
var rec = {};
|
|
666
|
+
for (var k = 0; k < headers.length; k += 1) {
|
|
667
|
+
rec[headers[k].name.toLowerCase()] = headers[k].value;
|
|
668
|
+
}
|
|
669
|
+
perRecipients.push(rec);
|
|
670
|
+
}
|
|
671
|
+
return { perMessage: perMessage, perRecipients: perRecipients };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function _actionToSubType(action) {
|
|
675
|
+
// RFC 3464 §2.3.3 actions map onto the framework's bounce-shape
|
|
676
|
+
// vocabulary. failed -> hard, delayed -> soft. delivered / relayed /
|
|
677
|
+
// expanded come back as type=delivery; the parser swallows those
|
|
678
|
+
// before this lookup runs.
|
|
679
|
+
var a = (action || "").toLowerCase();
|
|
680
|
+
if (a === "failed") return "hard";
|
|
681
|
+
if (a === "delayed") return "soft";
|
|
682
|
+
return "unknown";
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function _parseDsn(rawMessage) {
|
|
686
|
+
if (typeof rawMessage !== "string" || rawMessage.length === 0) {
|
|
687
|
+
throw _err("bounce/dsn-parse-failed",
|
|
688
|
+
"mailBounce.dsn.parse: rawMessage must be a non-empty string");
|
|
689
|
+
}
|
|
690
|
+
// Hot-path body cap. Above this limit the parser stops trying to
|
|
691
|
+
// interpret the bytes — pathological inputs become a typed error
|
|
692
|
+
// rather than a regex-backtrack hang.
|
|
693
|
+
if (rawMessage.length > DSN_MAX_BYTES) {
|
|
694
|
+
throw _err("bounce/dsn-parse-failed",
|
|
695
|
+
"mailBounce.dsn.parse: message exceeds " + DSN_MAX_BYTES + " bytes");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
var top = mimeParse.splitHeadersAndBody(rawMessage);
|
|
699
|
+
var ctRaw = mimeParse.findHeader(top.headers, "Content-Type");
|
|
700
|
+
if (!ctRaw) {
|
|
701
|
+
throw _err("bounce/dsn-malformed",
|
|
702
|
+
"mailBounce.dsn.parse: missing top-level Content-Type");
|
|
703
|
+
}
|
|
704
|
+
var ct = mimeParse.parseContentType(ctRaw);
|
|
705
|
+
if (ct.type !== "multipart/report") {
|
|
706
|
+
throw _err("bounce/dsn-malformed",
|
|
707
|
+
"mailBounce.dsn.parse: top-level Content-Type must be multipart/report; got " + ct.type);
|
|
708
|
+
}
|
|
709
|
+
if (ct.params["report-type"] && ct.params["report-type"].toLowerCase() !== "delivery-status") {
|
|
710
|
+
throw _err("bounce/dsn-malformed",
|
|
711
|
+
"mailBounce.dsn.parse: report-type must be delivery-status; got " + ct.params["report-type"]);
|
|
712
|
+
}
|
|
713
|
+
var boundary = ct.params.boundary;
|
|
714
|
+
if (!boundary) {
|
|
715
|
+
throw _err("bounce/dsn-malformed",
|
|
716
|
+
"mailBounce.dsn.parse: multipart/report missing boundary parameter");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
var parts = mimeParse.splitMimeParts(top.body, boundary);
|
|
720
|
+
if (parts.length < 2) {
|
|
721
|
+
throw _err("bounce/dsn-malformed",
|
|
722
|
+
"mailBounce.dsn.parse: multipart/report needs at least 2 parts (text + delivery-status); got " + parts.length);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Find the message/delivery-status part.
|
|
726
|
+
var statusBody = null;
|
|
727
|
+
var humanText = null;
|
|
728
|
+
var originalMessage = null;
|
|
729
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
730
|
+
var partSplit = mimeParse.splitHeadersAndBody(parts[i].replace(/^\r?\n/, ""));
|
|
731
|
+
var partCtRaw = mimeParse.findHeader(partSplit.headers, "Content-Type") || "text/plain";
|
|
732
|
+
var partCt = mimeParse.parseContentType(partCtRaw);
|
|
733
|
+
if (partCt.type === "message/delivery-status") {
|
|
734
|
+
statusBody = partSplit.body;
|
|
735
|
+
} else if (partCt.type === "text/plain" && humanText === null) {
|
|
736
|
+
humanText = partSplit.body;
|
|
737
|
+
} else if (partCt.type === "message/rfc822" || partCt.type === "text/rfc822-headers") {
|
|
738
|
+
originalMessage = partSplit.body;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (statusBody === null) {
|
|
743
|
+
throw _err("bounce/dsn-malformed",
|
|
744
|
+
"mailBounce.dsn.parse: no message/delivery-status part found");
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
var status = _parseDeliveryStatusBody(statusBody);
|
|
748
|
+
if (status.perRecipients.length === 0) {
|
|
749
|
+
throw _err("bounce/dsn-malformed",
|
|
750
|
+
"mailBounce.dsn.parse: message/delivery-status has no per-recipient groups");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// First recipient drives the normalized event (matches the SES /
|
|
754
|
+
// postmark convention; multi-recipient DSNs are exposed via
|
|
755
|
+
// event.raw.allRecipients for operators that need fan-out).
|
|
756
|
+
var recip = status.perRecipients[0];
|
|
757
|
+
var action = (recip["action"] || "").toLowerCase();
|
|
758
|
+
var finalRecipient = mimeParse.stripAddressType(recip["final-recipient"]) ||
|
|
759
|
+
mimeParse.stripAddressType(recip["original-recipient"]);
|
|
760
|
+
if (!finalRecipient) {
|
|
761
|
+
throw _err("bounce/dsn-malformed",
|
|
762
|
+
"mailBounce.dsn.parse: per-recipient group missing Final-Recipient");
|
|
763
|
+
}
|
|
764
|
+
if (action && !DSN_ACTIONS[action]) {
|
|
765
|
+
throw _err("bounce/dsn-malformed",
|
|
766
|
+
"mailBounce.dsn.parse: Action token '" + action + "' is not RFC 3464 §2.3.3");
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
var type, subType;
|
|
770
|
+
if (action === "delivered" || action === "relayed" || action === "expanded") {
|
|
771
|
+
type = "delivery"; subType = null;
|
|
772
|
+
} else {
|
|
773
|
+
type = "bounce"; subType = _actionToSubType(action);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
var diagnosticCode = recip["diagnostic-code"] || null;
|
|
777
|
+
// RFC 3464 §2.3.6 — Diagnostic-Code is `diagnostic-type;
|
|
778
|
+
// diagnostic`. Most are `smtp; <reply>` — strip the type prefix to
|
|
779
|
+
// surface the human-readable reason in audit metadata.
|
|
780
|
+
var reason = diagnosticCode ? mimeParse.stripAddressType(diagnosticCode) : null;
|
|
781
|
+
if (!reason && humanText) {
|
|
782
|
+
// Fall back to the human-readable section when the spec'd
|
|
783
|
+
// Diagnostic-Code field is absent (legacy MTAs).
|
|
784
|
+
reason = humanText.trim().split(/\r?\n/).slice(0, 5).join(" ").slice(0, 500) || null;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
var arrivalDate = status.perMessage["arrival-date"];
|
|
788
|
+
var messageId = mimeParse.findHeader(top.headers, "Message-ID") || null;
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
vendor: "rfc3464",
|
|
792
|
+
type: type,
|
|
793
|
+
subType: subType,
|
|
794
|
+
recipient: finalRecipient,
|
|
795
|
+
messageId: messageId,
|
|
796
|
+
reason: reason,
|
|
797
|
+
timestamp: arrivalDate || new Date().toISOString(),
|
|
798
|
+
raw: {
|
|
799
|
+
perMessage: status.perMessage,
|
|
800
|
+
allRecipients: status.perRecipients,
|
|
801
|
+
humanText: humanText,
|
|
802
|
+
originalMessage: originalMessage,
|
|
803
|
+
// Status code (per RFC 3463 — class.subject.detail) is one of the
|
|
804
|
+
// most useful operator fields; surface it on raw so policy code
|
|
805
|
+
// can branch without re-parsing.
|
|
806
|
+
status: recip["status"] || null,
|
|
807
|
+
action: action || null,
|
|
808
|
+
diagnosticCode: diagnosticCode,
|
|
809
|
+
},
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function _foldFieldValue(name, value) {
|
|
814
|
+
// RFC 5322 §2.2.3 — long lines fold at WSP. Keep it simple: emit
|
|
815
|
+
// `Name: value` and let downstream MTAs handle further folding.
|
|
816
|
+
return name + ": " + value + "\r\n";
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function _generateBoundary() {
|
|
820
|
+
return "blamejs-dsn-" + crypto.generateToken(C.BYTES.bytes(12));
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function _buildDsn(opts) {
|
|
824
|
+
validateOpts.requireObject(opts, "mailBounce.dsn.build", MailBounceError, "bounce/dsn-malformed");
|
|
825
|
+
validateOpts.requireNonEmptyString(opts.finalRecipient,
|
|
826
|
+
"mailBounce.dsn.build: opts.finalRecipient", MailBounceError, "bounce/dsn-malformed");
|
|
827
|
+
var action = String(opts.action || "failed").toLowerCase();
|
|
828
|
+
if (!DSN_ACTIONS[action]) {
|
|
829
|
+
throw _err("bounce/dsn-malformed",
|
|
830
|
+
"mailBounce.dsn.build: opts.action must be one of " +
|
|
831
|
+
Object.keys(DSN_ACTIONS).join(" / ") + "; got '" + action + "'");
|
|
832
|
+
}
|
|
833
|
+
if (typeof opts.status !== "string" || !/^\d\.\d{1,3}\.\d{1,3}$/.test(opts.status)) {
|
|
834
|
+
throw _err("bounce/dsn-malformed",
|
|
835
|
+
"mailBounce.dsn.build: opts.status must match RFC 3463 class.subject.detail; got '" +
|
|
836
|
+
String(opts.status) + "'");
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
var reportingMta = opts.reportingMta || "dns; localhost";
|
|
840
|
+
var arrivalDate = opts.arrivalDate || new Date().toUTCString();
|
|
841
|
+
var originalMessage = opts.originalMessage || null;
|
|
842
|
+
var diagnosticCode = opts.diagnosticCode || null;
|
|
843
|
+
var remoteMta = opts.remoteMta || null;
|
|
844
|
+
var humanText = opts.humanText || (
|
|
845
|
+
"This is the mail system at " + reportingMta + ".\r\n\r\n" +
|
|
846
|
+
"Your message could not be delivered to:\r\n\r\n" +
|
|
847
|
+
" " + opts.finalRecipient + "\r\n\r\n" +
|
|
848
|
+
(diagnosticCode ? "The remote server reported: " + diagnosticCode + "\r\n" : ""));
|
|
849
|
+
|
|
850
|
+
var recipType = mimeParse.addressType(opts.finalRecipient);
|
|
851
|
+
var origRecipType = opts.originalRecipient ? mimeParse.addressType(opts.originalRecipient) : recipType;
|
|
852
|
+
|
|
853
|
+
var boundary = _generateBoundary();
|
|
854
|
+
var lines = [];
|
|
855
|
+
lines.push("MIME-Version: 1.0");
|
|
856
|
+
lines.push('Content-Type: multipart/report; report-type=delivery-status; boundary="' + boundary + '"');
|
|
857
|
+
if (opts.from) lines.push("From: " + opts.from);
|
|
858
|
+
if (opts.to) lines.push("To: " + opts.to);
|
|
859
|
+
if (opts.subject) lines.push("Subject: " + opts.subject);
|
|
860
|
+
if (opts.messageId) lines.push("Message-ID: " + opts.messageId);
|
|
861
|
+
lines.push("");
|
|
862
|
+
|
|
863
|
+
// Part 1 - human-readable description.
|
|
864
|
+
lines.push("--" + boundary);
|
|
865
|
+
lines.push("Content-Type: text/plain; charset=utf-8");
|
|
866
|
+
lines.push("Content-Transfer-Encoding: 8bit");
|
|
867
|
+
lines.push("");
|
|
868
|
+
lines.push(humanText);
|
|
869
|
+
lines.push("");
|
|
870
|
+
|
|
871
|
+
// Part 2 - message/delivery-status.
|
|
872
|
+
lines.push("--" + boundary);
|
|
873
|
+
lines.push("Content-Type: message/delivery-status");
|
|
874
|
+
lines.push("");
|
|
875
|
+
// Per-message group.
|
|
876
|
+
var perMessage = "";
|
|
877
|
+
perMessage += _foldFieldValue("Reporting-MTA", reportingMta);
|
|
878
|
+
perMessage += _foldFieldValue("Arrival-Date", arrivalDate);
|
|
879
|
+
if (opts.originalEnvelopeId) {
|
|
880
|
+
perMessage += _foldFieldValue("Original-Envelope-Id", opts.originalEnvelopeId);
|
|
881
|
+
}
|
|
882
|
+
lines.push(perMessage.replace(/\r\n$/, ""));
|
|
883
|
+
lines.push("");
|
|
884
|
+
// Per-recipient group.
|
|
885
|
+
var perRecip = "";
|
|
886
|
+
if (opts.originalRecipient) {
|
|
887
|
+
perRecip += _foldFieldValue("Original-Recipient",
|
|
888
|
+
origRecipType + ";" + opts.originalRecipient);
|
|
889
|
+
}
|
|
890
|
+
perRecip += _foldFieldValue("Final-Recipient",
|
|
891
|
+
recipType + ";" + opts.finalRecipient);
|
|
892
|
+
perRecip += _foldFieldValue("Action", action);
|
|
893
|
+
perRecip += _foldFieldValue("Status", opts.status);
|
|
894
|
+
if (remoteMta) {
|
|
895
|
+
perRecip += _foldFieldValue("Remote-MTA", remoteMta);
|
|
896
|
+
}
|
|
897
|
+
if (diagnosticCode) {
|
|
898
|
+
perRecip += _foldFieldValue("Diagnostic-Code", diagnosticCode);
|
|
899
|
+
}
|
|
900
|
+
if (opts.lastAttemptDate) {
|
|
901
|
+
perRecip += _foldFieldValue("Last-Attempt-Date", opts.lastAttemptDate);
|
|
902
|
+
}
|
|
903
|
+
if (opts.willRetryUntil) {
|
|
904
|
+
perRecip += _foldFieldValue("Will-Retry-Until", opts.willRetryUntil);
|
|
905
|
+
}
|
|
906
|
+
lines.push(perRecip.replace(/\r\n$/, ""));
|
|
907
|
+
lines.push("");
|
|
908
|
+
|
|
909
|
+
// Part 3 (optional) - original message or just headers per RFC 3461
|
|
910
|
+
// RET= choice. The framework picks the part-type from the
|
|
911
|
+
// originalMessage shape: if `{ headersOnly: true, headers: "..." }`
|
|
912
|
+
// is supplied, emit text/rfc822-headers; if a plain string is
|
|
913
|
+
// supplied, emit message/rfc822 with the full body.
|
|
914
|
+
if (originalMessage) {
|
|
915
|
+
lines.push("--" + boundary);
|
|
916
|
+
if (typeof originalMessage === "object" && originalMessage.headersOnly) {
|
|
917
|
+
lines.push("Content-Type: text/rfc822-headers");
|
|
918
|
+
lines.push("");
|
|
919
|
+
lines.push(originalMessage.headers || "");
|
|
920
|
+
} else {
|
|
921
|
+
lines.push("Content-Type: message/rfc822");
|
|
922
|
+
lines.push("");
|
|
923
|
+
lines.push(typeof originalMessage === "string" ? originalMessage : "");
|
|
924
|
+
}
|
|
925
|
+
lines.push("");
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
lines.push("--" + boundary + "--");
|
|
929
|
+
lines.push("");
|
|
930
|
+
return lines.join("\r\n");
|
|
931
|
+
}
|
|
932
|
+
|
|
561
933
|
module.exports = {
|
|
562
934
|
parse: parse,
|
|
563
935
|
handler: handler,
|
|
@@ -570,4 +942,14 @@ module.exports = {
|
|
|
570
942
|
ses: _parseSes,
|
|
571
943
|
resend: _parseResend,
|
|
572
944
|
},
|
|
945
|
+
// Generic RFC 3464 / RFC 3461 / RFC 6533 DSN parser + generator.
|
|
946
|
+
dsn: {
|
|
947
|
+
parse: _parseDsn,
|
|
948
|
+
build: _buildDsn,
|
|
949
|
+
// Tables surfaced for tests + advanced operator code (e.g. operators
|
|
950
|
+
// building DSN-shaped reports against a custom action vocabulary).
|
|
951
|
+
PER_MESSAGE_FIELDS: DSN_PER_MESSAGE_FIELDS,
|
|
952
|
+
PER_RECIPIENT_FIELDS: DSN_PER_RECIPIENT_FIELDS,
|
|
953
|
+
ACTIONS: DSN_ACTIONS,
|
|
954
|
+
},
|
|
573
955
|
};
|