@bumail/queue 0.1.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +243 -0
  3. package/dist/chunks/index-p4k4rjb7.js +15 -0
  4. package/dist/chunks/index-p4k4rjb7.js.map +10 -0
  5. package/dist/chunks/index-xhsw4xcz.js +119 -0
  6. package/dist/chunks/index-xhsw4xcz.js.map +10 -0
  7. package/dist/contract/checks.d.ts +27 -0
  8. package/dist/contract/checks.d.ts.map +1 -0
  9. package/dist/contract/queue-store.d.ts +60 -0
  10. package/dist/contract/queue-store.d.ts.map +1 -0
  11. package/dist/contract/types.d.ts +102 -0
  12. package/dist/contract/types.d.ts.map +1 -0
  13. package/dist/dsn/build.d.ts +35 -0
  14. package/dist/dsn/build.d.ts.map +1 -0
  15. package/dist/dsn/content.d.ts +18 -0
  16. package/dist/dsn/content.d.ts.map +1 -0
  17. package/dist/dsn/fields.d.ts +38 -0
  18. package/dist/dsn/fields.d.ts.map +1 -0
  19. package/dist/errors.d.ts +17 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/index.d.ts +8 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +1089 -0
  24. package/dist/index.js.map +30 -0
  25. package/dist/memory/index.d.ts +2 -0
  26. package/dist/memory/index.d.ts.map +1 -0
  27. package/dist/memory/index.js +121 -0
  28. package/dist/memory/index.js.map +10 -0
  29. package/dist/memory/store.d.ts +21 -0
  30. package/dist/memory/store.d.ts.map +1 -0
  31. package/dist/queue/deliver.d.ts +20 -0
  32. package/dist/queue/deliver.d.ts.map +1 -0
  33. package/dist/queue/envelope.d.ts +26 -0
  34. package/dist/queue/envelope.d.ts.map +1 -0
  35. package/dist/queue/events.d.ts +48 -0
  36. package/dist/queue/events.d.ts.map +1 -0
  37. package/dist/queue/limiter.d.ts +14 -0
  38. package/dist/queue/limiter.d.ts.map +1 -0
  39. package/dist/queue/notify.d.ts +19 -0
  40. package/dist/queue/notify.d.ts.map +1 -0
  41. package/dist/queue/options.d.ts +111 -0
  42. package/dist/queue/options.d.ts.map +1 -0
  43. package/dist/queue/outcome.d.ts +26 -0
  44. package/dist/queue/outcome.d.ts.map +1 -0
  45. package/dist/queue/queue.d.ts +43 -0
  46. package/dist/queue/queue.d.ts.map +1 -0
  47. package/dist/queue/retry.d.ts +16 -0
  48. package/dist/queue/retry.d.ts.map +1 -0
  49. package/dist/queue/route.d.ts +10 -0
  50. package/dist/queue/route.d.ts.map +1 -0
  51. package/dist/queue/settings/dsn.d.ts +8 -0
  52. package/dist/queue/settings/dsn.d.ts.map +1 -0
  53. package/dist/queue/settings/index.d.ts +25 -0
  54. package/dist/queue/settings/index.d.ts.map +1 -0
  55. package/dist/queue/settings/limits.d.ts +18 -0
  56. package/dist/queue/settings/limits.d.ts.map +1 -0
  57. package/dist/queue/settings/numbers.d.ts +7 -0
  58. package/dist/queue/settings/numbers.d.ts.map +1 -0
  59. package/dist/queue/settings/routes.d.ts +3 -0
  60. package/dist/queue/settings/routes.d.ts.map +1 -0
  61. package/dist/queue/settings/sessions.d.ts +5 -0
  62. package/dist/queue/settings/sessions.d.ts.map +1 -0
  63. package/dist/queue/settle.d.ts +21 -0
  64. package/dist/queue/settle.d.ts.map +1 -0
  65. package/dist/queue/worker.d.ts +27 -0
  66. package/dist/queue/worker.d.ts.map +1 -0
  67. package/dist/sqlite/index.d.ts +2 -0
  68. package/dist/sqlite/index.d.ts.map +1 -0
  69. package/dist/sqlite/index.js +288 -0
  70. package/dist/sqlite/index.js.map +13 -0
  71. package/dist/sqlite/open.d.ts +8 -0
  72. package/dist/sqlite/open.d.ts.map +1 -0
  73. package/dist/sqlite/options.d.ts +16 -0
  74. package/dist/sqlite/options.d.ts.map +1 -0
  75. package/dist/sqlite/rows.d.ts +28 -0
  76. package/dist/sqlite/rows.d.ts.map +1 -0
  77. package/dist/sqlite/schema.d.ts +14 -0
  78. package/dist/sqlite/schema.d.ts.map +1 -0
  79. package/dist/sqlite/store.d.ts +30 -0
  80. package/dist/sqlite/store.d.ts.map +1 -0
  81. package/dist/text.d.ts +14 -0
  82. package/dist/text.d.ts.map +1 -0
  83. package/docs/README.md +10 -0
  84. package/docs/guide.md +461 -0
  85. package/docs/roadmap.md +67 -0
  86. package/docs/troubleshooting.md +394 -0
  87. package/package.json +71 -0
