@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.
Files changed (77) hide show
  1. package/README.md +27 -3
  2. package/dist/chunks/{index-3pt7rc59.js → index-9yp15arf.js} +98 -25
  3. package/dist/chunks/index-9yp15arf.js.map +14 -0
  4. package/dist/chunks/index-r8wb840a.js +24 -0
  5. package/dist/chunks/index-r8wb840a.js.map +10 -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/queue-store.d.ts +2 -1
  13. package/dist/contract/queue-store.d.ts.map +1 -1
  14. package/dist/contract/storable.d.ts +7 -0
  15. package/dist/contract/storable.d.ts.map +1 -0
  16. package/dist/contract/types.d.ts +52 -0
  17. package/dist/contract/types.d.ts.map +1 -1
  18. package/dist/dsn/build.d.ts +2 -0
  19. package/dist/dsn/build.d.ts.map +1 -1
  20. package/dist/dsn/delivered.d.ts +39 -0
  21. package/dist/dsn/delivered.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/human.d.ts +7 -0
  25. package/dist/dsn/human.d.ts.map +1 -0
  26. package/dist/dsn/policy.d.ts +17 -0
  27. package/dist/dsn/policy.d.ts.map +1 -0
  28. package/dist/index.d.ts +2 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +474 -300
  31. package/dist/index.js.map +20 -15
  32. package/dist/memory/index.js +1 -1
  33. package/dist/memory/index.js.map +1 -1
  34. package/dist/postgres/index.js +17 -9
  35. package/dist/postgres/index.js.map +6 -6
  36. package/dist/postgres/rows.d.ts +6 -1
  37. package/dist/postgres/rows.d.ts.map +1 -1
  38. package/dist/postgres/schema.d.ts.map +1 -1
  39. package/dist/postgres/statements.d.ts +9 -8
  40. package/dist/postgres/statements.d.ts.map +1 -1
  41. package/dist/postgres/store.d.ts.map +1 -1
  42. package/dist/queue/attempt.d.ts +7 -7
  43. package/dist/queue/attempt.d.ts.map +1 -1
  44. package/dist/queue/deliver.d.ts.map +1 -1
  45. package/dist/queue/envelope.d.ts +24 -4
  46. package/dist/queue/envelope.d.ts.map +1 -1
  47. package/dist/queue/events.d.ts +8 -1
  48. package/dist/queue/events.d.ts.map +1 -1
  49. package/dist/queue/fallback-session.d.ts +22 -0
  50. package/dist/queue/fallback-session.d.ts.map +1 -0
  51. package/dist/queue/notify.d.ts +6 -3
  52. package/dist/queue/notify.d.ts.map +1 -1
  53. package/dist/queue/outcome.d.ts.map +1 -1
  54. package/dist/queue/queue.d.ts.map +1 -1
  55. package/dist/queue/route.d.ts +7 -0
  56. package/dist/queue/route.d.ts.map +1 -1
  57. package/dist/queue/session.d.ts +14 -0
  58. package/dist/queue/session.d.ts.map +1 -0
  59. package/dist/queue/settle.d.ts.map +1 -1
  60. package/dist/redis/index.js +26 -13
  61. package/dist/redis/index.js.map +5 -5
  62. package/dist/redis/items.d.ts +2 -0
  63. package/dist/redis/items.d.ts.map +1 -1
  64. package/dist/redis/scripts.d.ts +1 -1
  65. package/dist/redis/scripts.d.ts.map +1 -1
  66. package/dist/redis/store.d.ts.map +1 -1
  67. package/dist/sqlite/index.js +15 -8
  68. package/dist/sqlite/index.js.map +5 -5
  69. package/dist/sqlite/rows.d.ts +2 -1
  70. package/dist/sqlite/rows.d.ts.map +1 -1
  71. package/dist/sqlite/schema.d.ts.map +1 -1
  72. package/dist/sqlite/store.d.ts.map +1 -1
  73. package/docs/guide.md +97 -7
  74. package/docs/roadmap.md +14 -7
  75. package/docs/troubleshooting.md +52 -1
  76. package/package.json +3 -3
  77. 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-3pt7rc59.js";
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
- return { from, to };
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 CR = 13;
46
- var LF = 10;
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 === CR ? bytes[i + 1] !== LF : byte === LF && (i === 0 || bytes[i - 1] !== CR);
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/attempt.ts
440
- var badAddress = (address) => ({
441
- address,
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 = sendOptionsOf(settings, domain, item.from, group, override);
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 doubt = route === "mx" && isSmtpError(error) && error.afterData;
468
- return { updates: doubt ? doubtful(updates, error) : updates, error };
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 doubtful = new Set(item.recipients.filter((r) => r.mayHaveReachedMx).map((r) => r.address));
491
- if (fallback !== undefined && route === "mx") {
492
- const mx = new Map(updates.filter((u) => u.status !== "delivered" && !u.mayHaveReachedMx && !doubtful.has(u.address) && fallsBack(u.reply, first.error)).map((u) => [u.address, u]));
493
- if (mx.size > 0) {
494
- const again = [...mx.keys()];
495
- const second = await session(ctx, item, message, domain, again, fallback, fallback);
496
- for (const [address, u] of mx) {
497
- used.set(address, {
498
- route: "smarthost",
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
- if (kind !== "failed" || !r.mayHaveReachedMx)
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 === "" ? MAY_HAVE_REACHED : `${reply.text} ${MAY_HAVE_REACHED}`
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
- if (item.from === "" || recipients.length === 0)
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
- recipients: recipients.map((r) => {
939
+ ...item.envid === undefined ? {} : { envid: item.envid },
940
+ recipients: wanted.map((r) => {
789
941
  const reply = reported(r, kind);
790
- return { address: r.address, ...reply ? { reply } : {}, lastAttempt };
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: recipients.map((r) => r.address)
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 doubtful = new Set(item.recipients.filter((r) => r.mayHaveReachedMx).map((r) => r.address));
828
- const all = updates.map((u) => doubtful.has(u.address) && u.status !== "delivered" ? { ...u, mayHaveReachedMx: true } : u).map((u) => u.status === "deferred" && next === undefined ? expired(u) : 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) => u.mayHaveReachedMx ? { mayHaveReachedMx: true } : {};
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=14BF571A961D5A4C64756E2164756E21
1422
+ //# debugId=D24C84F35DEF0E8164756E2164756E21
1249
1423
  //# sourceMappingURL=index.js.map