@bumail/queue 0.8.0 → 0.10.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.
Files changed (73) hide show
  1. package/README.md +27 -1
  2. package/dist/chunks/index-r8wb840a.js +24 -0
  3. package/dist/chunks/index-r8wb840a.js.map +10 -0
  4. package/dist/chunks/{index-e5wa9f3j.js → index-sz13a17b.js} +103 -21
  5. package/dist/chunks/index-sz13a17b.js.map +14 -0
  6. package/dist/contract/checks.d.ts +2 -6
  7. package/dist/contract/checks.d.ts.map +1 -1
  8. package/dist/contract/dsn-checks.d.ts +4 -0
  9. package/dist/contract/dsn-checks.d.ts.map +1 -0
  10. package/dist/contract/dsn-column.d.ts +12 -0
  11. package/dist/contract/dsn-column.d.ts.map +1 -0
  12. package/dist/contract/storable.d.ts +7 -0
  13. package/dist/contract/storable.d.ts.map +1 -0
  14. package/dist/contract/types.d.ts +42 -0
  15. package/dist/contract/types.d.ts.map +1 -1
  16. package/dist/dsn/build.d.ts +2 -0
  17. package/dist/dsn/build.d.ts.map +1 -1
  18. package/dist/dsn/delivered.d.ts +25 -0
  19. package/dist/dsn/delivered.d.ts.map +1 -0
  20. package/dist/dsn/failed.d.ts +29 -0
  21. package/dist/dsn/failed.d.ts.map +1 -0
  22. package/dist/dsn/fields.d.ts +11 -2
  23. package/dist/dsn/fields.d.ts.map +1 -1
  24. package/dist/dsn/for.d.ts +26 -0
  25. package/dist/dsn/for.d.ts.map +1 -0
  26. package/dist/dsn/human.d.ts +7 -0
  27. package/dist/dsn/human.d.ts.map +1 -0
  28. package/dist/dsn/policy.d.ts +19 -0
  29. package/dist/dsn/policy.d.ts.map +1 -0
  30. package/dist/index.d.ts +4 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +404 -255
  33. package/dist/index.js.map +21 -16
  34. package/dist/memory/index.js +1 -1
  35. package/dist/postgres/index.js +17 -9
  36. package/dist/postgres/index.js.map +6 -6
  37. package/dist/postgres/rows.d.ts +6 -1
  38. package/dist/postgres/rows.d.ts.map +1 -1
  39. package/dist/postgres/schema.d.ts.map +1 -1
  40. package/dist/postgres/statements.d.ts +9 -8
  41. package/dist/postgres/statements.d.ts.map +1 -1
  42. package/dist/postgres/store.d.ts.map +1 -1
  43. package/dist/queue/envelope.d.ts +24 -4
  44. package/dist/queue/envelope.d.ts.map +1 -1
  45. package/dist/queue/events.d.ts +2 -1
  46. package/dist/queue/events.d.ts.map +1 -1
  47. package/dist/queue/notify.d.ts +6 -3
  48. package/dist/queue/notify.d.ts.map +1 -1
  49. package/dist/queue/outcome.d.ts.map +1 -1
  50. package/dist/queue/queue.d.ts.map +1 -1
  51. package/dist/queue/route.d.ts +7 -0
  52. package/dist/queue/route.d.ts.map +1 -1
  53. package/dist/queue/session.d.ts.map +1 -1
  54. package/dist/queue/settings/limits.d.ts.map +1 -1
  55. package/dist/queue/settle.d.ts.map +1 -1
  56. package/dist/redis/index.js +26 -13
  57. package/dist/redis/index.js.map +5 -5
  58. package/dist/redis/items.d.ts +2 -0
  59. package/dist/redis/items.d.ts.map +1 -1
  60. package/dist/redis/scripts.d.ts +1 -1
  61. package/dist/redis/scripts.d.ts.map +1 -1
  62. package/dist/redis/store.d.ts.map +1 -1
  63. package/dist/sqlite/index.js +15 -8
  64. package/dist/sqlite/index.js.map +5 -5
  65. package/dist/sqlite/rows.d.ts +2 -1
  66. package/dist/sqlite/rows.d.ts.map +1 -1
  67. package/dist/sqlite/schema.d.ts.map +1 -1
  68. package/dist/sqlite/store.d.ts.map +1 -1
  69. package/docs/guide.md +102 -3
  70. package/docs/roadmap.md +8 -3
  71. package/docs/troubleshooting.md +42 -0
  72. package/package.json +3 -3
  73. package/dist/chunks/index-e5wa9f3j.js.map +0 -12