@@ -0,0 +1,394 @@
1
+ # Troubleshooting
2
+
3
+ Each entry is headed by the message of the `QueueError` thrown, or given
4
+ to the `error` event; its `code` is the group it is listed under. The
5
+ parts shown as … vary.
6
+
7
+ **Enqueuing** (`enqueue`)
8
+
9
+ - [`QueueError: The message is larger than the limit of … bytes`](#queueerror-the-message-is-larger-than-the-limit-of--bytes)
10
+ - [`QueueError: The message has … recipients; the limit is …`](#queueerror-the-message-has--recipients-the-limit-is-)
11
+ - [`QueueError: The queue holds … items, its limit`](#queueerror-the-queue-holds--items-its-limit)
12
+ - [`QueueError: … must be an address, local@domain, not "…"`](#queueerror--must-be-an-address-localdomain-not-)
13
+ - [`QueueError: to must hold one recipient or more`](#queueerror-to-must-hold-one-recipient-or-more)
14
+ - [`QueueError: The envelope is an object: { from, to }`](#queueerror-the-envelope-is-an-object--from-to-)
15
+ - [`QueueError: The message is a Uint8Array, a string or a ReadableStream<Uint8Array>`](#queueerror-the-message-is-a-uint8array-a-string-or-a-readablestreamuint8array)
16
+ - [`QueueError: The message stream must give Uint8Array chunks`](#queueerror-the-message-stream-must-give-uint8array-chunks)
17
+ - [`QueueError: The message has a bare CR or LF at byte …: every line must end in CRLF`](#queueerror-the-message-has-a-bare-cr-or-lf-at-byte--every-line-must-end-in-crlf)
18
+
19
+ **Creating the queue** (`createQueue`)
20
+
21
+ - [`QueueError: The 'mx' route needs a resolver, such as @bumail/dns's nodeResolver()`](#queueerror-the-mx-route-needs-a-resolver-such-as-bumaildnss-noderesolver)
22
+ - [`QueueError: hostname must be this server's public host name, not …`](#queueerror-hostname-must-be-this-servers-public-host-name-not-)
23
+ - [`QueueError: route must be 'mx' or a smarthost with a host`](#queueerror-route-must-be-mx-or-a-smarthost-with-a-host)
24
+ - [`QueueError: route.auth needs tls: 'required', the default with auth: with tls: '…' the password would go to a server whose certificate is not checked`](#queueerror-routeauth-needs-tls-required-the-default-with-auth-with-tls--the-password-would-go-to-a-server-whose-certificate-is-not-checked)
25
+ - [`QueueError: route.auth.username must be a non-empty string`, and the other smarthost options](#queueerror-routeauthusername-must-be-a-non-empty-string-and-the-other-smarthost-options)
26
+ - [`QueueError: timeouts.… must be a number of seconds above 0 and at most 2147483, not …`](#queueerror-timeouts-must-be-a-number-of-seconds-above-0-and-at-most-2147483-not-)
27
+ - [`QueueError: store must be a QueueStore, such as MemoryQueueStore`](#queueerror-store-must-be-a-queuestore-such-as-memoryqueuestore)
28
+ - [`QueueError: … must be an integer of at least …, not …`](#queueerror--must-be-an-integer-of-at-least--not-)
29
+ - [`QueueError: dsn.returnContent must be 'headers' or 'full', not …`](#queueerror-dsnreturncontent-must-be-headers-or-full-not-)
30
+ - [`QueueError: createQueue() takes an options object`](#queueerror-createqueue-takes-an-options-object)
31
+ - [`QueueError: owner must be a non-empty string`](#queueerror-owner-must-be-a-non-empty-string)
32
+
33
+ **The `error` event**
34
+
35
+ - [`QueueError: The lease on … was lost before its outcome was recorded; another worker will try it again`](#queueerror-the-lease-on--was-lost-before-its-outcome-was-recorded-another-worker-will-try-it-again)
36
+ - [`QueueError: The lease on … was lost while it was delivered`](#queueerror-the-lease-on--was-lost-while-it-was-delivered)
37
+ - [`SmtpError: sendMail(): …` (`INVALID_OPTION`), with recipients deferred as `4.3.5`](#smtperror-sendmail--invalid_option-with-recipients-deferred-as-435)
38
+ - [`SQLiteError: database is locked`, or another store error, during a delivery](#sqliteerror-database-is-locked-or-another-store-error-during-a-delivery)
39
+
40
+ **The `bun:sqlite` store** (`@bumail/queue/sqlite`)
41
+
42
+ - [`QueueError: A SQLite queue store needs a directory`](#queueerror-a-sqlite-queue-store-needs-a-directory)
43
+ - [`QueueError: The queue at "…" cannot be opened: …`](#queueerror-the-queue-at--cannot-be-opened-)
44
+ - [`QueueError: The database is at schema version …, newer than this store's …`](#queueerror-the-database-is-at-schema-version--newer-than-this-stores-)
45
+ - [`QueueError: busyTimeout must be an integer of at least 0, not …`](#queueerror-busytimeout-must-be-an-integer-of-at-least-0-not-)
46
+ - [`QueueError: The queue store is closed`](#queueerror-the-queue-store-is-closed)
47
+
48
+ **Listing** (`list`)
49
+
50
+ - [`QueueError: limit must be an integer from 1 to 1000, not …`](#queueerror-limit-must-be-an-integer-from-1-to-1000-not-)
51
+ - [`QueueError: offset must be an integer of at least 0, not …`](#queueerror-offset-must-be-an-integer-of-at-least-0-not-)
52
+
53
+ **A store's own checks** (a store of your own, called directly)
54
+
55
+ - [`QueueError: … must be a finite number`, `owner must be a non-empty string`, and the others](#a-stores-own-checks)
56
+
57
+ **Delivery**
58
+
59
+ - [A recipient stays `deferred` with `4.4.1` or `4.4.2`](#a-recipient-stays-deferred-with-441-or-442)
60
+ - [Every delivery fails at once with `5.7.1`, or with a reply naming your IP or EHLO name](#every-delivery-fails-at-once-with-571-or-with-a-reply-naming-your-ip-or-ehlo-name)
61
+ - [A recipient fails with `5.1.3`, `The address is not one SMTP can carry`](#a-recipient-fails-with-513-the-address-is-not-one-smtp-can-carry)
62
+ - [Every recipient fails with `5.1.7`, `The sender's address is not one SMTP can carry`](#every-recipient-fails-with-517-the-senders-address-is-not-one-smtp-can-carry)
63
+ - [`retryNow` returns `false` for an item that is in the queue](#retrynow-returns-false-for-an-item-that-is-in-the-queue)
64
+
65
+ ## Enqueuing
66
+
67
+ ### `QueueError: The message is larger than the limit of … bytes`
68
+
69
+ **Code:** `MESSAGE_TOO_BIG`.
70
+ **When:** `enqueue`, with a message past `limits.maxMessageSize` (25 MiB
71
+ by default). A stream is read only up to that bound, then cancelled.
72
+ **Fix:** refuse the message earlier — an SMTP server's `maxMessageSize`
73
+ at or below the queue's — or raise the limit:
74
+
75
+ ```ts
76
+ createQueue({ store, hostname, resolver, limits: { maxMessageSize: 50 * 1024 * 1024 } });
77
+ ```
78
+
79
+ ### `QueueError: The message has … recipients; the limit is …`
80
+
81
+ **Code:** `TOO_MANY_RECIPIENTS`.
82
+ **When:** `enqueue`, with more distinct recipients than
83
+ `limits.maxRecipients` (100, RFC 5321 §4.5.3.1.8's least).
84
+ **Fix:** enqueue the message in several batches, or raise the limit.
85
+
86
+ ### `QueueError: The queue holds … items, its limit`
87
+
88
+ **Code:** `QUEUE_FULL`.
89
+ **When:** `enqueue`, with `limits.maxItems` set and the store holding
90
+ that many items. DSNs bypass the limit.
91
+ **Why:** deliveries fall behind what is enqueued: a destination that
92
+ defers everything, a route that fails, no worker running.
93
+ **Fix:** answer the submitter with a temporary failure (a 452 in SMTP),
94
+ look at `queue.list()` and the `deferred` events, and check a worker
95
+ called `start()`.
96
+
97
+ ### `QueueError: … must be an address, local@domain, not "…"`
98
+
99
+ **Code:** `INVALID`.
100
+ **When:** `from`, a recipient, or `dsn.from` is not `local@domain`, is
101
+ longer than 254 characters, has a local part over 64, or holds a space,
102
+ a control character (CR and LF included) or an angle bracket — or is not
103
+ an address by RFC 5321's grammar, which `sendMail` would refuse at
104
+ delivery: `a(b)@example.com`, `a,b@example.com`, `"x@example.com`,
105
+ `a@b@example.com`, `a..b@example.com`, `a@example.com,`, `a@-example`.
106
+ Check one yourself with `@bumail/smtp/client`'s `isMailbox`.
107
+ **Fix:** give the bare address, without brackets or a display name:
108
+
109
+ ```ts
110
+ await queue.enqueue(message, { from: 'mary@example.net', to: ['joe@example.com'] });
111
+ ```
112
+
113
+ For a bounce of your own, the null sender is `from: ''`.
114
+
115
+ ### `QueueError: to must hold one recipient or more`
116
+
117
+ **Code:** `INVALID`. `to` is an empty array, or not an array nor a
118
+ string. Give one address or more.
119
+
120
+ ### `QueueError: The envelope is an object: { from, to }`
121
+
122
+ **Code:** `INVALID`. `enqueue`'s second argument is missing.
123
+
124
+ ### `QueueError: The message is a Uint8Array, a string or a ReadableStream<Uint8Array>`
125
+
126
+ **Code:** `INVALID`. Give the whole message; a `Blob` is
127
+ `new Uint8Array(await blob.arrayBuffer())` or `blob.stream()`.
128
+
129
+ ### `QueueError: The message stream must give Uint8Array chunks`
130
+
131
+ **Code:** `INVALID`. A stream of strings: pipe it through a
132
+ `TextEncoderStream` first.
133
+
134
+ ### `QueueError: The message has a bare CR or LF at byte …: every line must end in CRLF`
135
+
136
+ **Code:** `INVALID`.
137
+ **When:** `enqueue`, with a CR not followed by an LF, or an LF not after
138
+ a CR — often a message written with `\n` line ends.
139
+ **Why:** `sendMail` refuses such a message (`BARE_LINE_BREAK`, against
140
+ SMTP smuggling), which would fail every recipient at delivery.
141
+ **Fix:** write the message with CRLF before you sign and enqueue it:
142
+
143
+ ```ts
144
+ const message = text.replace(/\r?\n/g, '\r\n');
145
+ ```
146
+
147
+ ## Creating the queue
148
+
149
+ ### `QueueError: The 'mx' route needs a resolver, such as @bumail/dns's nodeResolver()`
150
+
151
+ **Code:** `INVALID`.
152
+ **When:** `route` is `'mx'` (the default) or a `routes` entry is, with no
153
+ `resolver`.
154
+ **Fix:**
155
+
156
+ ```ts
157
+ import { nodeResolver } from '@bumail/dns';
158
+ createQueue({ store, hostname, resolver: nodeResolver() });
159
+ ```
160
+
161
+ or route everything through a smarthost (`route: { host, port, auth }`).
162
+
163
+ ### `QueueError: hostname must be this server's public host name, not …`
164
+
165
+ **Code:** `INVALID`. `hostname` is missing, or not a host name
166
+ (`mail.example.net`): no space, no address literal.
167
+
168
+ ### `QueueError: route must be 'mx' or a smarthost with a host`
169
+
170
+ **Code:** `INVALID`, also as `routes["…"] must be …`. A smarthost needs
171
+ `host`, a name or an address with no space or slash:
172
+ `{ host: 'smtp.provider.example', port: 587 }`.
173
+
174
+ ### `QueueError: route.auth needs tls: 'required', the default with auth: with tls: '…' the password would go to a server whose certificate is not checked`
175
+
176
+ **Code:** `INVALID`, also as `routes["…"].auth needs …`.
177
+ **When:** a smarthost with `auth` and `tls: 'opportunistic'` or
178
+ `tls: 'none'`.
179
+ **Why:** credentials go only over TLS whose certificate checked out, as
180
+ `sendMail` requires; there is no way out for a test server here.
181
+ **Fix:** leave `tls` out (it is `'required'` with `auth`), and give the
182
+ server's CA with `ca` if its certificate is not publicly trusted:
183
+
184
+ ```ts
185
+ route: { host: 'smtp.provider.example', port: 587, auth: { username, password } },
186
+ ```
187
+
188
+ ### `QueueError: route.auth.username must be a non-empty string`, and the other smarthost options
189
+
190
+ **Code:** `INVALID`, also under `routes["…"].`: `auth.password must be a
191
+ non-empty string`, `auth.mechanism must be 'PLAIN' or 'LOGIN', not …`,
192
+ `port must be an integer from 1 to 65535, not …`, `tls must be
193
+ 'opportunistic', 'required' or 'none', not …`, `secure must be true or
194
+ false, not …`, and `secure is TLS from the first byte: it cannot go with
195
+ tls: 'none'`. The same checks `sendMail` makes, made once by
196
+ `createQueue` rather than at every delivery. `mxPort` and `mxTls` are
197
+ checked the same way.
198
+
199
+ ### `QueueError: timeouts.… must be a number of seconds above 0 and at most 2147483, not …`
200
+
201
+ **Code:** `INVALID`, also as `deadline must be …` or `timeouts must be
202
+ an object of seconds`. Timeouts are seconds,
203
+ as `sendMail` takes them, not milliseconds: `timeouts: { connect: 30 }`.
204
+
205
+ ### `QueueError: store must be a QueueStore, such as MemoryQueueStore`
206
+
207
+ **Code:** `INVALID`. Give `new MemoryQueueStore()` (from
208
+ `@bumail/queue/memory`), `SqliteQueueStore.open({ directory })` (from
209
+ `@bumail/queue/sqlite`), or a store of your own.
210
+
211
+ ### `QueueError: … must be an integer of at least …, not …`
212
+
213
+ **Code:** `INVALID`, also as `… must be an integer from … to …, not …`
214
+ or `… must be a number from … to …, not …`. A number option out of its range:
215
+ `concurrency`, `perDomain`, `leaseMs` (from 1000 to 6442450941),
216
+ `pollInterval` (at most 2147483647, the longest a timer waits),
217
+ `retry.*` (`retry.max` at least `retry.first`, `retry.jitter` from 0 to
218
+ 1), `dsn.delayAfter`, `limits.*` (`limits.maxReplyText` from 64 to
219
+ 900). Times are milliseconds.
220
+
221
+ ### `QueueError: dsn.returnContent must be 'headers' or 'full', not …`
222
+
223
+ **Code:** `INVALID`.
224
+
225
+ ### `QueueError: createQueue() takes an options object`
226
+
227
+ **Code:** `INVALID`.
228
+
229
+ ### `QueueError: owner must be a non-empty string`
230
+
231
+ **Code:** `INVALID`. Leave `owner` out for a random one, or give each
232
+ worker its own name.
233
+
234
+ ## The `error` event
235
+
236
+ ### `QueueError: The lease on … was lost before its outcome was recorded; another worker will try it again`
237
+
238
+ **Code:** `LEASE_LOST`.
239
+ **When:** a delivery took longer than its lease, and another worker
240
+ claimed the item meanwhile. The outcome of this attempt is not recorded,
241
+ so the recipients it reached may get the message twice.
242
+ **Why:** the lease is renewed every third of `leaseMs`; it is lost when
243
+ the process stalled longer than that, or the store could not be reached.
244
+ **Fix:** a longer `leaseMs`, or find what stalled the process.
245
+
246
+ ### `QueueError: The lease on … was lost while it was delivered`
247
+
248
+ **Code:** `LEASE_LOST`. A renewal found the lease taken: the same cause as
249
+ above, seen sooner.
250
+
251
+ ### `SmtpError: sendMail(): …` (`INVALID_OPTION`), with recipients deferred as `4.3.5`
252
+
253
+ **When:** `sendMail` refused the options of a session — the route, the
254
+ timeouts, `hostname` as EHLO — though `createQueue` took them.
255
+ **Why:** a configuration only `sendMail` can check, or a `send` of your
256
+ own that refuses its options. It is the route's fault, not the
257
+ recipient's: the recipients are deferred as `4.3.5` (system incorrectly
258
+ configured), not failed, so no DSN goes out for it until
259
+ `retry.giveUpAfter` (5 days by default), when they fail as `4.4.7` with a
260
+ DSN like any deferred recipient.
261
+ **Fix:** read the message, fix the option, restart: the deferred items
262
+ go out on their next attempt, or at once with `retryNow`.
263
+
264
+ ### `SQLiteError: database is locked`, or another store error, during a delivery
265
+
266
+ **When:** the `error` event, with the item's `id`, while it is delivered.
267
+ **Why:** a lease renewal failed: the database busy past `busyTimeout`,
268
+ or the store unreachable. The next renewal, a third of `leaseMs` later,
269
+ tries again; only a renewal that finds the lease taken stops (as
270
+ `LEASE_LOST`).
271
+ **Fix:** a longer `busyTimeout` when several processes write a lot; a
272
+ store that keeps failing will lose the lease once it expires.
273
+
274
+ ## The `bun:sqlite` store
275
+
276
+ ### `QueueError: A SQLite queue store needs a directory`
277
+
278
+ **Code:** `INVALID`. `SqliteQueueStore.open({ directory })`.
279
+
280
+ ### `QueueError: The queue at "…" cannot be opened: …`
281
+
282
+ **Code:** `INVALID`. The directory cannot be made or written, or
283
+ `queue.sqlite` is not a database (`file is not a database`). Check the
284
+ path and its owner. A directory the store makes is 0700; one that
285
+ already exists keeps its mode, so make it 0700 yourself unless a group
286
+ should read the queue.
287
+
288
+ ### `QueueError: The database is at schema version …, newer than this store's …`
289
+
290
+ **Code:** `INVALID`. A newer version of the package wrote the database:
291
+ upgrade this one.
292
+
293
+ ### `QueueError: busyTimeout must be an integer of at least 0, not …`
294
+
295
+ **Code:** `INVALID`. Milliseconds a write waits for another process.
296
+
297
+ ### `QueueError: The queue store is closed`
298
+
299
+ **Code:** `CLOSED`. The store was used after `close()`: call
300
+ `queue.stop()` first, then `store.close()`.
301
+
302
+ ## Listing
303
+
304
+ ### `QueueError: limit must be an integer from 1 to 1000, not …`
305
+
306
+ **Code:** `INVALID`. `queue.list()` (and a store's `list`) gives at most
307
+ 1000 items a page: page through with `offset`.
308
+
309
+ ```ts
310
+ for (let offset = 0; ; offset += 1000) {
311
+ const page = await queue.list({ offset, limit: 1000 });
312
+ if (page.length === 0) break;
313
+ }
314
+ ```
315
+
316
+ ### `QueueError: offset must be an integer of at least 0, not …`
317
+
318
+ **Code:** `INVALID`. `offset` counts items from the first due, from 0.
319
+
320
+ ## A store's own checks
321
+
322
+ **Code:** `INVALID`, from a store called directly with what the queue
323
+ never gives it. Each message names what is wrong:
324
+
325
+ - `QueueError: … must be a finite number` (`createdAt`, `now`,
326
+ `nextAttemptAt`)
327
+ - `QueueError: owner must be a non-empty string`
328
+ - `QueueError: leaseMs must be a positive number`
329
+ - `QueueError: maxItems must be an integer of at least 1, not …`
330
+ - `QueueError: A new item is an object`
331
+ - `QueueError: from must be a string`
332
+ - `QueueError: to must be a non-empty array of addresses`
333
+ - `QueueError: message must be a Uint8Array`
334
+ - `QueueError: A claim is an object`
335
+ - `QueueError: An attempt result is an object`
336
+ - `QueueError: attempts must be an integer of at least 0`
337
+ - `QueueError: delayNotified must be true or false`
338
+ - `QueueError: recipients must be an array of { address, status:
339
+ delivered, deferred or failed }`
340
+
341
+ A list's `offset` and `limit` are checked as under [Listing](#listing).
342
+
343
+ ## Delivery
344
+
345
+ ### A recipient stays `deferred` with `4.4.1` or `4.4.2`
346
+
347
+ **When:** the `deferred` event's `reply` has no `code`, a `status` of
348
+ `4.4.1` (no answer) or `4.4.2` (connection lost or timed out), and a
349
+ text such as `Could not connect to …`.
350
+ **Why:** usually outbound port 25 is blocked by your network or your
351
+ cloud provider.
352
+ **Fix:** route through a smarthost on port 587 or 465:
353
+
354
+ ```ts
355
+ createQueue({ store, hostname, route: { host: 'smtp.provider.example', port: 587, auth } });
356
+ ```
357
+
358
+ ### Every delivery fails at once with `5.7.1`, or with a reply naming your IP or EHLO name
359
+
360
+ **Why:** the receiving server refused your server itself: no reverse
361
+ DNS, a `hostname` that does not match it, an IP on a blocklist, or a
362
+ message that fails SPF, DKIM or DMARC.
363
+ **Fix:** give `hostname` the name your IP's PTR record holds, publish SPF
364
+ and DKIM for your domain, and sign every message before you enqueue it.
365
+
366
+ ### A recipient fails with `5.1.3`, `The address is not one SMTP can carry`
367
+
368
+ **When:** an item a store holds has a recipient `sendMail` would refuse:
369
+ written there by other code, since `enqueue` refuses it.
370
+ **Why:** it fails alone, so the session for its domain still goes ahead
371
+ for the other recipients.
372
+ **Fix:** enqueue through `queue.enqueue`, or check each address with
373
+ `@bumail/smtp/client`'s `isMailbox` before a store of your own keeps it.
374
+
375
+ ### Every recipient fails with `5.1.7`, `The sender's address is not one SMTP can carry`
376
+
377
+ **When:** an item a store holds has a sender (`from`) that is neither
378
+ `''` nor one `isMailbox` takes: written there by other code, since
379
+ `enqueue` refuses it.
380
+ **Why:** no session can carry it, so every recipient fails at once
381
+ rather than be deferred until `retry.giveUpAfter`. No session is opened
382
+ and no `perDomain` slot is taken, so the item never waits behind a busy
383
+ domain, and its recipients fail even while `stop()` runs. The failure DSN goes
384
+ to that same address, so it fails in turn, as `5.1.3`, and causes no
385
+ other.
386
+ **Fix:** enqueue through `queue.enqueue`, or check the sender with
387
+ `@bumail/smtp/client`'s `isMailbox` (or let it be `''`) before a store of
388
+ your own keeps it.
389
+
390
+ ### `retryNow` returns `false` for an item that is in the queue
391
+
392
+ **Why:** a worker is delivering it: its lease is held. That attempt's
393
+ outcome sets the next one, so `retryNow` changes nothing.
394
+ **Fix:** call it again once the attempt is over (the `deferred` event).
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@bumail/queue",
3
+ "version": "0.1.0",
4
+ "description": "The outbound mail queue of a mail server: every recipient's state, retries with back-off, delivery status notifications (RFC 3464), leases for several workers, and a memory and a bun:sqlite store, delivering through @bumail/smtp/client",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "docs",
12
+ "README.md",
13
+ "package.json",
14
+ "LICENSE"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./memory": {
23
+ "types": "./dist/memory/index.d.ts",
24
+ "import": "./dist/memory/index.js",
25
+ "default": "./dist/memory/index.js"
26
+ },
27
+ "./sqlite": {
28
+ "types": "./dist/sqlite/index.d.ts",
29
+ "import": "./dist/sqlite/index.js",
30
+ "default": "./dist/sqlite/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/softistx/bumail.git",
37
+ "directory": "packages/queue"
38
+ },
39
+ "publishConfig": {
40
+ "registry": "https://registry.npmjs.org",
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "build": "bun run ../../build.ts",
45
+ "test": "bun test src",
46
+ "typecheck": "tsc --noEmit"
47
+ },
48
+ "bumail": {
49
+ "entrypoints": [
50
+ "src/index.ts",
51
+ "src/memory/index.ts",
52
+ "src/sqlite/index.ts"
53
+ ]
54
+ },
55
+ "devDependencies": {
56
+ "@bumail/dns": "^0.1.1",
57
+ "@bumail/mime": "^0.1.2",
58
+ "@bumail/smtp": "^0.3.0",
59
+ "@types/bun": "^1.4.2"
60
+ },
61
+ "peerDependencies": {
62
+ "@bumail/mime": "^0.1.2",
63
+ "@bumail/smtp": "^0.3.0",
64
+ "typescript": "^6.0.3 || ^7.0.0"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "typescript": {
68
+ "optional": true
69
+ }
70
+ }
71
+ }