@blamejs/core 0.8.52 → 0.8.57
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 +5 -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/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/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-mdn.js
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.mailMdn
|
|
4
|
+
* @nav Communication
|
|
5
|
+
* @title MDN
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* RFC 3798 / RFC 8098 Message Disposition Notification builder +
|
|
9
|
+
* parser. An MDN is the "I read your message" return-receipt — a
|
|
10
|
+
* multipart/report MIME body with a `message/disposition-notification`
|
|
11
|
+
* segment that names what the user-agent did with the original
|
|
12
|
+
* message (displayed / deleted / dispatched / processed / failed).
|
|
13
|
+
*
|
|
14
|
+
* Auto-generation discipline: the framework refuses to auto-build an
|
|
15
|
+
* MDN unless the operator explicitly opts in via
|
|
16
|
+
* `requireUserConfirmation: false`. RFC 3798 §2.1 plus RFC 8098
|
|
17
|
+
* require user opt-in for MDN delivery — accidental automatic
|
|
18
|
+
* generation leaks behavioural metadata and is a known privacy
|
|
19
|
+
* regression in mail clients. The framework defaults to refusal so
|
|
20
|
+
* the operator codepath has to actively choose to send.
|
|
21
|
+
*
|
|
22
|
+
* Parser tolerates both bare RFC 3798 reports and the RFC 8098
|
|
23
|
+
* updated shape (action / sending / disposition modes), surfaces
|
|
24
|
+
* the original-message-id binding, the reporting user-agent string,
|
|
25
|
+
* and the optional original-message attachment.
|
|
26
|
+
*
|
|
27
|
+
* @card
|
|
28
|
+
* RFC 3798 / RFC 8098 Message Disposition Notification builder + parser — generate "message read" return-receipts and parse inbound MDNs into a normalized event shape. Auto-generation refuses without explicit operator opt-in to prevent accidental privacy leaks.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
var crypto = require("./crypto");
|
|
32
|
+
var lazyRequire = require("./lazy-require");
|
|
33
|
+
var mimeParse = require("./mime-parse");
|
|
34
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
35
|
+
var C = require("./constants");
|
|
36
|
+
var validateOpts = require("./validate-opts");
|
|
37
|
+
var { MailMdnError } = require("./framework-error");
|
|
38
|
+
|
|
39
|
+
// Body cap for the MDN parser — same rationale as the DSN cap. RFC
|
|
40
|
+
// 3798 doesn't specify a maximum, but real-world MDNs are tiny;
|
|
41
|
+
// anything above 1 MiB is pathological and will pin the regex
|
|
42
|
+
// scanner if accepted.
|
|
43
|
+
var MDN_MAX_BYTES = C.BYTES.mib(1);
|
|
44
|
+
|
|
45
|
+
// RFC 3798 §3.2.6 disposition-types.
|
|
46
|
+
var DISPOSITION_TYPES = {
|
|
47
|
+
"displayed": true,
|
|
48
|
+
"deleted": true,
|
|
49
|
+
"dispatched": true,
|
|
50
|
+
"processed": true,
|
|
51
|
+
"failed": true,
|
|
52
|
+
"denied": true,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// RFC 3798 §3.2.6.1 action-modes.
|
|
56
|
+
var ACTION_MODES = {
|
|
57
|
+
"manual-action": true,
|
|
58
|
+
"automatic-action": true,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// RFC 3798 §3.2.6.2 sending-modes.
|
|
62
|
+
var SENDING_MODES = {
|
|
63
|
+
"mdn-sent-manually": true,
|
|
64
|
+
"mdn-sent-automatically": true,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function _err(code, message) {
|
|
68
|
+
return new MailMdnError(code, message);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _parseDisposition(value) {
|
|
72
|
+
// RFC 3798 §3.2.6 — `disposition-mode; disposition-type/<modifier>`
|
|
73
|
+
// examples:
|
|
74
|
+
// manual-action/MDN-sent-manually; displayed
|
|
75
|
+
// automatic-action/MDN-sent-automatically; processed/error
|
|
76
|
+
if (typeof value !== "string") return null;
|
|
77
|
+
var semi = value.indexOf(";");
|
|
78
|
+
if (semi === -1) {
|
|
79
|
+
return {
|
|
80
|
+
actionMode: null,
|
|
81
|
+
sendingMode: null,
|
|
82
|
+
type: value.trim().toLowerCase(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
var modePart = value.slice(0, semi).trim();
|
|
86
|
+
var typePart = value.slice(semi + 1).trim();
|
|
87
|
+
var slash = modePart.indexOf("/");
|
|
88
|
+
var actionMode = slash === -1 ? modePart.toLowerCase() : modePart.slice(0, slash).trim().toLowerCase();
|
|
89
|
+
var sendingMode = slash === -1 ? null : modePart.slice(slash + 1).trim().toLowerCase();
|
|
90
|
+
var type = typePart.toLowerCase();
|
|
91
|
+
// Strip /modifier off the type token.
|
|
92
|
+
var typeSlash = type.indexOf("/");
|
|
93
|
+
if (typeSlash !== -1) type = type.slice(0, typeSlash).trim();
|
|
94
|
+
return {
|
|
95
|
+
actionMode: actionMode,
|
|
96
|
+
sendingMode: sendingMode,
|
|
97
|
+
type: type,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _generateBoundary() {
|
|
102
|
+
return "blamejs-mdn-" + crypto.generateToken(C.BYTES.bytes(12));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @primitive b.mailMdn.build
|
|
107
|
+
* @signature b.mailMdn.build(opts)
|
|
108
|
+
* @since 0.8.53
|
|
109
|
+
* @status stable
|
|
110
|
+
* @related b.mailMdn.parse, b.mailBounce.parse
|
|
111
|
+
*
|
|
112
|
+
* Build an RFC 3798 / RFC 8098 multipart/report message body carrying
|
|
113
|
+
* a `message/disposition-notification` segment. The result is a raw
|
|
114
|
+
* RFC 5322 message body ready for SMTP relay back to the sender.
|
|
115
|
+
*
|
|
116
|
+
* The framework refuses to auto-generate an MDN (emits the audit row
|
|
117
|
+
* `mailmdn.suppressed` instead) when:
|
|
118
|
+
*
|
|
119
|
+
* - The original message's `Disposition-Notification-Options`
|
|
120
|
+
* header asserted `important=required` AND
|
|
121
|
+
* - `opts.requireUserConfirmation` is not explicitly `false`
|
|
122
|
+
*
|
|
123
|
+
* RFC 3798 §2.1 requires user opt-in for MDN delivery; the default is
|
|
124
|
+
* refusal so accidental automatic generation by an unattended mail
|
|
125
|
+
* processor cannot leak behavioural metadata. Operators with an
|
|
126
|
+
* explicit "the user clicked send-receipt" code path pass
|
|
127
|
+
* `requireUserConfirmation: false` to skip the gate.
|
|
128
|
+
*
|
|
129
|
+
* @opts
|
|
130
|
+
* originalMessageId: string, // required — Message-Id of the message being acknowledged
|
|
131
|
+
* originalRecipient: string, // optional — RFC 5322 address of the original recipient
|
|
132
|
+
* finalRecipient: string, // required — RFC 5322 address of the final-recipient (may differ after forwarding)
|
|
133
|
+
* disposition: "displayed" | "deleted" | "dispatched" | "processed" | "failed" | "denied",
|
|
134
|
+
* actionMode: "manual-action" | "automatic-action", // default: manual-action
|
|
135
|
+
* sendingMode: "MDN-sent-manually" | "MDN-sent-automatically", // default: MDN-sent-manually
|
|
136
|
+
* reportingUserAgent: string, // optional — RFC 3798 §3.2.1 reporting agent name/version
|
|
137
|
+
* originalMessage: string, // optional — raw RFC 5322 message body to attach as message/rfc822
|
|
138
|
+
* from: string, // optional — From: header for the MDN envelope
|
|
139
|
+
* to: string, // optional — To: header for the MDN envelope (typically the original sender)
|
|
140
|
+
* subject: string, // optional — Subject: header
|
|
141
|
+
* dispositionNotificationOptions: string, // RFC 3798 Disposition-Notification-Options value from the inbound message
|
|
142
|
+
* requireUserConfirmation: boolean, // default: true — refuse to auto-build unless the operator explicitly opts out
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* var b = require("@blamejs/core");
|
|
146
|
+
* var mdn = b.mailMdn.build({
|
|
147
|
+
* originalMessageId: "<orig-1@sender.example>",
|
|
148
|
+
* finalRecipient: "user@example.com",
|
|
149
|
+
* disposition: "displayed",
|
|
150
|
+
* reportingUserAgent: "blamejs/0.8.53",
|
|
151
|
+
* requireUserConfirmation: false,
|
|
152
|
+
* });
|
|
153
|
+
* typeof mdn; // -> "string"
|
|
154
|
+
* /multipart\/report/.test(mdn); // -> true
|
|
155
|
+
* /message\/disposition-notification/.test(mdn); // -> true
|
|
156
|
+
*/
|
|
157
|
+
function build(opts) {
|
|
158
|
+
validateOpts.requireObject(opts, "mailMdn.build", MailMdnError, "mdn/missing-required-field");
|
|
159
|
+
validateOpts.requireNonEmptyString(opts.originalMessageId,
|
|
160
|
+
"mailMdn.build: opts.originalMessageId", MailMdnError, "mdn/missing-required-field");
|
|
161
|
+
validateOpts.requireNonEmptyString(opts.finalRecipient,
|
|
162
|
+
"mailMdn.build: opts.finalRecipient", MailMdnError, "mdn/missing-required-field");
|
|
163
|
+
var disposition = String(opts.disposition || "").toLowerCase();
|
|
164
|
+
if (!DISPOSITION_TYPES[disposition]) {
|
|
165
|
+
throw _err("mdn/missing-required-field",
|
|
166
|
+
"mailMdn.build: opts.disposition must be one of " +
|
|
167
|
+
Object.keys(DISPOSITION_TYPES).join(" / ") +
|
|
168
|
+
"; got '" + String(opts.disposition) + "'");
|
|
169
|
+
}
|
|
170
|
+
var actionMode = String(opts.actionMode || "manual-action").toLowerCase();
|
|
171
|
+
if (!ACTION_MODES[actionMode]) {
|
|
172
|
+
throw _err("mdn/missing-required-field",
|
|
173
|
+
"mailMdn.build: opts.actionMode must be one of " +
|
|
174
|
+
Object.keys(ACTION_MODES).join(" / ") +
|
|
175
|
+
"; got '" + String(opts.actionMode) + "'");
|
|
176
|
+
}
|
|
177
|
+
var sendingMode = String(opts.sendingMode || "mdn-sent-manually").toLowerCase();
|
|
178
|
+
// Accept the canonical mixed-case form too — RFC 3798 §3.2.6.2 uses
|
|
179
|
+
// `MDN-sent-manually` / `MDN-sent-automatically`. Compare lower-case
|
|
180
|
+
// for robustness; emit canonical mixed-case in the output.
|
|
181
|
+
if (!SENDING_MODES[sendingMode]) {
|
|
182
|
+
throw _err("mdn/missing-required-field",
|
|
183
|
+
"mailMdn.build: opts.sendingMode must be one of " +
|
|
184
|
+
"MDN-sent-manually / MDN-sent-automatically; got '" +
|
|
185
|
+
String(opts.sendingMode) + "'");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Auto-generation gate. RFC 3798 §2.1 — when the inbound message's
|
|
189
|
+
// Disposition-Notification-Options header asserts important=required,
|
|
190
|
+
// an auto-processor must not emit an MDN without explicit user
|
|
191
|
+
// confirmation. The framework defaults to requiring opt-in
|
|
192
|
+
// (requireUserConfirmation defaults to true) so accidental
|
|
193
|
+
// automatic generation is a typed refusal.
|
|
194
|
+
var requireConfirmation = opts.requireUserConfirmation !== false;
|
|
195
|
+
var dnOpts = String(opts.dispositionNotificationOptions || "").toLowerCase();
|
|
196
|
+
var requestRequiresConfirmation = /important\s*=\s*required/.test(dnOpts);
|
|
197
|
+
if (requestRequiresConfirmation && requireConfirmation) {
|
|
198
|
+
audit().safeEmit({
|
|
199
|
+
action: "mailmdn.suppressed",
|
|
200
|
+
outcome: "denied",
|
|
201
|
+
metadata: {
|
|
202
|
+
originalMessageId: opts.originalMessageId,
|
|
203
|
+
finalRecipient: opts.finalRecipient,
|
|
204
|
+
reason: "auto-generation refused: Disposition-Notification-Options demands user confirmation",
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
throw _err("mdn/auto-generation-refused",
|
|
208
|
+
"mailMdn.build: inbound Disposition-Notification-Options asserts important=required " +
|
|
209
|
+
"and opts.requireUserConfirmation is not explicitly false (RFC 3798 §2.1)");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
var boundary = _generateBoundary();
|
|
213
|
+
var recipType = mimeParse.addressType(opts.finalRecipient);
|
|
214
|
+
var origRecipType = opts.originalRecipient ? mimeParse.addressType(opts.originalRecipient) : recipType;
|
|
215
|
+
|
|
216
|
+
// Canonical sendingMode casing for the output (RFC 3798 §3.2.6.2).
|
|
217
|
+
var sendingModeOut = sendingMode === "mdn-sent-automatically"
|
|
218
|
+
? "MDN-sent-automatically"
|
|
219
|
+
: "MDN-sent-manually";
|
|
220
|
+
|
|
221
|
+
var lines = [];
|
|
222
|
+
lines.push("MIME-Version: 1.0");
|
|
223
|
+
lines.push('Content-Type: multipart/report; report-type=disposition-notification; boundary="' + boundary + '"');
|
|
224
|
+
if (opts.from) lines.push("From: " + opts.from);
|
|
225
|
+
if (opts.to) lines.push("To: " + opts.to);
|
|
226
|
+
if (opts.subject) lines.push("Subject: " + opts.subject);
|
|
227
|
+
lines.push("");
|
|
228
|
+
|
|
229
|
+
// Part 1 — human-readable description.
|
|
230
|
+
lines.push("--" + boundary);
|
|
231
|
+
lines.push("Content-Type: text/plain; charset=utf-8");
|
|
232
|
+
lines.push("Content-Transfer-Encoding: 8bit");
|
|
233
|
+
lines.push("");
|
|
234
|
+
lines.push("This is a Message Disposition Notification.");
|
|
235
|
+
lines.push("");
|
|
236
|
+
lines.push("The message sent on " + new Date().toUTCString());
|
|
237
|
+
lines.push("to " + opts.finalRecipient);
|
|
238
|
+
lines.push("with subject of (none) was " + disposition + ".");
|
|
239
|
+
lines.push("");
|
|
240
|
+
|
|
241
|
+
// Part 2 — message/disposition-notification.
|
|
242
|
+
lines.push("--" + boundary);
|
|
243
|
+
lines.push("Content-Type: message/disposition-notification");
|
|
244
|
+
lines.push("");
|
|
245
|
+
if (opts.reportingUserAgent) {
|
|
246
|
+
lines.push("Reporting-UA: " + opts.reportingUserAgent);
|
|
247
|
+
}
|
|
248
|
+
if (opts.originalRecipient) {
|
|
249
|
+
lines.push("Original-Recipient: " + origRecipType + ";" + opts.originalRecipient);
|
|
250
|
+
}
|
|
251
|
+
lines.push("Final-Recipient: " + recipType + ";" + opts.finalRecipient);
|
|
252
|
+
lines.push("Original-Message-ID: " + opts.originalMessageId);
|
|
253
|
+
lines.push("Disposition: " + actionMode + "/" + sendingModeOut + "; " + disposition);
|
|
254
|
+
lines.push("");
|
|
255
|
+
|
|
256
|
+
// Part 3 (optional) — original message attached as message/rfc822.
|
|
257
|
+
if (opts.originalMessage && typeof opts.originalMessage === "string") {
|
|
258
|
+
lines.push("--" + boundary);
|
|
259
|
+
lines.push("Content-Type: message/rfc822");
|
|
260
|
+
lines.push("");
|
|
261
|
+
lines.push(opts.originalMessage);
|
|
262
|
+
lines.push("");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
lines.push("--" + boundary + "--");
|
|
266
|
+
lines.push("");
|
|
267
|
+
|
|
268
|
+
audit().safeEmit({
|
|
269
|
+
action: "mailmdn.generated",
|
|
270
|
+
outcome: "success",
|
|
271
|
+
metadata: {
|
|
272
|
+
originalMessageId: opts.originalMessageId,
|
|
273
|
+
finalRecipient: opts.finalRecipient,
|
|
274
|
+
disposition: disposition,
|
|
275
|
+
actionMode: actionMode,
|
|
276
|
+
sendingMode: sendingModeOut,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return lines.join("\r\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @primitive b.mailMdn.parse
|
|
285
|
+
* @signature b.mailMdn.parse(rawMessage)
|
|
286
|
+
* @since 0.8.53
|
|
287
|
+
* @status stable
|
|
288
|
+
* @related b.mailMdn.build, b.mailBounce.parse
|
|
289
|
+
*
|
|
290
|
+
* Parse a raw RFC 3798 / RFC 8098 multipart/report message into a
|
|
291
|
+
* normalized event shape:
|
|
292
|
+
*
|
|
293
|
+
* {
|
|
294
|
+
* messageId: string | null, // outer Message-ID of the MDN itself
|
|
295
|
+
* originalMessageId: string, // Original-Message-ID field
|
|
296
|
+
* originalRecipient: string | null, // Original-Recipient field
|
|
297
|
+
* finalRecipient: string, // Final-Recipient field (required)
|
|
298
|
+
* disposition: {
|
|
299
|
+
* actionMode: "manual-action" | "automatic-action",
|
|
300
|
+
* sendingMode: "mdn-sent-manually" | "mdn-sent-automatically" | null,
|
|
301
|
+
* type: "displayed" | "deleted" | "dispatched" | "processed" | "failed" | "denied",
|
|
302
|
+
* },
|
|
303
|
+
* reportingUserAgent: string | null,
|
|
304
|
+
* originalMessage: string | null, // attached message/rfc822 body, when present
|
|
305
|
+
* }
|
|
306
|
+
*
|
|
307
|
+
* Throws `MailMdnError` on missing top-level Content-Type, non-
|
|
308
|
+
* multipart/report content type, missing message/disposition-
|
|
309
|
+
* notification segment, missing Final-Recipient, or oversized payload.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* var b = require("@blamejs/core");
|
|
313
|
+
* var mdn = b.mailMdn.build({
|
|
314
|
+
* originalMessageId: "<orig-1@sender.example>",
|
|
315
|
+
* finalRecipient: "user@example.com",
|
|
316
|
+
* disposition: "displayed",
|
|
317
|
+
* reportingUserAgent: "blamejs/0.8.53",
|
|
318
|
+
* requireUserConfirmation: false,
|
|
319
|
+
* });
|
|
320
|
+
* var parsed = b.mailMdn.parse(mdn);
|
|
321
|
+
* parsed.disposition.type; // -> "displayed"
|
|
322
|
+
* parsed.finalRecipient; // -> "user@example.com"
|
|
323
|
+
* parsed.originalMessageId; // -> "<orig-1@sender.example>"
|
|
324
|
+
*/
|
|
325
|
+
function parse(rawMessage) {
|
|
326
|
+
if (typeof rawMessage !== "string" || rawMessage.length === 0) {
|
|
327
|
+
throw _err("mdn/parse-failed",
|
|
328
|
+
"mailMdn.parse: rawMessage must be a non-empty string");
|
|
329
|
+
}
|
|
330
|
+
if (rawMessage.length > MDN_MAX_BYTES) {
|
|
331
|
+
throw _err("mdn/parse-failed",
|
|
332
|
+
"mailMdn.parse: message exceeds " + MDN_MAX_BYTES + " bytes");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
var top = mimeParse.splitHeadersAndBody(rawMessage);
|
|
336
|
+
var ctRaw = mimeParse.findHeader(top.headers, "Content-Type");
|
|
337
|
+
if (!ctRaw) {
|
|
338
|
+
throw _err("mdn/parse-failed",
|
|
339
|
+
"mailMdn.parse: missing top-level Content-Type");
|
|
340
|
+
}
|
|
341
|
+
var ct = mimeParse.parseContentType(ctRaw);
|
|
342
|
+
if (ct.type !== "multipart/report") {
|
|
343
|
+
throw _err("mdn/parse-failed",
|
|
344
|
+
"mailMdn.parse: top-level Content-Type must be multipart/report; got " + ct.type);
|
|
345
|
+
}
|
|
346
|
+
if (ct.params["report-type"] && ct.params["report-type"].toLowerCase() !== "disposition-notification") {
|
|
347
|
+
throw _err("mdn/parse-failed",
|
|
348
|
+
"mailMdn.parse: report-type must be disposition-notification; got " + ct.params["report-type"]);
|
|
349
|
+
}
|
|
350
|
+
var boundary = ct.params.boundary;
|
|
351
|
+
if (!boundary) {
|
|
352
|
+
throw _err("mdn/parse-failed",
|
|
353
|
+
"mailMdn.parse: multipart/report missing boundary parameter");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
var parts = mimeParse.splitMimeParts(top.body, boundary);
|
|
357
|
+
if (parts.length < 2) {
|
|
358
|
+
throw _err("mdn/parse-failed",
|
|
359
|
+
"mailMdn.parse: multipart/report needs at least 2 parts; got " + parts.length);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
var notification = null;
|
|
363
|
+
var originalMessage = null;
|
|
364
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
365
|
+
var partSplit = mimeParse.splitHeadersAndBody(parts[i].replace(/^\r?\n/, ""));
|
|
366
|
+
var partCtRaw = mimeParse.findHeader(partSplit.headers, "Content-Type") || "text/plain";
|
|
367
|
+
var partCt = mimeParse.parseContentType(partCtRaw);
|
|
368
|
+
if (partCt.type === "message/disposition-notification") {
|
|
369
|
+
notification = mimeParse.parseHeaderBlock(partSplit.body);
|
|
370
|
+
} else if (partCt.type === "message/rfc822" || partCt.type === "text/rfc822-headers") {
|
|
371
|
+
originalMessage = partSplit.body;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!notification) {
|
|
376
|
+
throw _err("mdn/parse-failed",
|
|
377
|
+
"mailMdn.parse: no message/disposition-notification part found");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Index notification fields by lowercase name.
|
|
381
|
+
var fields = {};
|
|
382
|
+
for (var j = 0; j < notification.length; j += 1) {
|
|
383
|
+
fields[notification[j].name.toLowerCase()] = notification[j].value;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
var finalRecipient = mimeParse.stripAddressType(fields["final-recipient"]);
|
|
387
|
+
if (!finalRecipient) {
|
|
388
|
+
throw _err("mdn/missing-required-field",
|
|
389
|
+
"mailMdn.parse: message/disposition-notification missing Final-Recipient");
|
|
390
|
+
}
|
|
391
|
+
var dispositionField = fields["disposition"];
|
|
392
|
+
if (!dispositionField) {
|
|
393
|
+
throw _err("mdn/missing-required-field",
|
|
394
|
+
"mailMdn.parse: message/disposition-notification missing Disposition");
|
|
395
|
+
}
|
|
396
|
+
var disposition = _parseDisposition(dispositionField);
|
|
397
|
+
if (!disposition || !DISPOSITION_TYPES[disposition.type]) {
|
|
398
|
+
throw _err("mdn/parse-failed",
|
|
399
|
+
"mailMdn.parse: Disposition type token not in RFC 3798 §3.2.6 vocabulary; got '" +
|
|
400
|
+
(disposition && disposition.type) + "'");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
messageId: mimeParse.findHeader(top.headers, "Message-ID"),
|
|
405
|
+
originalMessageId: fields["original-message-id"] || null,
|
|
406
|
+
originalRecipient: mimeParse.stripAddressType(fields["original-recipient"]),
|
|
407
|
+
finalRecipient: finalRecipient,
|
|
408
|
+
disposition: disposition,
|
|
409
|
+
reportingUserAgent: fields["reporting-ua"] || null,
|
|
410
|
+
originalMessage: originalMessage,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = {
|
|
415
|
+
build: build,
|
|
416
|
+
parse: parse,
|
|
417
|
+
MailMdnError: MailMdnError,
|
|
418
|
+
// Vocabulary tables surfaced for tests + advanced operator code
|
|
419
|
+
// (e.g. operators wiring an inbound MDN router that switches on
|
|
420
|
+
// disposition.type).
|
|
421
|
+
DISPOSITION_TYPES: Object.keys(DISPOSITION_TYPES),
|
|
422
|
+
ACTION_MODES: Object.keys(ACTION_MODES),
|
|
423
|
+
SENDING_MODES: Object.keys(SENDING_MODES),
|
|
424
|
+
};
|