package/dist/index.js CHANGED
@@ -1,12 +1,353 @@
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-e5wa9f3j.js";
9
+ } from "./chunks/index-sz13a17b.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
+ var DEFAULT_MAX_RETURN = 64 * 1024;
310
+
311
+ // src/dsn/for.ts
312
+ function dsnFor(kind, input, recipients, returnContent) {
313
+ if (input.to === "" || recipients.length === 0)
314
+ return;
315
+ return buildDsn({
316
+ kind,
317
+ reportingMta: input.reportingMta,
318
+ from: input.from,
319
+ to: input.to,
320
+ arrival: input.arrival,
321
+ date: input.date ?? new Date,
322
+ ...input.envid === undefined ? {} : { envid: input.envid },
323
+ recipients,
324
+ ...input.original ? { original: input.original } : {},
325
+ returnContent,
326
+ maxReturn: input.maxReturn ?? DEFAULT_MAX_RETURN
327
+ });
328
+ }
329
+
330
+ // src/dsn/delivered.ts
331
+ function deliveredDsn(input) {
332
+ const date = input.date ?? new Date;
333
+ const wanted = input.recipients.filter((r) => asked("delivered", r.notify));
334
+ return dsnFor("delivered", { ...input, date }, wanted.map((r) => ({
335
+ address: r.address,
336
+ lastAttempt: date,
337
+ ...r.orcpt ? { orcpt: r.orcpt } : {}
338
+ })), "headers");
339
+ }
340
+ // src/dsn/failed.ts
341
+ function failedDsn(input) {
342
+ const date = input.date ?? new Date;
343
+ const wanted = input.recipients.filter((r) => asked("failed", r.notify));
344
+ return dsnFor("failed", { ...input, date }, wanted.map((r) => ({
345
+ address: r.address,
346
+ reply: r.reply,
347
+ lastAttempt: date,
348
+ ...r.orcpt ? { orcpt: r.orcpt } : {}
349
+ })), returnOf(input.ret, "headers"));
350
+ }
10
351
  // src/queue/envelope.ts
11
352
  import { isMailbox } from "@bumail/smtp/client";
12
353
  var ADDRESS = /^[^\s<>]{1,64}@[^\s<>@]{1,253}$/u;
@@ -39,15 +380,27 @@ function checkEnvelope(envelope, maxRecipients) {
39
380
  if (to.length > maxRecipients) {
40
381
  throw new QueueError2("TOO_MANY_RECIPIENTS", `The message has ${to.length} recipients; the limit is ${maxRecipients}`);
41
382
  }
42
- return { from, to };
383
+ const keyOf = (a) => `${a.slice(0, a.lastIndexOf("@"))}@${domainOf(a)}`;
384
+ const kept = new Map(to.map((a) => [keyOf(a), a]));
385
+ const recipients = Array.isArray(envelope.recipients) ? envelope.recipients.map((r) => {
386
+ const target = typeof r?.address === "string" && list.includes(r.address) ? kept.get(keyOf(r.address)) : undefined;
387
+ return target === undefined ? r : { ...r, address: target };
388
+ }).filter((r, i, all) => all.findIndex((o) => o?.address === r?.address) === i) : envelope.recipients;
389
+ return {
390
+ from,
391
+ to,
392
+ ...envelope.ret === undefined ? {} : { ret: envelope.ret },
393
+ ...envelope.envid === undefined ? {} : { envid: envelope.envid },
394
+ ...recipients === undefined ? {} : { recipients }
395
+ };
43
396
  }
44
397
  var tooBig = (max) => new QueueError2("MESSAGE_TOO_BIG", `The message is larger than the limit of ${max} bytes`);
