@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.
Files changed (68) hide show
  1. package/README.md +25 -1
  2. package/dist/chunks/{index-e5wa9f3j.js → index-9yp15arf.js} +91 -21
  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/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 +39 -0
  19. package/dist/dsn/delivered.d.ts.map +1 -0
  20. package/dist/dsn/fields.d.ts +11 -2
  21. package/dist/dsn/fields.d.ts.map +1 -1
  22. package/dist/dsn/human.d.ts +7 -0
  23. package/dist/dsn/human.d.ts.map +1 -0
  24. package/dist/dsn/policy.d.ts +17 -0
  25. package/dist/dsn/policy.d.ts.map +1 -0
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +384 -254
  29. package/dist/index.js.map +18 -15
  30. package/dist/memory/index.js +1 -1
  31. package/dist/postgres/index.js +17 -9
  32. package/dist/postgres/index.js.map +6 -6
  33. package/dist/postgres/rows.d.ts +6 -1
  34. package/dist/postgres/rows.d.ts.map +1 -1
  35. package/dist/postgres/schema.d.ts.map +1 -1
  36. package/dist/postgres/statements.d.ts +9 -8
  37. package/dist/postgres/statements.d.ts.map +1 -1
  38. package/dist/postgres/store.d.ts.map +1 -1
  39. package/dist/queue/envelope.d.ts +24 -4
  40. package/dist/queue/envelope.d.ts.map +1 -1
  41. package/dist/queue/events.d.ts +2 -1
  42. package/dist/queue/events.d.ts.map +1 -1
  43. package/dist/queue/notify.d.ts +6 -3
  44. package/dist/queue/notify.d.ts.map +1 -1
  45. package/dist/queue/outcome.d.ts.map +1 -1
  46. package/dist/queue/queue.d.ts.map +1 -1
  47. package/dist/queue/route.d.ts +7 -0
  48. package/dist/queue/route.d.ts.map +1 -1
  49. package/dist/queue/session.d.ts.map +1 -1
  50. package/dist/queue/settle.d.ts.map +1 -1
  51. package/dist/redis/index.js +26 -13
  52. package/dist/redis/index.js.map +5 -5
  53. package/dist/redis/items.d.ts +2 -0
  54. package/dist/redis/items.d.ts.map +1 -1
  55. package/dist/redis/scripts.d.ts +1 -1
  56. package/dist/redis/scripts.d.ts.map +1 -1
  57. package/dist/redis/store.d.ts.map +1 -1
  58. package/dist/sqlite/index.js +15 -8
  59. package/dist/sqlite/index.js.map +5 -5
  60. package/dist/sqlite/rows.d.ts +2 -1
  61. package/dist/sqlite/rows.d.ts.map +1 -1
  62. package/dist/sqlite/schema.d.ts.map +1 -1
  63. package/dist/sqlite/store.d.ts.map +1 -1
  64. package/docs/guide.md +72 -3
  65. package/docs/roadmap.md +8 -3
  66. package/docs/troubleshooting.md +41 -0
  67. package/package.json +3 -3
  68. 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-e5wa9f3j.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 = {
@@ -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 = sendOptionsOf(settings, domain, item.from, group, override);
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
- 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)
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
- recipients: recipients.map((r) => {
939
+ ...item.envid === undefined ? {} : { envid: item.envid },
940
+ recipients: wanted.map((r) => {
820
941
  const reply = reported(r, kind);
821
- 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
+ };
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: recipients.map((r) => r.address)
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=3E9664C55274145964756E2164756E21
1422
+ //# debugId=D24C84F35DEF0E8164756E2164756E21
1293
1423
  //# sourceMappingURL=index.js.map