@bumail/queue 0.7.2 → 0.9.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/README.md +27 -3
- package/dist/chunks/{index-3pt7rc59.js → index-9yp15arf.js} +98 -25
- package/dist/chunks/index-9yp15arf.js.map +14 -0
- package/dist/chunks/index-r8wb840a.js +24 -0
- package/dist/chunks/index-r8wb840a.js.map +10 -0
- package/dist/contract/checks.d.ts +2 -6
- package/dist/contract/checks.d.ts.map +1 -1
- package/dist/contract/dsn-checks.d.ts +4 -0
- package/dist/contract/dsn-checks.d.ts.map +1 -0
- package/dist/contract/dsn-column.d.ts +12 -0
- package/dist/contract/dsn-column.d.ts.map +1 -0
- package/dist/contract/queue-store.d.ts +2 -1
- package/dist/contract/queue-store.d.ts.map +1 -1
- package/dist/contract/storable.d.ts +7 -0
- package/dist/contract/storable.d.ts.map +1 -0
- package/dist/contract/types.d.ts +52 -0
- package/dist/contract/types.d.ts.map +1 -1
- package/dist/dsn/build.d.ts +2 -0
- package/dist/dsn/build.d.ts.map +1 -1
- package/dist/dsn/delivered.d.ts +39 -0
- package/dist/dsn/delivered.d.ts.map +1 -0
- package/dist/dsn/fields.d.ts +11 -2
- package/dist/dsn/fields.d.ts.map +1 -1
- package/dist/dsn/human.d.ts +7 -0
- package/dist/dsn/human.d.ts.map +1 -0
- package/dist/dsn/policy.d.ts +17 -0
- package/dist/dsn/policy.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +474 -300
- package/dist/index.js.map +20 -15
- package/dist/memory/index.js +1 -1
- package/dist/memory/index.js.map +1 -1
- package/dist/postgres/index.js +17 -9
- package/dist/postgres/index.js.map +6 -6
- package/dist/postgres/rows.d.ts +6 -1
- package/dist/postgres/rows.d.ts.map +1 -1
- package/dist/postgres/schema.d.ts.map +1 -1
- package/dist/postgres/statements.d.ts +9 -8
- package/dist/postgres/statements.d.ts.map +1 -1
- package/dist/postgres/store.d.ts.map +1 -1
- package/dist/queue/attempt.d.ts +7 -7
- package/dist/queue/attempt.d.ts.map +1 -1
- package/dist/queue/deliver.d.ts.map +1 -1
- package/dist/queue/envelope.d.ts +24 -4
- package/dist/queue/envelope.d.ts.map +1 -1
- package/dist/queue/events.d.ts +8 -1
- package/dist/queue/events.d.ts.map +1 -1
- package/dist/queue/fallback-session.d.ts +22 -0
- package/dist/queue/fallback-session.d.ts.map +1 -0
- package/dist/queue/notify.d.ts +6 -3
- package/dist/queue/notify.d.ts.map +1 -1
- package/dist/queue/outcome.d.ts.map +1 -1
- package/dist/queue/queue.d.ts.map +1 -1
- package/dist/queue/route.d.ts +7 -0
- package/dist/queue/route.d.ts.map +1 -1
- package/dist/queue/session.d.ts +14 -0
- package/dist/queue/session.d.ts.map +1 -0
- package/dist/queue/settle.d.ts.map +1 -1
- package/dist/redis/index.js +26 -13
- package/dist/redis/index.js.map +5 -5
- package/dist/redis/items.d.ts +2 -0
- package/dist/redis/items.d.ts.map +1 -1
- package/dist/redis/scripts.d.ts +1 -1
- package/dist/redis/scripts.d.ts.map +1 -1
- package/dist/redis/store.d.ts.map +1 -1
- package/dist/sqlite/index.js +15 -8
- package/dist/sqlite/index.js.map +5 -5
- package/dist/sqlite/rows.d.ts +2 -1
- package/dist/sqlite/rows.d.ts.map +1 -1
- package/dist/sqlite/schema.d.ts.map +1 -1
- package/dist/sqlite/store.d.ts.map +1 -1
- package/docs/guide.md +97 -7
- package/docs/roadmap.md +14 -7
- package/docs/troubleshooting.md +52 -1
- package/package.json +3 -3
- package/dist/chunks/index-3pt7rc59.js.map +0 -12
package/dist/index.js
CHANGED
|
@@ -1,12 +1,336 @@
|
|
|
1
1
|
import {
|
|
2
|
-
QueueError2,
|
|
3
|
-
invalid,
|
|
4
2
|
hasControl,
|
|
5
3
|
replaceControls,
|
|
6
4
|
cleanText,
|
|
7
5
|
asciiText,
|
|
6
|
+
QueueError2,
|
|
7
|
+
invalid,
|
|
8
8
|
checkOwner
|
|
9
|
-
} from "./chunks/index-
|
|
9
|
+
} from "./chunks/index-9yp15arf.js";
|
|
10
|
+
|
|
11
|
+
// src/dsn/build.ts
|
|
12
|
+
import {
|
|
13
|
+
encodeHeaderValue,
|
|
14
|
+
encodeQuotedPrintable,
|
|
15
|
+
foldHeader as foldHeader2,
|
|
16
|
+
formatDate as formatDate3
|
|
17
|
+
} from "@bumail/mime";
|
|
18
|
+
|
|
19
|
+
// src/dsn/content.ts
|
|
20
|
+
var CR = 13;
|
|
21
|
+
var LF = 10;
|
|
22
|
+
var MAX_LINE = 998;
|
|
23
|
+
var isContinuation = (byte) => byte !== undefined && byte >= 128 && byte < 192;
|
|
24
|
+
function crlf(bytes) {
|
|
25
|
+
const out = [];
|
|
26
|
+
let column = 0;
|
|
27
|
+
for (let i = 0;i < bytes.length; i++) {
|
|
28
|
+
const byte = bytes[i];
|
|
29
|
+
if (byte === CR || byte === LF) {
|
|
30
|
+
out.push(CR, LF);
|
|
31
|
+
column = 0;
|
|
32
|
+
if (byte === CR && bytes[i + 1] === LF)
|
|
33
|
+
i++;
|
|
34
|
+
} else if (column < MAX_LINE) {
|
|
35
|
+
out.push(byte);
|
|
36
|
+
column++;
|
|
37
|
+
} else if (column === MAX_LINE) {
|
|
38
|
+
if (isContinuation(byte)) {
|
|
39
|
+
while (isContinuation(out.at(-1))) {
|
|
40
|
+
out.pop();
|
|
41
|
+
}
|
|
42
|
+
if ((out.at(-1) ?? 0) >= 192)
|
|
43
|
+
out.pop();
|
|
44
|
+
}
|
|
45
|
+
column++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return Uint8Array.from(out);
|
|
49
|
+
}
|
|
50
|
+
function headerEnd(bytes) {
|
|
51
|
+
for (let i = 0;i + 3 < bytes.length; i++) {
|
|
52
|
+
if (bytes[i] === CR && bytes[i + 1] === LF && bytes[i + 2] === CR && bytes[i + 3] === LF) {
|
|
53
|
+
return i + 2;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return bytes.length;
|
|
57
|
+
}
|
|
58
|
+
function wholeLines(bytes, max) {
|
|
59
|
+
if (bytes.length <= max)
|
|
60
|
+
return bytes;
|
|
61
|
+
const cut = bytes.subarray(0, max).lastIndexOf(LF);
|
|
62
|
+
return bytes.subarray(0, cut + 1);
|
|
63
|
+
}
|
|
64
|
+
function returned(original, mode, max) {
|
|
65
|
+
const message = crlf(original.subarray(0, max * 2 + 2));
|
|
66
|
+
const whole = original.length <= max && message.length <= max;
|
|
67
|
+
const body = whole && mode === "full" ? message : wholeLines(message.subarray(0, headerEnd(message)), max);
|
|
68
|
+
return {
|
|
69
|
+
type: whole && mode === "full" ? "message/rfc822" : "text/rfc822-headers",
|
|
70
|
+
body,
|
|
71
|
+
eightBit: body.some((byte) => byte > 127)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/dsn/fields.ts
|
|
76
|
+
import { foldHeader, formatDate } from "@bumail/mime";
|
|
77
|
+
var strip = (text) => replaceControls(text, "");
|
|
78
|
+
var MAX_ESCAPED = 900;
|
|
79
|
+
function typedAddress(address) {
|
|
80
|
+
const clean = cleanAddress(address);
|
|
81
|
+
if (isAscii(clean))
|
|
82
|
+
return `rfc822; ${clean}`;
|
|
83
|
+
return `utf-8; ${escapedAddress(clean).slice(0, MAX_ESCAPED)}`;
|
|
84
|
+
}
|
|
85
|
+
var cleanAddress = (address) => strip(address).replace(/[\s\\]/g, "");
|
|
86
|
+
var isAscii = (text) => /^[\x21-\x7e]*$/.test(text);
|
|
87
|
+
function escapedAddress(clean) {
|
|
88
|
+
let escaped = "";
|
|
89
|
+
for (const c of clean) {
|
|
90
|
+
escaped += /[\x21-\x7e]/.test(c) ? c : `\\x{${(c.codePointAt(0) ?? 0).toString(16).toUpperCase()}}`;
|
|
91
|
+
}
|
|
92
|
+
return escaped;
|
|
93
|
+
}
|
|
94
|
+
var remoteMta = (host) => host && /^[A-Za-z0-9.:[\]-]{1,255}$/.test(host) ? `dns; ${host}` : undefined;
|
|
95
|
+
function statusOf(kind, reply) {
|
|
96
|
+
if (reply?.status)
|
|
97
|
+
return reply.status;
|
|
98
|
+
if (reply?.code !== undefined) {
|
|
99
|
+
if (reply.code < 400)
|
|
100
|
+
return "2.0.0";
|
|
101
|
+
return reply.code >= 500 ? "5.0.0" : "4.0.0";
|
|
102
|
+
}
|
|
103
|
+
if (kind === "relayed" || kind === "delivered")
|
|
104
|
+
return "2.0.0";
|
|
105
|
+
return kind === "failed" ? "5.0.0" : "4.0.0";
|
|
106
|
+
}
|
|
107
|
+
function originalRecipient(orcpt) {
|
|
108
|
+
const type = orcpt.type.toLowerCase();
|
|
109
|
+
if (type === "rfc822" || type === "utf-8") {
|
|
110
|
+
const clean = cleanAddress(orcpt.address);
|
|
111
|
+
const whole = isAscii(clean) ? clean : escapedAddress(clean);
|
|
112
|
+
return whole.length > MAX_ESCAPED ? undefined : typedAddress(orcpt.address);
|
|
113
|
+
}
|
|
114
|
+
const address = strip(orcpt.address).replace(/\s/g, "");
|
|
115
|
+
if (!/^[A-Za-z0-9-]{1,20}$/.test(orcpt.type) || !/^[\x21-\x7e]{1,900}$/.test(address)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
return `${orcpt.type.toLowerCase()}; ${address}`;
|
|
119
|
+
}
|
|
120
|
+
function recipientFields(report, recipient) {
|
|
121
|
+
const { reply } = recipient;
|
|
122
|
+
const original = recipient.orcpt && originalRecipient(recipient.orcpt);
|
|
123
|
+
const fields = [
|
|
124
|
+
...original ? [["Original-Recipient", original]] : [],
|
|
125
|
+
["Final-Recipient", typedAddress(recipient.address)],
|
|
126
|
+
["Action", report.kind],
|
|
127
|
+
["Status", statusOf(report.kind, reply)]
|
|
128
|
+
];
|
|
129
|
+
const remote = remoteMta(reply?.host);
|
|
130
|
+
if (remote)
|
|
131
|
+
fields.push(["Remote-MTA", remote]);
|
|
132
|
+
if (reply) {
|
|
133
|
+
const text = asciiText(reply.text, 900);
|
|
134
|
+
fields.push(reply.code === undefined ? ["Diagnostic-Code", `X-Bumail; ${text}`] : ["Diagnostic-Code", `smtp; ${reply.code} ${text}`]);
|
|
135
|
+
}
|
|
136
|
+
fields.push(["Last-Attempt-Date", formatDate(recipient.lastAttempt)]);
|
|
137
|
+
if (report.kind === "delayed" && report.willRetryUntil) {
|
|
138
|
+
fields.push(["Will-Retry-Until", formatDate(report.willRetryUntil)]);
|
|
139
|
+
}
|
|
140
|
+
return fields.map(([name, value]) => foldHeader(name, value));
|
|
141
|
+
}
|
|
142
|
+
function deliveryStatus(report) {
|
|
143
|
+
const envid = strip(report.envid ?? "").trim();
|
|
144
|
+
const message = [
|
|
145
|
+
...envid === "" ? [] : [foldHeader("Original-Envelope-Id", envid)],
|
|
146
|
+
foldHeader("Reporting-MTA", `dns; ${strip(report.reportingMta)}`),
|
|
147
|
+
foldHeader("Arrival-Date", formatDate(report.arrival))
|
|
148
|
+
].join(`\r
|
|
149
|
+
`);
|
|
150
|
+
const blocks = report.recipients.map((r) => recipientFields(report, r).join(`\r
|
|
151
|
+
`));
|
|
152
|
+
return `${[message, ...blocks].join(`\r
|
|
153
|
+
\r
|
|
154
|
+
`)}\r
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/dsn/human.ts
|
|
159
|
+
import { formatDate as formatDate2 } from "@bumail/mime";
|
|
160
|
+
var SUBJECTS = {
|
|
161
|
+
failed: "Undelivered Mail Returned to Sender",
|
|
162
|
+
delayed: "Delayed Mail (still being retried)",
|
|
163
|
+
relayed: "Mail Relayed (delivery not confirmed)",
|
|
164
|
+
delivered: "Mail Delivered"
|
|
165
|
+
};
|
|
166
|
+
function opening(input) {
|
|
167
|
+
switch (input.kind) {
|
|
168
|
+
case "failed":
|
|
169
|
+
return [
|
|
170
|
+
"Your message could not be delivered to the recipients below.",
|
|
171
|
+
"This is a permanent failure: the message will not be retried."
|
|
172
|
+
];
|
|
173
|
+
case "delayed":
|
|
174
|
+
return [
|
|
175
|
+
"Your message has not been delivered yet to the recipients below.",
|
|
176
|
+
"This is a warning only: the message is still being retried",
|
|
177
|
+
...input.willRetryUntil ? [`until ${formatDate2(input.willRetryUntil)}.`] : [],
|
|
178
|
+
"You do not need to send it again."
|
|
179
|
+
];
|
|
180
|
+
case "relayed":
|
|
181
|
+
return [
|
|
182
|
+
"Your message was passed on to the recipients below, whose server",
|
|
183
|
+
"does not report delivery status (RFC 3461 §5.2.2).",
|
|
184
|
+
"This server can say no more: no further notice will follow."
|
|
185
|
+
];
|
|
186
|
+
case "delivered":
|
|
187
|
+
return ["Your message was delivered to the recipients below."];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function humanText(input) {
|
|
191
|
+
const lines = [
|
|
192
|
+
`This is the mail system at ${input.reportingMta}.`,
|
|
193
|
+
"",
|
|
194
|
+
...opening(input)
|
|
195
|
+
];
|
|
196
|
+
if (!input.original && input.kind !== "delivered") {
|
|
197
|
+
lines.push("", "Your message could not be read from the queue, so it is not returned.");
|
|
198
|
+
}
|
|
199
|
+
lines.push("");
|
|
200
|
+
for (const r of input.recipients) {
|
|
201
|
+
const said = r.reply ? `${r.reply.code ?? ""} ${cleanText(r.reply.text, 900)}`.trim() : statusOf(input.kind);
|
|
202
|
+
const why = r.reply?.status === "4.4.7" ? `delivery time expired, still failing: ${said}` : said;
|
|
203
|
+
lines.push(`<${cleanText(r.address, 254)}>: ${why}`);
|
|
204
|
+
}
|
|
205
|
+
return `${lines.join(`\r
|
|
206
|
+
`)}\r
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/dsn/build.ts
|
|
211
|
+
var encoder = new TextEncoder;
|
|
212
|
+
var strip2 = (text) => replaceControls(text, "").replace(/[\s<>]/g, "");
|
|
213
|
+
function headerFields(input, boundary) {
|
|
214
|
+
const subject = SUBJECTS[input.kind];
|
|
215
|
+
const domain = strip2(input.reportingMta);
|
|
216
|
+
return [
|
|
217
|
+
foldHeader2("Date", formatDate3(input.date)),
|
|
218
|
+
foldHeader2("From", `Mail Delivery System <${strip2(input.from)}>`),
|
|
219
|
+
foldHeader2("To", `<${strip2(input.to)}>`),
|
|
220
|
+
foldHeader2("Subject", encodeHeaderValue(subject)),
|
|
221
|
+
foldHeader2("Message-ID", `<${crypto.randomUUID()}@${domain}>`),
|
|
222
|
+
foldHeader2("Auto-Submitted", "auto-replied"),
|
|
223
|
+
foldHeader2("MIME-Version", "1.0"),
|
|
224
|
+
foldHeader2("Content-Type", `multipart/report; report-type=delivery-status; boundary="${boundary}"`)
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
var concat = (parts) => {
|
|
228
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
229
|
+
let at = 0;
|
|
230
|
+
for (const part of parts) {
|
|
231
|
+
out.set(part, at);
|
|
232
|
+
at += part.length;
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
};
|
|
236
|
+
function boundaryFor(content) {
|
|
237
|
+
const text = new TextDecoder("latin1").decode(content);
|
|
238
|
+
for (;; ) {
|
|
239
|
+
const boundary = `=_dsn_${crypto.randomUUID()}`;
|
|
240
|
+
if (!text.includes(boundary))
|
|
241
|
+
return boundary;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function buildDsn(input) {
|
|
245
|
+
const original = input.original ? returned(input.original, input.returnContent, input.maxReturn) : undefined;
|
|
246
|
+
const boundary = boundaryFor(original?.body ?? new Uint8Array);
|
|
247
|
+
const text = encodeQuotedPrintable(humanText(input));
|
|
248
|
+
const status = deliveryStatus({
|
|
249
|
+
kind: input.kind,
|
|
250
|
+
reportingMta: input.reportingMta,
|
|
251
|
+
arrival: input.arrival,
|
|
252
|
+
...input.envid === undefined ? {} : { envid: input.envid },
|
|
253
|
+
recipients: input.recipients,
|
|
254
|
+
...input.willRetryUntil ? { willRetryUntil: input.willRetryUntil } : {}
|
|
255
|
+
});
|
|
256
|
+
const parts = [
|
|
257
|
+
...headerFields(input, boundary),
|
|
258
|
+
"",
|
|
259
|
+
"This is a MIME-formatted delivery status notification.",
|
|
260
|
+
"",
|
|
261
|
+
`--${boundary}`,
|
|
262
|
+
"Content-Type: text/plain; charset=utf-8",
|
|
263
|
+
"Content-Transfer-Encoding: quoted-printable",
|
|
264
|
+
"",
|
|
265
|
+
text,
|
|
266
|
+
`--${boundary}`,
|
|
267
|
+
"Content-Type: message/delivery-status",
|
|
268
|
+
"",
|
|
269
|
+
status
|
|
270
|
+
];
|
|
271
|
+
const end = `--${boundary}--\r
|
|
272
|
+
`;
|
|
273
|
+
if (!original)
|
|
274
|
+
return encoder.encode(`${parts.join(`\r
|
|
275
|
+
`)}\r
|
|
276
|
+
${end}`);
|
|
277
|
+
const head = [
|
|
278
|
+
...parts,
|
|
279
|
+
`--${boundary}`,
|
|
280
|
+
`Content-Type: ${original.type}`,
|
|
281
|
+
...original.eightBit ? ["Content-Transfer-Encoding: 8bit"] : [],
|
|
282
|
+
"",
|
|
283
|
+
""
|
|
284
|
+
].join(`\r
|
|
285
|
+
`);
|
|
286
|
+
return concat([
|
|
287
|
+
encoder.encode(head),
|
|
288
|
+
original.body,
|
|
289
|
+
encoder.encode(`\r
|
|
290
|
+
${end}`)
|
|
291
|
+
]);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/dsn/policy.ts
|
|
295
|
+
function asked(kind, notify) {
|
|
296
|
+
if (notify === "NEVER")
|
|
297
|
+
return false;
|
|
298
|
+
if (notify === undefined)
|
|
299
|
+
return kind === "failed" || kind === "delayed";
|
|
300
|
+
const event = {
|
|
301
|
+
failed: "FAILURE",
|
|
302
|
+
delayed: "DELAY",
|
|
303
|
+
relayed: "SUCCESS",
|
|
304
|
+
delivered: "SUCCESS"
|
|
305
|
+
};
|
|
306
|
+
return notify.includes(event[kind]);
|
|
307
|
+
}
|
|
308
|
+
var returnOf = (ret, fallback) => ret === "HDRS" ? "headers" : ret === "FULL" ? "full" : fallback;
|
|
309
|
+
|
|
310
|
+
// src/dsn/delivered.ts
|
|
311
|
+
function deliveredDsn(input) {
|
|
312
|
+
const wanted = input.recipients.filter((r) => asked("delivered", r.notify));
|
|
313
|
+
if (input.to === "" || wanted.length === 0)
|
|
314
|
+
return;
|
|
315
|
+
const date = input.date ?? new Date;
|
|
316
|
+
return buildDsn({
|
|
317
|
+
kind: "delivered",
|
|
318
|
+
reportingMta: input.reportingMta,
|
|
319
|
+
from: input.from,
|
|
320
|
+
to: input.to,
|
|
321
|
+
arrival: input.arrival,
|
|
322
|
+
date,
|
|
323
|
+
...input.envid === undefined ? {} : { envid: input.envid },
|
|
324
|
+
recipients: wanted.map((r) => ({
|
|
325
|
+
address: r.address,
|
|
326
|
+
lastAttempt: date,
|
|
327
|
+
...r.orcpt ? { orcpt: r.orcpt } : {}
|
|
328
|
+
})),
|
|
329
|
+
...input.original ? { original: input.original } : {},
|
|
330
|
+
returnContent: "headers",
|
|
331
|
+
maxReturn: input.maxReturn ?? 64 * 1024
|
|
332
|
+
});
|
|
333
|
+
}
|
|
10
334
|
// src/queue/envelope.ts
|
|
11
335
|
import { isMailbox } from "@bumail/smtp/client";
|
|
12
336
|
var ADDRESS = /^[^\s<>]{1,64}@[^\s<>@]{1,253}$/u;
|
|
@@ -39,15 +363,27 @@ function checkEnvelope(envelope, maxRecipients) {
|
|
|
39
363
|
if (to.length > maxRecipients) {
|
|
40
364
|
throw new QueueError2("TOO_MANY_RECIPIENTS", `The message has ${to.length} recipients; the limit is ${maxRecipients}`);
|
|
41
365
|
}
|
|
42
|
-
|
|
366
|
+
const keyOf = (a) => `${a.slice(0, a.lastIndexOf("@"))}@${domainOf(a)}`;
|
|
367
|
+
const kept = new Map(to.map((a) => [keyOf(a), a]));
|
|
368
|
+
const recipients = Array.isArray(envelope.recipients) ? envelope.recipients.map((r) => {
|
|
369
|
+
const target = typeof r?.address === "string" && list.includes(r.address) ? kept.get(keyOf(r.address)) : undefined;
|
|
370
|
+
return target === undefined ? r : { ...r, address: target };
|
|
371
|
+
}).filter((r, i, all) => all.findIndex((o) => o?.address === r?.address) === i) : envelope.recipients;
|
|
372
|
+
return {
|
|
373
|
+
from,
|
|
374
|
+
to,
|
|
375
|
+
...envelope.ret === undefined ? {} : { ret: envelope.ret },
|
|
376
|
+
...envelope.envid === undefined ? {} : { envid: envelope.envid },
|
|
377
|
+
...recipients === undefined ? {} : { recipients }
|
|
378
|
+
};
|
|
43
379
|
}
|
|
44
380
|
var tooBig = (max) => new QueueError2("MESSAGE_TOO_BIG", `The message is larger than the limit of ${max} bytes`);
|
|
45
|
-
var
|
|
46
|
-
var
|
|
381
|
+
var CR2 = 13;
|
|
382
|
+
var LF2 = 10;
|
|
47
383
|
function checkLineEnds(bytes) {
|
|
48
384
|
for (let i = 0;i < bytes.length; i++) {
|
|
49
385
|
const byte = bytes[i];
|
|
50
|
-
const bare = byte ===
|
|
386
|
+
const bare = byte === CR2 ? bytes[i + 1] !== LF2 : byte === LF2 && (i === 0 || bytes[i - 1] !== CR2);
|
|
51
387
|
if (bare) {
|
|
52
388
|
throw invalid(`The message has a bare CR or LF at byte ${i}: every line must end in CRLF`);
|
|
53
389
|
}
|
|
@@ -337,7 +673,8 @@ function outcomesOf(result, group, max) {
|
|
|
337
673
|
updates.set(recipient, {
|
|
338
674
|
address: recipient,
|
|
339
675
|
status: "delivered",
|
|
340
|
-
reply: diagnosticOf(result.reply, max, host)
|
|
676
|
+
reply: diagnosticOf(result.reply, max, host),
|
|
677
|
+
...result.dsnOffered === false ? { dsnOffered: false } : {}
|
|
341
678
|
});
|
|
342
679
|
}
|
|
343
680
|
for (const { recipient, reply } of result.rejected) {
|
|
@@ -403,6 +740,19 @@ function routeOf(settings, domain) {
|
|
|
403
740
|
function hostOf(route) {
|
|
404
741
|
return route === "mx" ? undefined : route.host;
|
|
405
742
|
}
|
|
743
|
+
function dsnOptionsOf(item, group) {
|
|
744
|
+
const only = new Set(group);
|
|
745
|
+
const recipients = item.recipients.filter((r) => only.has(r.address) && (r.notify !== undefined || r.orcpt !== undefined)).map((r) => ({
|
|
746
|
+
address: r.address,
|
|
747
|
+
...r.notify === undefined ? {} : { notify: r.notify },
|
|
748
|
+
...r.orcpt === undefined ? {} : { orcpt: r.orcpt }
|
|
749
|
+
}));
|
|
750
|
+
return {
|
|
751
|
+
...item.ret === undefined ? {} : { ret: item.ret },
|
|
752
|
+
...item.envid === undefined ? {} : { envid: item.envid },
|
|
753
|
+
...recipients.length === 0 ? {} : { recipients }
|
|
754
|
+
};
|
|
755
|
+
}
|
|
406
756
|
function sendOptionsOf(settings, domain, from, to, override) {
|
|
407
757
|
const { options, hostname } = settings;
|
|
408
758
|
const common = {
|
|
@@ -436,44 +786,89 @@ function sendOptionsOf(settings, domain, from, to, override) {
|
|
|
436
786
|
};
|
|
437
787
|
}
|
|
438
788
|
|
|
439
|
-
// src/queue/
|
|
440
|
-
var
|
|
441
|
-
|
|
442
|
-
status: "failed",
|
|
443
|
-
reply: {
|
|
444
|
-
status: "5.1.3",
|
|
445
|
-
text: "The address is not one SMTP can carry"
|
|
446
|
-
}
|
|
447
|
-
});
|
|
448
|
-
var marked = (u) => u.status === "delivered" ? u : { ...u, mayHaveReachedMx: true };
|
|
449
|
-
function doubtful(updates, error) {
|
|
789
|
+
// src/queue/session.ts
|
|
790
|
+
var marked = (u, key) => u.status === "delivered" ? u : { ...u, [key]: true };
|
|
791
|
+
function doubtful(updates, error, key) {
|
|
450
792
|
if (!error.accepted)
|
|
451
|
-
return updates.map(marked);
|
|
793
|
+
return updates.map((u) => marked(u, key));
|
|
452
794
|
const taken = new Set(error.accepted.map((r) => r.recipient));
|
|
453
795
|
const refused = new Set((error.rejected ?? []).map((r) => r.recipient).filter((a) => !taken.has(a)));
|
|
454
|
-
return updates.map((u) => refused.has(u.address) ? u : marked(u));
|
|
796
|
+
return updates.map((u) => refused.has(u.address) ? u : marked(u, key));
|
|
455
797
|
}
|
|
456
798
|
async function session(ctx, item, message, domain, group, route, override) {
|
|
457
799
|
const { settings } = ctx;
|
|
458
800
|
const max = settings.limits.maxReplyText;
|
|
459
801
|
try {
|
|
460
|
-
const options =
|
|
802
|
+
const options = {
|
|
803
|
+
...sendOptionsOf(settings, domain, item.from, group, override),
|
|
804
|
+
...dsnOptionsOf(item, group)
|
|
805
|
+
};
|
|
461
806
|
const result = await settings.send(message, options);
|
|
462
807
|
return { updates: outcomesOf(result, group, max) };
|
|
463
808
|
} catch (error) {
|
|
464
809
|
if (isRouteError(error))
|
|
465
810
|
ctx.events.emit("error", { error, id: item.id });
|
|
466
811
|
const updates = outcomesOfError(error, group, max, hostOf(route));
|
|
467
|
-
const
|
|
468
|
-
|
|
812
|
+
const key = route === "mx" ? "mayHaveReachedMx" : "mayHaveReachedFallback";
|
|
813
|
+
const doubt = (route === "mx" || override !== undefined) && isSmtpError(error) && error.afterData;
|
|
814
|
+
return { updates: doubt ? doubtful(updates, error, key) : updates, error };
|
|
469
815
|
}
|
|
470
816
|
}
|
|
817
|
+
|
|
818
|
+
// src/queue/fallback-session.ts
|
|
471
819
|
function merged(mx, fb) {
|
|
472
820
|
if (mx.status === "deferred" && fb.status === "failed") {
|
|
473
821
|
return { ...fb, status: "deferred" };
|
|
474
822
|
}
|
|
475
823
|
return fb;
|
|
476
824
|
}
|
|
825
|
+
var stillHeld = (u) => u.status === "failed" ? { ...u, status: "deferred" } : u;
|
|
826
|
+
async function fallBackAtOnce(ctx, item, message, domain, first, fallback, used) {
|
|
827
|
+
const mxHeld = new Set(item.recipients.filter((r) => r.mayHaveReachedMx).map((r) => r.address));
|
|
828
|
+
const mx = new Map(first.updates.filter((u) => u.status !== "delivered" && !u.mayHaveReachedMx && !mxHeld.has(u.address) && fallsBack(u.reply, first.error)).map((u) => [u.address, u]));
|
|
829
|
+
if (mx.size === 0)
|
|
830
|
+
return first.updates;
|
|
831
|
+
const second = await session(ctx, item, message, domain, [...mx.keys()], fallback, fallback);
|
|
832
|
+
for (const [address, u] of mx) {
|
|
833
|
+
used.set(address, {
|
|
834
|
+
route: "smarthost",
|
|
835
|
+
fallback: true,
|
|
836
|
+
...u.reply ? { mxReply: u.reply } : {}
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
return [
|
|
840
|
+
...first.updates.filter((u) => !mx.has(u.address)),
|
|
841
|
+
...second.updates.map((u) => {
|
|
842
|
+
const was = mx.get(u.address);
|
|
843
|
+
return was ? merged(was, u) : u;
|
|
844
|
+
})
|
|
845
|
+
];
|
|
846
|
+
}
|
|
847
|
+
function heldAtFallback(item, group, byMx, fallback) {
|
|
848
|
+
if (!byMx || fallback === undefined)
|
|
849
|
+
return [];
|
|
850
|
+
const held = new Set(item.recipients.filter((r) => r.mayHaveReachedFallback && !r.mayHaveReachedMx).map((r) => r.address));
|
|
851
|
+
return group.filter((address) => held.has(address));
|
|
852
|
+
}
|
|
853
|
+
async function deliverHeld(ctx, item, message, domain, held, fallback, used) {
|
|
854
|
+
if (held.length === 0)
|
|
855
|
+
return [];
|
|
856
|
+
for (const address of held) {
|
|
857
|
+
used.set(address, { route: "smarthost", fallback: true });
|
|
858
|
+
}
|
|
859
|
+
const second = await session(ctx, item, message, domain, held, fallback, fallback);
|
|
860
|
+
return second.updates.map(stillHeld);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/queue/attempt.ts
|
|
864
|
+
var badAddress = (address) => ({
|
|
865
|
+
address,
|
|
866
|
+
status: "failed",
|
|
867
|
+
reply: {
|
|
868
|
+
status: "5.1.3",
|
|
869
|
+
text: "The address is not one SMTP can carry"
|
|
870
|
+
}
|
|
871
|
+
});
|
|
477
872
|
async function attemptGroup(ctx, item, message, domain, recipients, used) {
|
|
478
873
|
const { settings } = ctx;
|
|
479
874
|
const bad = recipients.filter((address) => !isMailbox2(address));
|
|
@@ -484,267 +879,16 @@ async function attemptGroup(ctx, item, message, domain, recipients, used) {
|
|
|
484
879
|
const label = route === "mx" ? "mx" : "smarthost";
|
|
485
880
|
for (const address of group)
|
|
486
881
|
used.set(address, { route: label });
|
|
487
|
-
const first = await session(ctx, item, message, domain, group, route);
|
|
488
|
-
let updates = first.updates;
|
|
489
882
|
const { fallback } = settings.options;
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
fallback: true,
|
|
500
|
-
...u.reply ? { mxReply: u.reply } : {}
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
updates = [
|
|
504
|
-
...updates.filter((u) => !mx.has(u.address)),
|
|
505
|
-
...second.updates.map((u) => {
|
|
506
|
-
const was = mx.get(u.address);
|
|
507
|
-
return was ? merged(was, u) : u;
|
|
508
|
-
})
|
|
509
|
-
];
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return [...updates, ...bad.map(badAddress)];
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// src/dsn/build.ts
|
|
516
|
-
import {
|
|
517
|
-
encodeHeaderValue,
|
|
518
|
-
encodeQuotedPrintable,
|
|
519
|
-
foldHeader as foldHeader2,
|
|
520
|
-
formatDate as formatDate2
|
|
521
|
-
} from "@bumail/mime";
|
|
522
|
-
|
|
523
|
-
// src/dsn/content.ts
|
|
524
|
-
var CR2 = 13;
|
|
525
|
-
var LF2 = 10;
|
|
526
|
-
var MAX_LINE = 998;
|
|
527
|
-
var isContinuation = (byte) => byte !== undefined && byte >= 128 && byte < 192;
|
|
528
|
-
function crlf(bytes) {
|
|
529
|
-
const out = [];
|
|
530
|
-
let column = 0;
|
|
531
|
-
for (let i = 0;i < bytes.length; i++) {
|
|
532
|
-
const byte = bytes[i];
|
|
533
|
-
if (byte === CR2 || byte === LF2) {
|
|
534
|
-
out.push(CR2, LF2);
|
|
535
|
-
column = 0;
|
|
536
|
-
if (byte === CR2 && bytes[i + 1] === LF2)
|
|
537
|
-
i++;
|
|
538
|
-
} else if (column < MAX_LINE) {
|
|
539
|
-
out.push(byte);
|
|
540
|
-
column++;
|
|
541
|
-
} else if (column === MAX_LINE) {
|
|
542
|
-
if (isContinuation(byte)) {
|
|
543
|
-
while (isContinuation(out.at(-1))) {
|
|
544
|
-
out.pop();
|
|
545
|
-
}
|
|
546
|
-
if ((out.at(-1) ?? 0) >= 192)
|
|
547
|
-
out.pop();
|
|
548
|
-
}
|
|
549
|
-
column++;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
return Uint8Array.from(out);
|
|
553
|
-
}
|
|
554
|
-
function headerEnd(bytes) {
|
|
555
|
-
for (let i = 0;i + 3 < bytes.length; i++) {
|
|
556
|
-
if (bytes[i] === CR2 && bytes[i + 1] === LF2 && bytes[i + 2] === CR2 && bytes[i + 3] === LF2) {
|
|
557
|
-
return i + 2;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
return bytes.length;
|
|
561
|
-
}
|
|
562
|
-
function wholeLines(bytes, max) {
|
|
563
|
-
if (bytes.length <= max)
|
|
564
|
-
return bytes;
|
|
565
|
-
const cut = bytes.subarray(0, max).lastIndexOf(LF2);
|
|
566
|
-
return bytes.subarray(0, cut + 1);
|
|
567
|
-
}
|
|
568
|
-
function returned(original, mode, max) {
|
|
569
|
-
const message = crlf(original.subarray(0, max * 2 + 2));
|
|
570
|
-
const whole = original.length <= max && message.length <= max;
|
|
571
|
-
const body = whole && mode === "full" ? message : wholeLines(message.subarray(0, headerEnd(message)), max);
|
|
572
|
-
return {
|
|
573
|
-
type: whole && mode === "full" ? "message/rfc822" : "text/rfc822-headers",
|
|
574
|
-
body,
|
|
575
|
-
eightBit: body.some((byte) => byte > 127)
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// src/dsn/fields.ts
|
|
580
|
-
import { foldHeader, formatDate } from "@bumail/mime";
|
|
581
|
-
var strip = (text) => replaceControls(text, "");
|
|
582
|
-
var MAX_ESCAPED = 900;
|
|
583
|
-
function typedAddress(address) {
|
|
584
|
-
const clean = strip(address).replace(/[\s\\]/g, "");
|
|
585
|
-
if (/^[\x21-\x7e]*$/.test(clean))
|
|
586
|
-
return `rfc822; ${clean}`;
|
|
587
|
-
let escaped = "";
|
|
588
|
-
for (const c of clean) {
|
|
589
|
-
const piece = /[\x21-\x7e]/.test(c) ? c : `\\x{${(c.codePointAt(0) ?? 0).toString(16).toUpperCase()}}`;
|
|
590
|
-
if (escaped.length + piece.length > MAX_ESCAPED)
|
|
591
|
-
break;
|
|
592
|
-
escaped += piece;
|
|
593
|
-
}
|
|
594
|
-
return `utf-8; ${escaped}`;
|
|
595
|
-
}
|
|
596
|
-
var remoteMta = (host) => host && /^[A-Za-z0-9.:[\]-]{1,255}$/.test(host) ? `dns; ${host}` : undefined;
|
|
597
|
-
function statusOf(kind, reply) {
|
|
598
|
-
if (reply?.status)
|
|
599
|
-
return reply.status;
|
|
600
|
-
if (reply?.code !== undefined) {
|
|
601
|
-
return reply.code >= 500 ? "5.0.0" : "4.0.0";
|
|
602
|
-
}
|
|
603
|
-
return kind === "failed" ? "5.0.0" : "4.0.0";
|
|
604
|
-
}
|
|
605
|
-
function recipientFields(report, recipient) {
|
|
606
|
-
const { reply } = recipient;
|
|
607
|
-
const fields = [
|
|
608
|
-
["Final-Recipient", typedAddress(recipient.address)],
|
|
609
|
-
["Action", report.kind],
|
|
610
|
-
["Status", statusOf(report.kind, reply)]
|
|
611
|
-
];
|
|
612
|
-
const remote = remoteMta(reply?.host);
|
|
613
|
-
if (remote)
|
|
614
|
-
fields.push(["Remote-MTA", remote]);
|
|
615
|
-
if (reply) {
|
|
616
|
-
const text = asciiText(reply.text, 900);
|
|
617
|
-
fields.push(reply.code === undefined ? ["Diagnostic-Code", `X-Bumail; ${text}`] : ["Diagnostic-Code", `smtp; ${reply.code} ${text}`]);
|
|
618
|
-
}
|
|
619
|
-
fields.push(["Last-Attempt-Date", formatDate(recipient.lastAttempt)]);
|
|
620
|
-
if (report.kind === "delayed" && report.willRetryUntil) {
|
|
621
|
-
fields.push(["Will-Retry-Until", formatDate(report.willRetryUntil)]);
|
|
622
|
-
}
|
|
623
|
-
return fields.map(([name, value]) => foldHeader(name, value));
|
|
624
|
-
}
|
|
625
|
-
function deliveryStatus(report) {
|
|
626
|
-
const message = [
|
|
627
|
-
foldHeader("Reporting-MTA", `dns; ${strip(report.reportingMta)}`),
|
|
628
|
-
foldHeader("Arrival-Date", formatDate(report.arrival))
|
|
629
|
-
].join(`\r
|
|
630
|
-
`);
|
|
631
|
-
const blocks = report.recipients.map((r) => recipientFields(report, r).join(`\r
|
|
632
|
-
`));
|
|
633
|
-
return `${[message, ...blocks].join(`\r
|
|
634
|
-
\r
|
|
635
|
-
`)}\r
|
|
636
|
-
`;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// src/dsn/build.ts
|
|
640
|
-
var encoder = new TextEncoder;
|
|
641
|
-
var strip2 = (text) => replaceControls(text, "").replace(/[\s<>]/g, "");
|
|
642
|
-
function humanText(input) {
|
|
643
|
-
const failed = input.kind === "failed";
|
|
644
|
-
const lines = failed ? [
|
|
645
|
-
`This is the mail system at ${input.reportingMta}.`,
|
|
646
|
-
"",
|
|
647
|
-
"Your message could not be delivered to the recipients below.",
|
|
648
|
-
"This is a permanent failure: the message will not be retried."
|
|
649
|
-
] : [
|
|
650
|
-
`This is the mail system at ${input.reportingMta}.`,
|
|
651
|
-
"",
|
|
652
|
-
"Your message has not been delivered yet to the recipients below.",
|
|
653
|
-
"This is a warning only: the message is still being retried",
|
|
654
|
-
...input.willRetryUntil ? [`until ${formatDate2(input.willRetryUntil)}.`] : [],
|
|
655
|
-
"You do not need to send it again."
|
|
656
|
-
];
|
|
657
|
-
if (!input.original) {
|
|
658
|
-
lines.push("", "Your message could not be read from the queue, so it is not returned.");
|
|
659
|
-
}
|
|
660
|
-
lines.push("");
|
|
661
|
-
for (const r of input.recipients) {
|
|
662
|
-
const said = r.reply ? `${r.reply.code ?? ""} ${cleanText(r.reply.text, 900)}`.trim() : statusOf(input.kind);
|
|
663
|
-
const why = r.reply?.status === "4.4.7" ? `delivery time expired, still failing: ${said}` : said;
|
|
664
|
-
lines.push(`<${cleanText(r.address, 254)}>: ${why}`);
|
|
665
|
-
}
|
|
666
|
-
return `${lines.join(`\r
|
|
667
|
-
`)}\r
|
|
668
|
-
`;
|
|
669
|
-
}
|
|
670
|
-
function headerFields(input, boundary) {
|
|
671
|
-
const subject = input.kind === "failed" ? "Undelivered Mail Returned to Sender" : "Delayed Mail (still being retried)";
|
|
672
|
-
const domain = strip2(input.reportingMta);
|
|
673
|
-
return [
|
|
674
|
-
foldHeader2("Date", formatDate2(input.date)),
|
|
675
|
-
foldHeader2("From", `Mail Delivery System <${strip2(input.from)}>`),
|
|
676
|
-
foldHeader2("To", `<${strip2(input.to)}>`),
|
|
677
|
-
foldHeader2("Subject", encodeHeaderValue(subject)),
|
|
678
|
-
foldHeader2("Message-ID", `<${crypto.randomUUID()}@${domain}>`),
|
|
679
|
-
foldHeader2("Auto-Submitted", "auto-replied"),
|
|
680
|
-
foldHeader2("MIME-Version", "1.0"),
|
|
681
|
-
foldHeader2("Content-Type", `multipart/report; report-type=delivery-status; boundary="${boundary}"`)
|
|
682
|
-
];
|
|
683
|
-
}
|
|
684
|
-
var concat = (parts) => {
|
|
685
|
-
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
686
|
-
let at = 0;
|
|
687
|
-
for (const part of parts) {
|
|
688
|
-
out.set(part, at);
|
|
689
|
-
at += part.length;
|
|
690
|
-
}
|
|
691
|
-
return out;
|
|
692
|
-
};
|
|
693
|
-
function boundaryFor(content) {
|
|
694
|
-
const text = new TextDecoder("latin1").decode(content);
|
|
695
|
-
for (;; ) {
|
|
696
|
-
const boundary = `=_dsn_${crypto.randomUUID()}`;
|
|
697
|
-
if (!text.includes(boundary))
|
|
698
|
-
return boundary;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
function buildDsn(input) {
|
|
702
|
-
const original = input.original ? returned(input.original, input.returnContent, input.maxReturn) : undefined;
|
|
703
|
-
const boundary = boundaryFor(original?.body ?? new Uint8Array);
|
|
704
|
-
const text = encodeQuotedPrintable(humanText(input));
|
|
705
|
-
const status = deliveryStatus({
|
|
706
|
-
kind: input.kind,
|
|
707
|
-
reportingMta: input.reportingMta,
|
|
708
|
-
arrival: input.arrival,
|
|
709
|
-
recipients: input.recipients,
|
|
710
|
-
...input.willRetryUntil ? { willRetryUntil: input.willRetryUntil } : {}
|
|
711
|
-
});
|
|
712
|
-
const parts = [
|
|
713
|
-
...headerFields(input, boundary),
|
|
714
|
-
"",
|
|
715
|
-
"This is a MIME-formatted delivery status notification.",
|
|
716
|
-
"",
|
|
717
|
-
`--${boundary}`,
|
|
718
|
-
"Content-Type: text/plain; charset=utf-8",
|
|
719
|
-
"Content-Transfer-Encoding: quoted-printable",
|
|
720
|
-
"",
|
|
721
|
-
text,
|
|
722
|
-
`--${boundary}`,
|
|
723
|
-
"Content-Type: message/delivery-status",
|
|
724
|
-
"",
|
|
725
|
-
status
|
|
726
|
-
];
|
|
727
|
-
const end = `--${boundary}--\r
|
|
728
|
-
`;
|
|
729
|
-
if (!original)
|
|
730
|
-
return encoder.encode(`${parts.join(`\r
|
|
731
|
-
`)}\r
|
|
732
|
-
${end}`);
|
|
733
|
-
const head = [
|
|
734
|
-
...parts,
|
|
735
|
-
`--${boundary}`,
|
|
736
|
-
`Content-Type: ${original.type}`,
|
|
737
|
-
...original.eightBit ? ["Content-Transfer-Encoding: 8bit"] : [],
|
|
738
|
-
"",
|
|
739
|
-
""
|
|
740
|
-
].join(`\r
|
|
741
|
-
`);
|
|
742
|
-
return concat([
|
|
743
|
-
encoder.encode(head),
|
|
744
|
-
original.body,
|
|
745
|
-
encoder.encode(`\r
|
|
746
|
-
${end}`)
|
|
747
|
-
]);
|
|
883
|
+
const held = heldAtFallback(item, group, route === "mx", fallback);
|
|
884
|
+
const kept = fallback === undefined ? [] : await deliverHeld(ctx, item, message, domain, held, fallback, used);
|
|
885
|
+
const viaMx = group.filter((address) => !held.includes(address));
|
|
886
|
+
let updates = [];
|
|
887
|
+
if (viaMx.length > 0) {
|
|
888
|
+
const first = await session(ctx, item, message, domain, viaMx, route);
|
|
889
|
+
updates = fallback !== undefined && route === "mx" ? await fallBackAtOnce(ctx, item, message, domain, first, fallback, used) : first.updates;
|
|
890
|
+
}
|
|
891
|
+
return [...kept, ...updates, ...bad.map(badAddress)];
|
|
748
892
|
}
|
|
749
893
|
|
|
750
894
|
// src/queue/retry.ts
|
|
@@ -763,17 +907,24 @@ function nextAttemptAt(createdAt, now, attempts, retry, random) {
|
|
|
763
907
|
|
|
764
908
|
// src/queue/notify.ts
|
|
765
909
|
var MAY_HAVE_REACHED = "An earlier attempt may have reached the receiving server, so the message may have been delivered.";
|
|
910
|
+
var MAY_HAVE_REACHED_FALLBACK = "An earlier attempt may have reached the fallback smarthost, so the message may have been delivered.";
|
|
766
911
|
function reported(r, kind) {
|
|
767
|
-
|
|
912
|
+
const said = [
|
|
913
|
+
...r.mayHaveReachedMx ? [MAY_HAVE_REACHED] : [],
|
|
914
|
+
...r.mayHaveReachedFallback ? [MAY_HAVE_REACHED_FALLBACK] : []
|
|
915
|
+
];
|
|
916
|
+
if (kind !== "failed" || said.length === 0)
|
|
768
917
|
return r.reply;
|
|
769
918
|
const reply = r.reply ?? { text: "" };
|
|
770
919
|
return {
|
|
771
920
|
...reply,
|
|
772
|
-
text: reply.text
|
|
921
|
+
text: [reply.text, ...said].filter((t) => t !== "").join(" ")
|
|
773
922
|
};
|
|
774
923
|
}
|
|
775
924
|
async function notify(ctx, item, message, kind, recipients, now) {
|
|
776
|
-
|
|
925
|
+
const states = new Map(item.recipients.map((r) => [r.address, r]));
|
|
926
|
+
const wanted = recipients.filter((r) => asked(kind, states.get(r.address)?.notify));
|
|
927
|
+
if (item.from === "" || wanted.length === 0)
|
|
777
928
|
return;
|
|
778
929
|
const { settings, store, events } = ctx;
|
|
779
930
|
try {
|
|
@@ -785,13 +936,20 @@ async function notify(ctx, item, message, kind, recipients, now) {
|
|
|
785
936
|
to: item.from,
|
|
786
937
|
arrival: new Date(item.createdAt),
|
|
787
938
|
date: lastAttempt,
|
|
788
|
-
|
|
939
|
+
...item.envid === undefined ? {} : { envid: item.envid },
|
|
940
|
+
recipients: wanted.map((r) => {
|
|
789
941
|
const reply = reported(r, kind);
|
|
790
|
-
|
|
942
|
+
const orcpt = states.get(r.address)?.orcpt;
|
|
943
|
+
return {
|
|
944
|
+
address: r.address,
|
|
945
|
+
...reply ? { reply } : {},
|
|
946
|
+
lastAttempt,
|
|
947
|
+
...orcpt ? { orcpt } : {}
|
|
948
|
+
};
|
|
791
949
|
}),
|
|
792
950
|
...kind === "delayed" ? { willRetryUntil: new Date(giveUpAt(item.createdAt, settings.retry)) } : {},
|
|
793
951
|
...message ? { original: message } : {},
|
|
794
|
-
returnContent: settings.dsn.returnContent,
|
|
952
|
+
returnContent: kind === "relayed" ? "headers" : returnOf(item.ret, settings.dsn.returnContent),
|
|
795
953
|
maxReturn: settings.limits.maxDsnReturn
|
|
796
954
|
});
|
|
797
955
|
const added = await store.add({
|
|
@@ -805,7 +963,7 @@ async function notify(ctx, item, message, kind, recipients, now) {
|
|
|
805
963
|
id: added.id,
|
|
806
964
|
of: item.id,
|
|
807
965
|
to: item.from,
|
|
808
|
-
recipients:
|
|
966
|
+
recipients: wanted.map((r) => r.address)
|
|
809
967
|
});
|
|
810
968
|
} catch (error) {
|
|
811
969
|
events.emit("error", { error, id: item.id });
|
|
@@ -818,17 +976,27 @@ function expired(update) {
|
|
|
818
976
|
address: update.address,
|
|
819
977
|
status: "failed",
|
|
820
978
|
reply: { text: "", ...update.reply, status: "4.4.7" },
|
|
821
|
-
...update.mayHaveReachedMx ? { mayHaveReachedMx: true } : {}
|
|
979
|
+
...update.mayHaveReachedMx ? { mayHaveReachedMx: true } : {},
|
|
980
|
+
...update.mayHaveReachedFallback ? { mayHaveReachedFallback: true } : {}
|
|
822
981
|
};
|
|
823
982
|
}
|
|
824
983
|
function settle(item, updates, skipped, now, settings) {
|
|
825
984
|
const attempts = item.attempts + (updates.length > 0 ? 1 : 0);
|
|
826
985
|
const next = nextAttemptAt(item.createdAt, now, attempts, settings.retry, settings.random);
|
|
827
|
-
const
|
|
828
|
-
const all = updates.map((u) =>
|
|
986
|
+
const was = new Map(item.recipients.map((r) => [r.address, r]));
|
|
987
|
+
const all = updates.map((u) => {
|
|
988
|
+
const r = was.get(u.address);
|
|
989
|
+
if (!r || u.status === "delivered")
|
|
990
|
+
return u;
|
|
991
|
+
return {
|
|
992
|
+
...u,
|
|
993
|
+
...r.mayHaveReachedMx ? { mayHaveReachedMx: true } : {},
|
|
994
|
+
...r.mayHaveReachedFallback ? { mayHaveReachedFallback: true } : {}
|
|
995
|
+
};
|
|
996
|
+
}).map((u) => u.status === "deferred" && next === undefined ? expired(u) : u);
|
|
829
997
|
const deferred = all.filter((u) => u.status === "deferred");
|
|
830
998
|
const { delayAfter } = settings.dsn;
|
|
831
|
-
const delayDsn = !item.delayNotified && item.from !== "" && delayAfter !== false && deferred.length > 0 && now - item.createdAt >= delayAfter;
|
|
999
|
+
const delayDsn = !item.delayNotified && deferred.some((u) => asked("delayed", was.get(u.address)?.notify)) && item.from !== "" && delayAfter !== false && deferred.length > 0 && now - item.createdAt >= delayAfter;
|
|
832
1000
|
return {
|
|
833
1001
|
result: {
|
|
834
1002
|
now,
|
|
@@ -887,7 +1055,10 @@ function emitOutcomes(events, item, settled, used) {
|
|
|
887
1055
|
attempts,
|
|
888
1056
|
...used.get(u.address)
|
|
889
1057
|
});
|
|
890
|
-
const marked = (u) =>
|
|
1058
|
+
const marked = (u) => ({
|
|
1059
|
+
...u.mayHaveReachedMx ? { mayHaveReachedMx: true } : {},
|
|
1060
|
+
...u.mayHaveReachedFallback ? { mayHaveReachedFallback: true } : {}
|
|
1061
|
+
});
|
|
891
1062
|
for (const u of settled.delivered)
|
|
892
1063
|
events.emit("delivered", base(u));
|
|
893
1064
|
for (const u of settled.deferred) {
|
|
@@ -951,6 +1122,8 @@ async function record(ctx, item, updates, skipped, message, used) {
|
|
|
951
1122
|
}
|
|
952
1123
|
emitOutcomes(events, item, settled, used);
|
|
953
1124
|
await notify(ctx, item, message, "failed", settled.failed, now);
|
|
1125
|
+
const relayed = settled.delivered.filter((u) => u.dsnOffered === false);
|
|
1126
|
+
await notify(ctx, item, message, "relayed", relayed, now);
|
|
954
1127
|
if (settled.delayDsn) {
|
|
955
1128
|
await notify(ctx, item, message, "delayed", settled.deferred, now);
|
|
956
1129
|
}
|
|
@@ -1214,10 +1387,10 @@ function createQueue(options) {
|
|
|
1214
1387
|
return {
|
|
1215
1388
|
owner: settings.owner,
|
|
1216
1389
|
async enqueue(message, envelope) {
|
|
1217
|
-
const { from, to } = checkEnvelope(envelope, limits.maxRecipients);
|
|
1390
|
+
const { from, to, ...dsn } = checkEnvelope(envelope, limits.maxRecipients);
|
|
1218
1391
|
const bytes = await readMessage(message, limits.maxMessageSize);
|
|
1219
1392
|
checkLineEnds(bytes);
|
|
1220
|
-
const item = await store.add({ from, to, message: bytes, createdAt: settings.now() }, limits.maxItems === undefined ? {} : { maxItems: limits.maxItems });
|
|
1393
|
+
const item = await store.add({ from, to, message: bytes, createdAt: settings.now(), ...dsn }, limits.maxItems === undefined ? {} : { maxItems: limits.maxItems });
|
|
1221
1394
|
worker.wake();
|
|
1222
1395
|
return item;
|
|
1223
1396
|
},
|
|
@@ -1242,8 +1415,9 @@ function createQueue(options) {
|
|
|
1242
1415
|
}
|
|
1243
1416
|
export {
|
|
1244
1417
|
QueueError2 as QueueError,
|
|
1245
|
-
createQueue
|
|
1418
|
+
createQueue,
|
|
1419
|
+
deliveredDsn
|
|
1246
1420
|
};
|
|
1247
1421
|
|
|
1248
|
-
//# debugId=
|
|
1422
|
+
//# debugId=D24C84F35DEF0E8164756E2164756E21
|
|
1249
1423
|
//# sourceMappingURL=index.js.map
|