@bumail/queue 0.8.0 → 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 +25 -1
- package/dist/chunks/{index-e5wa9f3j.js → index-9yp15arf.js} +91 -21
- 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/storable.d.ts +7 -0
- package/dist/contract/storable.d.ts.map +1 -0
- package/dist/contract/types.d.ts +42 -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 +384 -254
- package/dist/index.js.map +18 -15
- package/dist/memory/index.js +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/envelope.d.ts +24 -4
- package/dist/queue/envelope.d.ts.map +1 -1
- package/dist/queue/events.d.ts +2 -1
- package/dist/queue/events.d.ts.map +1 -1
- 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.map +1 -1
- 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 +72 -3
- package/docs/roadmap.md +8 -3
- package/docs/troubleshooting.md +41 -0
- package/package.json +3 -3
- package/dist/chunks/index-e5wa9f3j.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 = {
|
|
@@ -449,7 +799,10 @@ async function session(ctx, item, message, domain, group, route, override) {
|
|
|
449
799
|
const { settings } = ctx;
|
|
450
800
|
const max = settings.limits.maxReplyText;
|
|
451
801
|
try {
|
|
452
|
-
const options =
|
|
802
|
+
const options = {
|
|
803
|
+
...sendOptionsOf(settings, domain, item.from, group, override),
|
|
804
|
+
...dsnOptionsOf(item, group)
|
|
805
|
+
};
|
|
453
806
|
const result = await settings.send(message, options);
|
|
454
807
|
return { updates: outcomesOf(result, group, max) };
|
|
455
808
|
} catch (error) {
|
|
@@ -538,241 +891,6 @@ async function attemptGroup(ctx, item, message, domain, recipients, used) {
|
|
|
538
891
|
return [...kept, ...updates, ...bad.map(badAddress)];
|
|
539
892
|
}
|
|
540
893
|
|
|
541
|
-
// src/dsn/build.ts
|
|
542
|
-
import {
|
|
543
|
-
encodeHeaderValue,
|
|
544
|
-
encodeQuotedPrintable,
|
|
545
|
-
foldHeader as foldHeader2,
|
|
546
|
-
formatDate as formatDate2
|
|
547
|
-
} from "@bumail/mime";
|
|
548
|
-
|
|
549
|
-
// src/dsn/content.ts
|
|
550
|
-
var CR2 = 13;
|
|
551
|
-
var LF2 = 10;
|
|
552
|
-
var MAX_LINE = 998;
|
|
553
|
-
var isContinuation = (byte) => byte !== undefined && byte >= 128 && byte < 192;
|
|
554
|
-
function crlf(bytes) {
|
|
555
|
-
const out = [];
|
|
556
|
-
let column = 0;
|
|
557
|
-
for (let i = 0;i < bytes.length; i++) {
|
|
558
|
-
const byte = bytes[i];
|
|
559
|
-
if (byte === CR2 || byte === LF2) {
|
|
560
|
-
out.push(CR2, LF2);
|
|
561
|
-
column = 0;
|
|
562
|
-
if (byte === CR2 && bytes[i + 1] === LF2)
|
|
563
|
-
i++;
|
|
564
|
-
} else if (column < MAX_LINE) {
|
|
565
|
-
out.push(byte);
|
|
566
|
-
column++;
|
|
567
|
-
} else if (column === MAX_LINE) {
|
|
568
|
-
if (isContinuation(byte)) {
|
|
569
|
-
while (isContinuation(out.at(-1))) {
|
|
570
|
-
out.pop();
|
|
571
|
-
}
|
|
572
|
-
if ((out.at(-1) ?? 0) >= 192)
|
|
573
|
-
out.pop();
|
|
574
|
-
}
|
|
575
|
-
column++;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
return Uint8Array.from(out);
|
|
579
|
-
}
|
|
580
|
-
function headerEnd(bytes) {
|
|
581
|
-
for (let i = 0;i + 3 < bytes.length; i++) {
|
|
582
|
-
if (bytes[i] === CR2 && bytes[i + 1] === LF2 && bytes[i + 2] === CR2 && bytes[i + 3] === LF2) {
|
|
583
|
-
return i + 2;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
return bytes.length;
|
|
587
|
-
}
|
|
588
|
-
function wholeLines(bytes, max) {
|
|
589
|
-
if (bytes.length <= max)
|
|
590
|
-
return bytes;
|
|
591
|
-
const cut = bytes.subarray(0, max).lastIndexOf(LF2);
|
|
592
|
-
return bytes.subarray(0, cut + 1);
|
|
593
|
-
}
|
|
594
|
-
function returned(original, mode, max) {
|
|
595
|
-
const message = crlf(original.subarray(0, max * 2 + 2));
|
|
596
|
-
const whole = original.length <= max && message.length <= max;
|
|
597
|
-
const body = whole && mode === "full" ? message : wholeLines(message.subarray(0, headerEnd(message)), max);
|
|
598
|
-
return {
|
|
599
|
-
type: whole && mode === "full" ? "message/rfc822" : "text/rfc822-headers",
|
|
600
|
-
body,
|
|
601
|
-
eightBit: body.some((byte) => byte > 127)
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// src/dsn/fields.ts
|
|
606
|
-
import { foldHeader, formatDate } from "@bumail/mime";
|
|
607
|
-
var strip = (text) => replaceControls(text, "");
|
|
608
|
-
var MAX_ESCAPED = 900;
|
|
609
|
-
function typedAddress(address) {
|
|
610
|
-
const clean = strip(address).replace(/[\s\\]/g, "");
|
|
611
|
-
if (/^[\x21-\x7e]*$/.test(clean))
|
|
612
|
-
return `rfc822; ${clean}`;
|
|
613
|
-
let escaped = "";
|
|
614
|
-
for (const c of clean) {
|
|
615
|
-
const piece = /[\x21-\x7e]/.test(c) ? c : `\\x{${(c.codePointAt(0) ?? 0).toString(16).toUpperCase()}}`;
|
|
616
|
-
if (escaped.length + piece.length > MAX_ESCAPED)
|
|
617
|
-
break;
|
|
618
|
-
escaped += piece;
|
|
619
|
-
}
|
|
620
|
-
return `utf-8; ${escaped}`;
|
|
621
|
-
}
|
|
622
|
-
var remoteMta = (host) => host && /^[A-Za-z0-9.:[\]-]{1,255}$/.test(host) ? `dns; ${host}` : undefined;
|
|
623
|
-
function statusOf(kind, reply) {
|
|
624
|
-
if (reply?.status)
|
|
625
|
-
return reply.status;
|
|
626
|
-
if (reply?.code !== undefined) {
|
|
627
|
-
return reply.code >= 500 ? "5.0.0" : "4.0.0";
|
|
628
|
-
}
|
|
629
|
-
return kind === "failed" ? "5.0.0" : "4.0.0";
|
|
630
|
-
}
|
|
631
|
-
function recipientFields(report, recipient) {
|
|
632
|
-
const { reply } = recipient;
|
|
633
|
-
const fields = [
|
|
634
|
-
["Final-Recipient", typedAddress(recipient.address)],
|
|
635
|
-
["Action", report.kind],
|
|
636
|
-
["Status", statusOf(report.kind, reply)]
|
|
637
|
-
];
|
|
638
|
-
const remote = remoteMta(reply?.host);
|
|
639
|
-
if (remote)
|
|
640
|
-
fields.push(["Remote-MTA", remote]);
|
|
641
|
-
if (reply) {
|
|
642
|
-
const text = asciiText(reply.text, 900);
|
|
643
|
-
fields.push(reply.code === undefined ? ["Diagnostic-Code", `X-Bumail; ${text}`] : ["Diagnostic-Code", `smtp; ${reply.code} ${text}`]);
|
|
644
|
-
}
|
|
645
|
-
fields.push(["Last-Attempt-Date", formatDate(recipient.lastAttempt)]);
|
|
646
|
-
if (report.kind === "delayed" && report.willRetryUntil) {
|
|
647
|
-
fields.push(["Will-Retry-Until", formatDate(report.willRetryUntil)]);
|
|
648
|
-
}
|
|
649
|
-
return fields.map(([name, value]) => foldHeader(name, value));
|
|
650
|
-
}
|
|
651
|
-
function deliveryStatus(report) {
|
|
652
|
-
const message = [
|
|
653
|
-
foldHeader("Reporting-MTA", `dns; ${strip(report.reportingMta)}`),
|
|
654
|
-
foldHeader("Arrival-Date", formatDate(report.arrival))
|
|
655
|
-
].join(`\r
|
|
656
|
-
`);
|
|
657
|
-
const blocks = report.recipients.map((r) => recipientFields(report, r).join(`\r
|
|
658
|
-
`));
|
|
659
|
-
return `${[message, ...blocks].join(`\r
|
|
660
|
-
\r
|
|
661
|
-
`)}\r
|
|
662
|
-
`;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// src/dsn/build.ts
|
|
666
|
-
var encoder = new TextEncoder;
|
|
667
|
-
var strip2 = (text) => replaceControls(text, "").replace(/[\s<>]/g, "");
|
|
668
|
-
function humanText(input) {
|
|
669
|
-
const failed = input.kind === "failed";
|
|
670
|
-
const lines = failed ? [
|
|
671
|
-
`This is the mail system at ${input.reportingMta}.`,
|
|
672
|
-
"",
|
|
673
|
-
"Your message could not be delivered to the recipients below.",
|
|
674
|
-
"This is a permanent failure: the message will not be retried."
|
|
675
|
-
] : [
|
|
676
|
-
`This is the mail system at ${input.reportingMta}.`,
|
|
677
|
-
"",
|
|
678
|
-
"Your message has not been delivered yet to the recipients below.",
|
|
679
|
-
"This is a warning only: the message is still being retried",
|
|
680
|
-
...input.willRetryUntil ? [`until ${formatDate2(input.willRetryUntil)}.`] : [],
|
|
681
|
-
"You do not need to send it again."
|
|
682
|
-
];
|
|
683
|
-
if (!input.original) {
|
|
684
|
-
lines.push("", "Your message could not be read from the queue, so it is not returned.");
|
|
685
|
-
}
|
|
686
|
-
lines.push("");
|
|
687
|
-
for (const r of input.recipients) {
|
|
688
|
-
const said = r.reply ? `${r.reply.code ?? ""} ${cleanText(r.reply.text, 900)}`.trim() : statusOf(input.kind);
|
|
689
|
-
const why = r.reply?.status === "4.4.7" ? `delivery time expired, still failing: ${said}` : said;
|
|
690
|
-
lines.push(`<${cleanText(r.address, 254)}>: ${why}`);
|
|
691
|
-
}
|
|
692
|
-
return `${lines.join(`\r
|
|
693
|
-
`)}\r
|
|
694
|
-
`;
|
|
695
|
-
}
|
|
696
|
-
function headerFields(input, boundary) {
|
|
697
|
-
const subject = input.kind === "failed" ? "Undelivered Mail Returned to Sender" : "Delayed Mail (still being retried)";
|
|
698
|
-
const domain = strip2(input.reportingMta);
|
|
699
|
-
return [
|
|
700
|
-
foldHeader2("Date", formatDate2(input.date)),
|
|
701
|
-
foldHeader2("From", `Mail Delivery System <${strip2(input.from)}>`),
|
|
702
|
-
foldHeader2("To", `<${strip2(input.to)}>`),
|
|
703
|
-
foldHeader2("Subject", encodeHeaderValue(subject)),
|
|
704
|
-
foldHeader2("Message-ID", `<${crypto.randomUUID()}@${domain}>`),
|
|
705
|
-
foldHeader2("Auto-Submitted", "auto-replied"),
|
|
706
|
-
foldHeader2("MIME-Version", "1.0"),
|
|
707
|
-
foldHeader2("Content-Type", `multipart/report; report-type=delivery-status; boundary="${boundary}"`)
|
|
708
|
-
];
|
|
709
|
-
}
|
|
710
|
-
var concat = (parts) => {
|
|
711
|
-
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
712
|
-
let at = 0;
|
|
713
|
-
for (const part of parts) {
|
|
714
|
-
out.set(part, at);
|
|
715
|
-
at += part.length;
|
|
716
|
-
}
|
|
717
|
-
return out;
|
|
718
|
-
};
|
|
719
|
-
function boundaryFor(content) {
|
|
720
|
-
const text = new TextDecoder("latin1").decode(content);
|
|
721
|
-
for (;; ) {
|
|
722
|
-
const boundary = `=_dsn_${crypto.randomUUID()}`;
|
|
723
|
-
if (!text.includes(boundary))
|
|
724
|
-
return boundary;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
function buildDsn(input) {
|
|
728
|
-
const original = input.original ? returned(input.original, input.returnContent, input.maxReturn) : undefined;
|
|
729
|
-
const boundary = boundaryFor(original?.body ?? new Uint8Array);
|
|
730
|
-
const text = encodeQuotedPrintable(humanText(input));
|
|
731
|
-
const status = deliveryStatus({
|
|
732
|
-
kind: input.kind,
|
|
733
|
-
reportingMta: input.reportingMta,
|
|
734
|
-
arrival: input.arrival,
|
|
735
|
-
recipients: input.recipients,
|
|
736
|
-
...input.willRetryUntil ? { willRetryUntil: input.willRetryUntil } : {}
|
|
737
|
-
});
|
|
738
|
-
const parts = [
|
|
739
|
-
...headerFields(input, boundary),
|
|
740
|
-
"",
|
|
741
|
-
"This is a MIME-formatted delivery status notification.",
|
|
742
|
-
"",
|
|
743
|
-
`--${boundary}`,
|
|
744
|
-
"Content-Type: text/plain; charset=utf-8",
|
|
745
|
-
"Content-Transfer-Encoding: quoted-printable",
|
|
746
|
-
"",
|
|
747
|
-
text,
|
|
748
|
-
`--${boundary}`,
|
|
749
|
-
"Content-Type: message/delivery-status",
|
|
750
|
-
"",
|
|
751
|
-
status
|
|
752
|
-
];
|
|
753
|
-
const end = `--${boundary}--\r
|
|
754
|
-
`;
|
|
755
|
-
if (!original)
|
|
756
|
-
return encoder.encode(`${parts.join(`\r
|
|
757
|
-
`)}\r
|
|
758
|
-
${end}`);
|
|
759
|
-
const head = [
|
|
760
|
-
...parts,
|
|
761
|
-
`--${boundary}`,
|
|
762
|
-
`Content-Type: ${original.type}`,
|
|
763
|
-
...original.eightBit ? ["Content-Transfer-Encoding: 8bit"] : [],
|
|
764
|
-
"",
|
|
765
|
-
""
|
|
766
|
-
].join(`\r
|
|
767
|
-
`);
|
|
768
|
-
return concat([
|
|
769
|
-
encoder.encode(head),
|
|
770
|
-
original.body,
|
|
771
|
-
encoder.encode(`\r
|
|
772
|
-
${end}`)
|
|
773
|
-
]);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
894
|
// src/queue/retry.ts
|
|
777
895
|
function retryDelay(attempts, retry, random) {
|
|
778
896
|
const exponent = Math.max(0, attempts - 1);
|
|
@@ -804,7 +922,9 @@ function reported(r, kind) {
|
|
|
804
922
|
};
|
|
805
923
|
}
|
|
806
924
|
async function notify(ctx, item, message, kind, recipients, now) {
|
|
807
|
-
|
|
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)
|
|
808
928
|
return;
|
|
809
929
|
const { settings, store, events } = ctx;
|
|
810
930
|
try {
|
|
@@ -816,13 +936,20 @@ async function notify(ctx, item, message, kind, recipients, now) {
|
|
|
816
936
|
to: item.from,
|
|
817
937
|
arrival: new Date(item.createdAt),
|
|
818
938
|
date: lastAttempt,
|
|
819
|
-
|
|
939
|
+
...item.envid === undefined ? {} : { envid: item.envid },
|
|
940
|
+
recipients: wanted.map((r) => {
|
|
820
941
|
const reply = reported(r, kind);
|
|
821
|
-
|
|
942
|
+
const orcpt = states.get(r.address)?.orcpt;
|
|
943
|
+
return {
|
|
944
|
+
address: r.address,
|
|
945
|
+
...reply ? { reply } : {},
|
|
946
|
+
lastAttempt,
|
|
947
|
+
...orcpt ? { orcpt } : {}
|
|
948
|
+
};
|
|
822
949
|
}),
|
|
823
950
|
...kind === "delayed" ? { willRetryUntil: new Date(giveUpAt(item.createdAt, settings.retry)) } : {},
|
|
824
951
|
...message ? { original: message } : {},
|
|
825
|
-
returnContent: settings.dsn.returnContent,
|
|
952
|
+
returnContent: kind === "relayed" ? "headers" : returnOf(item.ret, settings.dsn.returnContent),
|
|
826
953
|
maxReturn: settings.limits.maxDsnReturn
|
|
827
954
|
});
|
|
828
955
|
const added = await store.add({
|
|
@@ -836,7 +963,7 @@ async function notify(ctx, item, message, kind, recipients, now) {
|
|
|
836
963
|
id: added.id,
|
|
837
964
|
of: item.id,
|
|
838
965
|
to: item.from,
|
|
839
|
-
recipients:
|
|
966
|
+
recipients: wanted.map((r) => r.address)
|
|
840
967
|
});
|
|
841
968
|
} catch (error) {
|
|
842
969
|
events.emit("error", { error, id: item.id });
|
|
@@ -869,7 +996,7 @@ function settle(item, updates, skipped, now, settings) {
|
|
|
869
996
|
}).map((u) => u.status === "deferred" && next === undefined ? expired(u) : u);
|
|
870
997
|
const deferred = all.filter((u) => u.status === "deferred");
|
|
871
998
|
const { delayAfter } = settings.dsn;
|
|
872
|
-
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;
|
|
873
1000
|
return {
|
|
874
1001
|
result: {
|
|
875
1002
|
now,
|
|
@@ -995,6 +1122,8 @@ async function record(ctx, item, updates, skipped, message, used) {
|
|
|
995
1122
|
}
|
|
996
1123
|
emitOutcomes(events, item, settled, used);
|
|
997
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);
|
|
998
1127
|
if (settled.delayDsn) {
|
|
999
1128
|
await notify(ctx, item, message, "delayed", settled.deferred, now);
|
|
1000
1129
|
}
|
|
@@ -1258,10 +1387,10 @@ function createQueue(options) {
|
|
|
1258
1387
|
return {
|
|
1259
1388
|
owner: settings.owner,
|
|
1260
1389
|
async enqueue(message, envelope) {
|
|
1261
|
-
const { from, to } = checkEnvelope(envelope, limits.maxRecipients);
|
|
1390
|
+
const { from, to, ...dsn } = checkEnvelope(envelope, limits.maxRecipients);
|
|
1262
1391
|
const bytes = await readMessage(message, limits.maxMessageSize);
|
|
1263
1392
|
checkLineEnds(bytes);
|
|
1264
|
-
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 });
|
|
1265
1394
|
worker.wake();
|
|
1266
1395
|
return item;
|
|
1267
1396
|
},
|
|
@@ -1286,8 +1415,9 @@ function createQueue(options) {
|
|
|
1286
1415
|
}
|
|
1287
1416
|
export {
|
|
1288
1417
|
QueueError2 as QueueError,
|
|
1289
|
-
createQueue
|
|
1418
|
+
createQueue,
|
|
1419
|
+
deliveredDsn
|
|
1290
1420
|
};
|
|
1291
1421
|
|
|
1292
|
-
//# debugId=
|
|
1422
|
+
//# debugId=D24C84F35DEF0E8164756E2164756E21
|
|
1293
1423
|
//# sourceMappingURL=index.js.map
|