45
- var CR = 13;
46
- var LF = 10;
398
+ var CR2 = 13;
399
+ var LF2 = 10;
47
400
  function checkLineEnds(bytes) {
48
401
  for (let i = 0;i < bytes.length; i++) {
49
402
  const byte = bytes[i];
50
- const bare = byte === CR ? bytes[i + 1] !== LF : byte === LF && (i === 0 || bytes[i - 1] !== CR);
403
+ const bare = byte === CR2 ? bytes[i + 1] !== LF2 : byte === LF2 && (i === 0 || bytes[i - 1] !== CR2);
51
404
  if (bare) {
52
405
  throw invalid(`The message has a bare CR or LF at byte ${i}: every line must end in CRLF`);
53
406
  }
@@ -168,7 +521,7 @@ function limitsOf(options) {
168
521
  maxRecipients: numberOf("limits.maxRecipients", l.maxRecipients, 100, 1),
169
522
  maxItems: l.maxItems === undefined ? undefined : numberOf("limits.maxItems", l.maxItems, 0, 1),
170
523
  maxReplyText: numberOf("limits.maxReplyText", l.maxReplyText, 512, 64, 900),
171
- maxDsnReturn: numberOf("limits.maxDsnReturn", l.maxDsnReturn, 64 * 1024, 0)
524
+ maxDsnReturn: numberOf("limits.maxDsnReturn", l.maxDsnReturn, DEFAULT_MAX_RETURN, 0)
172
525
  };
173
526
  }
174
527
 
@@ -337,7 +690,8 @@ function outcomesOf(result, group, max) {
337
690
  updates.set(recipient, {
338
691
  address: recipient,
339
692
  status: "delivered",
340
- reply: diagnosticOf(result.reply, max, host)
693
+ reply: diagnosticOf(result.reply, max, host),
694
+ ...result.dsnOffered === false ? { dsnOffered: false } : {}
341
695
  });
342
696
  }
343
697
  for (const { recipient, reply } of result.rejected) {
@@ -403,6 +757,19 @@ function routeOf(settings, domain) {
403
757
  function hostOf(route) {
404
758
  return route === "mx" ? undefined : route.host;
405
759
  }
760
+ function dsnOptionsOf(item, group) {
761
+ const only = new Set(group);
762
+ const recipients = item.recipients.filter((r) => only.has(r.address) && (r.notify !== undefined || r.orcpt !== undefined)).map((r) => ({
763
+ address: r.address,
764
+ ...r.notify === undefined ? {} : { notify: r.notify },
765
+ ...r.orcpt === undefined ? {} : { orcpt: r.orcpt }
766
+ }));
767
+ return {
768
+ ...item.ret === undefined ? {} : { ret: item.ret },
769
+ ...item.envid === undefined ? {} : { envid: item.envid },
770
+ ...recipients.length === 0 ? {} : { recipients }
771
+ };
772
+ }
406
773
  function sendOptionsOf(settings, domain, from, to, override) {
407
774
  const { options, hostname } = settings;
408
775
  const common = {
@@ -449,7 +816,10 @@ async function session(ctx, item, message, domain, group, route, override) {
449
816
  const { settings } = ctx;
450
817
  const max = settings.limits.maxReplyText;
451
818
  try {
452
- const options = sendOptionsOf(settings, domain, item.from, group, override);
819
+ const options = {
820
+ ...sendOptionsOf(settings, domain, item.from, group, override),
821
+ ...dsnOptionsOf(item, group)
822
+ };
453
823
  const result = await settings.send(message, options);
454
824
  return { updates: outcomesOf(result, group, max) };
455
825
  } catch (error) {
@@ -538,241 +908,6 @@ async function attemptGroup(ctx, item, message, domain, recipients, used) {
538
908
  return [...kept, ...updates, ...bad.map(badAddress)];
539
909
  }
540
910
 
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
911
  // src/queue/retry.ts
777
912
  function retryDelay(attempts, retry, random) {
778
913
  const exponent = Math.max(0, attempts - 1);
@@ -804,7 +939,9 @@ function reported(r, kind) {
804
939
  };
805
940
  }
806
941
  async function notify(ctx, item, message, kind, recipients, now) {
807
- if (item.from === "" || recipients.length === 0)
942
+ const states = new Map(item.recipients.map((r) => [r.address, r]));
943
+ const wanted = recipients.filter((r) => asked(kind, states.get(r.address)?.notify));
944
+ if (item.from === "" || wanted.length === 0)
808
945
  return;
809
946
  const { settings, store, events } = ctx;
810
947
  try {
@@ -816,13 +953,20 @@ async function notify(ctx, item, message, kind, recipients, now) {
816
953
  to: item.from,
817
954
  arrival: new Date(item.createdAt),
818
955
  date: lastAttempt,
819
- recipients: recipients.map((r) => {
956
+ ...item.envid === undefined ? {} : { envid: item.envid },
957
+ recipients: wanted.map((r) => {
820
958
  const reply = reported(r, kind);
821
- return { address: r.address, ...reply ? { reply } : {}, lastAttempt };
959
+ const orcpt = states.get(r.address)?.orcpt;
960
+ return {
961
+ address: r.address,
962
+ ...reply ? { reply } : {},
963
+ lastAttempt,
964
+ ...orcpt ? { orcpt } : {}
965
+ };
822
966
  }),
823
967
  ...kind === "delayed" ? { willRetryUntil: new Date(giveUpAt(item.createdAt, settings.retry)) } : {},
824
968
  ...message ? { original: message } : {},
825
- returnContent: settings.dsn.returnContent,
969
+ returnContent: kind === "relayed" ? "headers" : returnOf(item.ret, settings.dsn.returnContent),
826
970
  maxReturn: settings.limits.maxDsnReturn
827
971
  });
828
972
  const added = await store.add({
@@ -836,7 +980,7 @@ async function notify(ctx, item, message, kind, recipients, now) {
836
980
  id: added.id,
837
981
  of: item.id,
838
982
  to: item.from,
839
- recipients: recipients.map((r) => r.address)
983
+ recipients: wanted.map((r) => r.address)
840
984
  });
841
985
  } catch (error) {
842
986
  events.emit("error", { error, id: item.id });
@@ -869,7 +1013,7 @@ function settle(item, updates, skipped, now, settings) {
869
1013
  }).map((u) => u.status === "deferred" && next === undefined ? expired(u) : u);
870
1014
  const deferred = all.filter((u) => u.status === "deferred");
871
1015
  const { delayAfter } = settings.dsn;
872
- const delayDsn = !item.delayNotified && item.from !== "" && delayAfter !== false && deferred.length > 0 && now - item.createdAt >= delayAfter;
1016
+ 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
1017
  return {
874
1018
  result: {
875
1019
  now,
@@ -995,6 +1139,8 @@ async function record(ctx, item, updates, skipped, message, used) {
995
1139
  }
996
1140
  emitOutcomes(events, item, settled, used);
997
1141
  await notify(ctx, item, message, "failed", settled.failed, now);
1142
+ const relayed = settled.delivered.filter((u) => u.dsnOffered === false);
1143
+ await notify(ctx, item, message, "relayed", relayed, now);
998
1144
  if (settled.delayDsn) {
999
1145
  await notify(ctx, item, message, "delayed", settled.deferred, now);
1000
1146
  }
@@ -1258,10 +1404,10 @@ function createQueue(options) {
1258
1404
  return {
1259
1405
  owner: settings.owner,
1260
1406
  async enqueue(message, envelope) {
1261
- const { from, to } = checkEnvelope(envelope, limits.maxRecipients);
1407
+ const { from, to, ...dsn } = checkEnvelope(envelope, limits.maxRecipients);
1262
1408
  const bytes = await readMessage(message, limits.maxMessageSize);
1263
1409
  checkLineEnds(bytes);
1264
- const item = await store.add({ from, to, message: bytes, createdAt: settings.now() }, limits.maxItems === undefined ? {} : { maxItems: limits.maxItems });
1410
+ const item = await store.add({ from, to, message: bytes, createdAt: settings.now(), ...dsn }, limits.maxItems === undefined ? {} : { maxItems: limits.maxItems });
1265
1411
  worker.wake();
1266
1412
  return item;
1267
1413
  },
@@ -1285,9 +1431,12 @@ function createQueue(options) {
1285
1431
  };
1286
1432
  }
1287
1433
  export {
1434
+ DEFAULT_MAX_RETURN,
1288
1435
  QueueError2 as QueueError,
1289
- createQueue
1436
+ createQueue,
1437
+ deliveredDsn,
1438
+ failedDsn
1290
1439
  };
1291
1440
 
1292
- //# debugId=3E9664C55274145964756E2164756E21
1441
+ //# debugId=9F6E913A6461985264756E2164756E21
1293
1442
  //# sourceMappingURL=index.js.map