@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.
- package/README.md +27 -3
- package/dist/chunks/{index-3pt7rc59.js → index-9yp15arf.js} +98 -25
- package/dist/chunks/index-9yp15arf.js.map +14 -0
- package/dist/chunks/index-r8wb840a.js +24 -0
- package/dist/chunks/index-r8wb840a.js.map +10 -0
- package/dist/contract/checks.d.ts +2 -6
- package/dist/contract/checks.d.ts.map +1 -1
- package/dist/contract/dsn-checks.d.ts +4 -0
- package/dist/contract/dsn-checks.d.ts.map +1 -0
- package/dist/contract/dsn-column.d.ts +12 -0
- package/dist/contract/dsn-column.d.ts.map +1 -0
- package/dist/contract/queue-store.d.ts +2 -1
- package/dist/contract/queue-store.d.ts.map +1 -1
- package/dist/contract/storable.d.ts +7 -0
- package/dist/contract/storable.d.ts.map +1 -0
- package/dist/contract/types.d.ts +52 -0
- package/dist/contract/types.d.ts.map +1 -1
- package/dist/dsn/build.d.ts +2 -0
- package/dist/dsn/build.d.ts.map +1 -1
- package/dist/dsn/delivered.d.ts +39 -0
- package/dist/dsn/delivered.d.ts.map +1 -0
- package/dist/dsn/fields.d.ts +11 -2
- package/dist/dsn/fields.d.ts.map +1 -1
- package/dist/dsn/human.d.ts +7 -0
- package/dist/dsn/human.d.ts.map +1 -0
- package/dist/dsn/policy.d.ts +17 -0
- package/dist/dsn/policy.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +474 -300
- package/dist/index.js.map +20 -15
- package/dist/memory/index.js +1 -1
- package/dist/memory/index.js.map +1 -1
- package/dist/postgres/index.js +17 -9
- package/dist/postgres/index.js.map +6 -6
- package/dist/postgres/rows.d.ts +6 -1
- package/dist/postgres/rows.d.ts.map +1 -1
- package/dist/postgres/schema.d.ts.map +1 -1
- package/dist/postgres/statements.d.ts +9 -8
- package/dist/postgres/statements.d.ts.map +1 -1
- package/dist/postgres/store.d.ts.map +1 -1
- package/dist/queue/attempt.d.ts +7 -7
- package/dist/queue/attempt.d.ts.map +1 -1
- package/dist/queue/deliver.d.ts.map +1 -1
- package/dist/queue/envelope.d.ts +24 -4
- package/dist/queue/envelope.d.ts.map +1 -1
- package/dist/queue/events.d.ts +8 -1
- package/dist/queue/events.d.ts.map +1 -1
- package/dist/queue/fallback-session.d.ts +22 -0
- package/dist/queue/fallback-session.d.ts.map +1 -0
- package/dist/queue/notify.d.ts +6 -3
- package/dist/queue/notify.d.ts.map +1 -1
- package/dist/queue/outcome.d.ts.map +1 -1
- package/dist/queue/queue.d.ts.map +1 -1
- package/dist/queue/route.d.ts +7 -0
- package/dist/queue/route.d.ts.map +1 -1
- package/dist/queue/session.d.ts +14 -0
- package/dist/queue/session.d.ts.map +1 -0
- package/dist/queue/settle.d.ts.map +1 -1
- package/dist/redis/index.js +26 -13
- package/dist/redis/index.js.map +5 -5
- package/dist/redis/items.d.ts +2 -0
- package/dist/redis/items.d.ts.map +1 -1
- package/dist/redis/scripts.d.ts +1 -1
- package/dist/redis/scripts.d.ts.map +1 -1
- package/dist/redis/store.d.ts.map +1 -1
- package/dist/sqlite/index.js +15 -8
- package/dist/sqlite/index.js.map +5 -5
- package/dist/sqlite/rows.d.ts +2 -1
- package/dist/sqlite/rows.d.ts.map +1 -1
- package/dist/sqlite/schema.d.ts.map +1 -1
- package/dist/sqlite/store.d.ts.map +1 -1
- package/docs/guide.md +97 -7
- package/docs/roadmap.md +14 -7
- package/docs/troubleshooting.md +52 -1
- package/package.json +3 -3
- package/dist/chunks/index-3pt7rc59.js.map +0 -12
package/docs/guide.md
CHANGED
|
@@ -59,6 +59,12 @@ const item = await queue.enqueue(message, { from: 'mary@example.net', to: ['joe@
|
|
|
59
59
|
for the null sender of a bounce you write yourself. `to` is one address
|
|
60
60
|
or an array. Each recipient is kept once: two addresses that differ
|
|
61
61
|
only in the case of their domain are one.
|
|
62
|
+
- **The DSN parameters** (RFC 3461) are optional: `ret` (`'FULL'` or
|
|
63
|
+
`'HDRS'`), `envid` (1 to 100 printable ASCII characters, 100 or fewer once xtext-encoded) and
|
|
64
|
+
`recipients`, a list of `{ address, notify?, orcpt? }` matched to `to`
|
|
65
|
+
by address, the shape of `@bumail/smtp`'s `Envelope.recipients`. They
|
|
66
|
+
are kept with the item by every store and described in
|
|
67
|
+
[Delivery status notifications](#delivery-status-notifications).
|
|
62
68
|
- **What comes back** is the item: an `id`, every recipient `pending`,
|
|
63
69
|
`nextAttemptAt` now. `start()` wakes at once to deliver it.
|
|
64
70
|
|
|
@@ -190,6 +196,26 @@ An explicit route is still followed: if the operator sets a smarthost for
|
|
|
190
196
|
the domain (`route` or `routes`), a marked recipient is sent there at the
|
|
191
197
|
next attempt. Only the automatic fallback is blocked.
|
|
192
198
|
|
|
199
|
+
The reverse holds too. When the *fallback smarthost's* session ends after
|
|
200
|
+
the final dot with no reply (`afterData`), the smarthost may hold the
|
|
201
|
+
message, and an attempt by the MX would send a second copy. The queue
|
|
202
|
+
records `mayHaveReachedFallback: true` on the recipient (same rules as
|
|
203
|
+
`mayHaveReachedMx`: every recipient the smarthost did not refuse at
|
|
204
|
+
`RCPT`, kept once recorded, never `false`). While a `fallback` is
|
|
205
|
+
configured, every later attempt leaves the MX out for a marked recipient
|
|
206
|
+
and sends it to the smarthost in a session of its own, with no MX session
|
|
207
|
+
at all when every recipient of the domain is marked. If the smarthost
|
|
208
|
+
refuses it, the recipient stays `deferred` (no DSN, since the earlier copy
|
|
209
|
+
may still arrive) with the normal back-off, until `retry.giveUpAfter`
|
|
210
|
+
fails it as `4.4.7` with the usual failed DSN, whose diagnostic adds that
|
|
211
|
+
an earlier attempt may have reached the fallback smarthost. The `deferred`
|
|
212
|
+
and `failed` events carry `mayHaveReachedFallback: true`. If `fallback` is
|
|
213
|
+
no longer configured, a marked recipient goes by MX as before. A recipient
|
|
214
|
+
with both marks (possible only if `fallback` was removed and added again)
|
|
215
|
+
follows `mayHaveReachedMx`: it stays on the MX and never falls back. As for the
|
|
216
|
+
MX mark, items deferred before this version are not backfilled and all
|
|
217
|
+
workers on a store are upgraded together.
|
|
218
|
+
|
|
193
219
|
Two things to know when upgrading from 0.6. A recipient deferred under
|
|
194
220
|
0.6 after the final dot carries no mark and is not backfilled: it may
|
|
195
221
|
still fall back once, as it did. And all the workers on one store are
|
|
@@ -343,9 +369,56 @@ Last-Attempt-Date: Thu, 01 Oct 2026 12:00:03 +0000
|
|
|
343
369
|
cut after the last whole line within that bound, and every line past
|
|
344
370
|
998 bytes (RFC 5322 §2.1.1) is cut there, never inside a UTF-8
|
|
345
371
|
character, so the DSN itself is never refused for a long line.
|
|
372
|
+
- **`NOTIFY`** (RFC 3461 §4.1) filters them, per recipient:
|
|
373
|
+
`'NEVER'` sends none; absent, a failure and a delay, as before; a
|
|
374
|
+
list, only those events (`SUCCESS`, `FAILURE`, `DELAY`). The
|
|
375
|
+
`failed` and `dsn` events still fire; only the DSN is left out.
|
|
376
|
+
- **`SUCCESS`** (§5.2.2): when a recipient asked for it and the next hop
|
|
377
|
+
did not list `DSN` in its EHLO (`SendMailResult.dsnOffered` is `false`),
|
|
378
|
+
the queue sends a `relayed` DSN (`Action: relayed`, status `2.0.0`):
|
|
379
|
+
the message was handed on and no further notice can follow. When the
|
|
380
|
+
next hop offered DSN, it took the parameters and the responsibility, and
|
|
381
|
+
the queue sends none. A `dsn` event of kind `relayed` tells it.
|
|
382
|
+
- **`RET`** (§4.3): `HDRS` returns the header fields only, `FULL` the
|
|
383
|
+
whole message (still bounded by `limits.maxDsnReturn`, headers when it
|
|
384
|
+
is larger); absent, `dsn.returnContent`. It speaks of failure and delay DSNs
|
|
385
|
+
(§4.3): a `relayed` or `delivered` DSN returns the header fields
|
|
386
|
+
whatever `RET` says.
|
|
387
|
+
- **`ENVID` and `ORCPT`** (RFC 3464 §2.2.1, §2.3.1): `Original-Envelope-Id`
|
|
388
|
+
in the per-message fields and `Original-Recipient` (`rfc822; addr`) in
|
|
389
|
+
the recipient's, when the envelope gave them. An `ORCPT` too long for a
|
|
390
|
+
header line (over 900 characters once escaped) is left out of the DSN,
|
|
391
|
+
which is still sent.
|
|
392
|
+
- **Passed on**: `ret`, `envid` and the recipients' `notify` and `orcpt`
|
|
393
|
+
go to `sendMail`, which sends them when the next hop lists `DSN`.
|
|
346
394
|
- **Its From** is `Mail Delivery System <postmaster@<hostname>>`, or
|
|
347
395
|
`dsn.from`. It carries `Auto-Submitted: auto-replied` (RFC 3834).
|
|
348
396
|
|
|
397
|
+
### The `delivered` DSN of a local delivery
|
|
398
|
+
|
|
399
|
+
When the application puts a message in a local mailbox itself, it writes
|
|
400
|
+
the `delivered` DSN (`Action: delivered`, RFC 3461 §5.2.3) with
|
|
401
|
+
`deliveredDsn()` and enqueues it from the null sender:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import { deliveredDsn } from '@bumail/queue';
|
|
405
|
+
|
|
406
|
+
const dsn = deliveredDsn({
|
|
407
|
+
reportingMta: 'mail.example.net',
|
|
408
|
+
from: 'postmaster@example.net',
|
|
409
|
+
to: envelope.from, // the reverse-path of the delivered message
|
|
410
|
+
arrival: received,
|
|
411
|
+
recipients: [{ address: 'joe@example.net', notify: ['SUCCESS'], orcpt }],
|
|
412
|
+
envid,
|
|
413
|
+
original: message, // optional; without it no third part
|
|
414
|
+
});
|
|
415
|
+
if (dsn) await queue.enqueue(dsn, { from: '', to: envelope.from });
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
It returns `undefined` when there is nothing to send: the reverse-path is
|
|
419
|
+
null, or no recipient asked for `SUCCESS`. Only the recipients that asked
|
|
420
|
+
are listed.
|
|
421
|
+
|
|
349
422
|
Nothing from outside reaches a header field raw: a reply's text, a host
|
|
350
423
|
name and an address have their control characters taken out (CR and LF
|
|
351
424
|
included), the `message/delivery-status` part is 7-bit (a UTF-8 address
|
|
@@ -423,9 +496,9 @@ off(); // stops listening
|
|
|
423
496
|
| event | payload |
|
|
424
497
|
| --- | --- |
|
|
425
498
|
| `delivered` | `{ id, from, recipient, reply?, attempts, route?, fallback?, mxReply? }`: `route` is `'mx'` or `'smarthost'`, `fallback` is `true` after the [fallback](#falling-back-to-a-smarthost), `mxReply` the MX's reply that led to it |
|
|
426
|
-
| `deferred` | the same, and `nextAttemptAt`, and `mayHaveReachedMx: true` when the MX may hold a copy ([why](#falling-back-to-a-smarthost)) |
|
|
427
|
-
| `failed` | the same as `delivered`, and `mayHaveReachedMx
|
|
428
|
-
| `dsn` | `{ kind: 'delayed' \| 'failed', id, of, to, recipients }`: `id` is the DSN's own item, `of` the item it reports on |
|
|
499
|
+
| `deferred` | the same, and `nextAttemptAt`, and `mayHaveReachedMx: true` when the MX may hold a copy, `mayHaveReachedFallback: true` when the fallback smarthost may ([why](#falling-back-to-a-smarthost)) |
|
|
500
|
+
| `failed` | the same as `delivered`, and `mayHaveReachedMx` and `mayHaveReachedFallback` as for `deferred` |
|
|
501
|
+
| `dsn` | `{ kind: 'delayed' \| 'failed' \| 'relayed', id, of, to, recipients }`: `id` is the DSN's own item, `of` the item it reports on |
|
|
429
502
|
| `error` | `{ error, id? }`: a store that failed (a lease renewal included), a DSN that could not be enqueued, a lease lost (`LEASE_LOST`), a message the store lost (`MESSAGE_UNREADABLE`), a route `sendMail` refused (`INVALID_OPTION`) |
|
|
430
503
|
|
|
431
504
|
Events about an outcome come once it is recorded, in this order: a
|
|
@@ -600,7 +673,17 @@ GRANT SELECT, INSERT, UPDATE, DELETE
|
|
|
600
673
|
```
|
|
601
674
|
|
|
602
675
|
After an upgrade that adds a migration, run that step again before the
|
|
603
|
-
workers start: theirs cannot make the change.
|
|
676
|
+
workers start: theirs cannot make the change. The second migration adds
|
|
677
|
+
one nullable `dsn jsonb` column to the items table (the item's `RET` and
|
|
678
|
+
`ENVID`); existing rows keep `NULL` and are delivered as before. A
|
|
679
|
+
worker of the previous version *started* after the migration is refused
|
|
680
|
+
(`schema version 2, newer than this store's 1`); one *still running*
|
|
681
|
+
keeps claiming items, ignores `NOTIFY`, `RET` and `ENVID`, and drops the
|
|
682
|
+
recipients' `notify` and `orcpt` when it completes an attempt. So stop
|
|
683
|
+
every old worker first, then start the new ones. The same holds for the
|
|
684
|
+
SQLite store, which migrates itself on open, and for Redis, which needs
|
|
685
|
+
no migration (an item's hash gains `ret` and `envid` fields) but where an
|
|
686
|
+
old worker drops `notify` and `orcpt` just alike.
|
|
604
687
|
|
|
605
688
|
### Several instances and leases
|
|
606
689
|
|
|
@@ -833,6 +916,7 @@ const send: Sender = async (_message, options) => {
|
|
|
833
916
|
port: 25,
|
|
834
917
|
tls: false,
|
|
835
918
|
authenticated: false,
|
|
919
|
+
dsnOffered: true,
|
|
836
920
|
};
|
|
837
921
|
};
|
|
838
922
|
const queue = createQueue({
|
|
@@ -878,13 +962,19 @@ codes, and keeps its promises:
|
|
|
878
962
|
of the lease, in one step and only for the lease's owner; a final
|
|
879
963
|
status is never changed; an item whose every recipient is final is
|
|
880
964
|
dropped with its message.
|
|
881
|
-
- `complete` records a recipient's `mayHaveReachedMx: true`
|
|
882
|
-
`
|
|
965
|
+
- `complete` records a recipient's `mayHaveReachedMx: true` and
|
|
966
|
+
`mayHaveReachedFallback: true` (on its `RecipientUpdate`) and keeps
|
|
967
|
+
them: `get`, `list` and `claim` return it on
|
|
883
968
|
the `RecipientState`, a later outcome without it never clears it, and
|
|
884
969
|
the store never writes `false`. Without it the queue falls back to a
|
|
885
970
|
smarthost for a recipient the MX may already hold. A 0.6 worker drops
|
|
886
971
|
the mark when it completes an attempt, so upgrade every worker on a
|
|
887
972
|
store together.
|
|
973
|
+
- `add` keeps the item's `ret` and `envid` and each recipient's `notify`
|
|
974
|
+
and `orcpt` (`NewQueueItem`), and `get`, `list`, `claim` and `complete`
|
|
975
|
+
return them unchanged, even after an outcome. The contract's
|
|
976
|
+
`checkNewItem` refuses a bad one as `INVALID`. An item stored before
|
|
977
|
+
they existed has none of them and is delivered as before.
|
|
888
978
|
- Times are given by the caller; the store keeps no clock.
|
|
889
979
|
- Nothing it returns is shared with what it keeps.
|
|
890
980
|
- `reschedule` with an owner refuses an empty owner as `INVALID`, even
|
|
@@ -928,5 +1018,5 @@ them yet.
|
|
|
928
1018
|
| `pollInterval` | `number`, to 2147483647 | 5000 | ms between passes of `start()` |
|
|
929
1019
|
| `owner` | `string` | a random UUID | this worker's name on its leases |
|
|
930
1020
|
| `clock` | `{ now(): number }` | `Date.now` | the time |
|
|
931
|
-
| `send` | `Sender` | `sendMail` | delivers one session |
|
|
1021
|
+
| `send` | `Sender` | `sendMail` | delivers one session; a custom one should return `dsnOffered` (leave it out and no `relayed` DSN is ever sent) |
|
|
932
1022
|
| `random` | `() => number` | `Math.random` | the jitter |
|
package/docs/roadmap.md
CHANGED
|
@@ -6,7 +6,7 @@ only number on it.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
-
Nothing
|
|
9
|
+
Nothing is under way besides what is marked shipped below.
|
|
10
10
|
|
|
11
11
|
## Next
|
|
12
12
|
|
|
@@ -20,10 +20,6 @@ Nothing in progress.
|
|
|
20
20
|
connections.
|
|
21
21
|
- **The contract's specs, exported**, so a store written outside this
|
|
22
22
|
package can hold itself to them.
|
|
23
|
-
- **The reverse doubt** — a recipient whose session by the smarthost
|
|
24
|
-
ended after the final dot with no reply is not yet marked either: the
|
|
25
|
-
queue could keep that doubt too, and not send it again by MX (issue
|
|
26
|
-
#144).
|
|
27
23
|
- **A DSN that cannot be lost on a crash** — today a crash between
|
|
28
24
|
recording a failure and enqueuing its DSN loses that DSN; the store
|
|
29
25
|
will take both in one step.
|
|
@@ -32,8 +28,6 @@ Nothing in progress.
|
|
|
32
28
|
|
|
33
29
|
- **REQUIRETLS (RFC 8689)** — a message that asks for TLS all the way is
|
|
34
30
|
never sent in clear, and fails rather than downgrade.
|
|
35
|
-
- **DSN requests (RFC 3461)** — `NOTIFY`, `RET` and `ENVID` given at
|
|
36
|
-
enqueue and passed on, with a "delivered" DSN when asked for.
|
|
37
31
|
- **Retries per recipient** — a schedule of its own for each recipient,
|
|
38
32
|
rather than one per message.
|
|
39
33
|
- **A history** — delivered and failed items kept for a while, for an
|
|
@@ -57,11 +51,24 @@ Nothing in progress.
|
|
|
57
51
|
|
|
58
52
|
### Unreleased
|
|
59
53
|
|
|
54
|
+
- **DSN requests (RFC 3461)**: the queue honours a sender's `NOTIFY`, `RET`,
|
|
55
|
+
`ENVID` and `ORCPT` given at enqueue. `NOTIFY=NEVER` silences failure
|
|
56
|
+
DSNs; a message handed to a next hop without DSN is reported with a
|
|
57
|
+
`relayed` DSN; DSNs carry `Original-Envelope-Id` and
|
|
58
|
+
`Original-Recipient`; and `deliveredDsn()` builds the `delivered` DSN for
|
|
59
|
+
a local delivery. The SQLite and PostgreSQL stores run migration 2: stop
|
|
60
|
+
every old worker before starting the new ones.
|
|
60
61
|
- **The MX's doubt is remembered** — a recipient whose session by MX ended
|
|
61
62
|
after the final dot with no reply is marked `mayHaveReachedMx`, kept by
|
|
62
63
|
every store: no later attempt falls back to the smarthost on its own, it
|
|
63
64
|
stays on the MX until `retry.giveUpAfter` (a `4.4.7` DSN that says so),
|
|
64
65
|
and the `deferred` and `failed` events carry the mark.
|
|
66
|
+
- **The fallback smarthost's doubt is remembered** — a recipient whose
|
|
67
|
+
session by the fallback smarthost ended after the final dot with no reply
|
|
68
|
+
is marked `mayHaveReachedFallback`, kept by every store: later attempts
|
|
69
|
+
skip the MX for it and go to the smarthost again, until
|
|
70
|
+
`retry.giveUpAfter` (a `4.4.7` DSN that says so), and the `deferred` and
|
|
71
|
+
`failed` events carry the mark.
|
|
65
72
|
- **Only the recipients the MX did not refuse are in doubt** — in a session that
|
|
66
73
|
failed after the final dot, a recipient the MX refused at `RCPT` is
|
|
67
74
|
settled from its own reply (5xx failed, 4xx deferred) and not marked, so
|
package/docs/troubleshooting.md
CHANGED
|
@@ -11,6 +11,7 @@ parts shown as … vary.
|
|
|
11
11
|
- [`QueueError: The queue holds … items, its limit`](#queueerror-the-queue-holds--items-its-limit)
|
|
12
12
|
- [`QueueError: … must be an address, local@domain, not "…"`](#queueerror--must-be-an-address-localdomain-not-)
|
|
13
13
|
- [`QueueError: to must hold one recipient or more`](#queueerror-to-must-hold-one-recipient-or-more)
|
|
14
|
+
- [`QueueError: ret must be 'FULL' or 'HDRS'`, and the other DSN parameters](#queueerror-ret-must-be-full-or-hdrs-and-the-other-dsn-parameters)
|
|
14
15
|
- [`QueueError: The envelope is an object: { from, to }`](#queueerror-the-envelope-is-an-object--from-to-)
|
|
15
16
|
- [`QueueError: The message is a Uint8Array, a string or a ReadableStream<Uint8Array>`](#queueerror-the-message-is-a-uint8array-a-string-or-a-readablestreamuint8array)
|
|
16
17
|
- [`QueueError: The message stream must give Uint8Array chunks`](#queueerror-the-message-stream-must-give-uint8array-chunks)
|
|
@@ -49,6 +50,7 @@ parts shown as … vary.
|
|
|
49
50
|
- [`QueueError: A SQLite queue store needs a directory`](#queueerror-a-sqlite-queue-store-needs-a-directory)
|
|
50
51
|
- [`QueueError: The queue at "…" cannot be opened: …`](#queueerror-the-queue-at--cannot-be-opened-)
|
|
51
52
|
- [`QueueError: The database is at schema version …, newer than this store's …`](#queueerror-the-database-is-at-schema-version--newer-than-this-stores-)
|
|
53
|
+
- [`QueueError: The database is at schema version 2, newer than this store's 1`](#queueerror-the-database-is-at-schema-version-2-newer-than-this-stores-1)
|
|
52
54
|
- [`QueueError: busyTimeout must be an integer of at least 0, not …`](#queueerror-busytimeout-must-be-an-integer-of-at-least-0-not-)
|
|
53
55
|
- [`QueueError: The queue store is closed`](#queueerror-the-queue-store-is-closed)
|
|
54
56
|
|
|
@@ -148,6 +150,36 @@ For a bounce of your own, the null sender is `from: ''`.
|
|
|
148
150
|
**Code:** `INVALID`. `to` is an empty array, or not an array nor a
|
|
149
151
|
string. Give one address or more.
|
|
150
152
|
|
|
153
|
+
### `QueueError: ret must be 'FULL' or 'HDRS'`, and the other DSN parameters
|
|
154
|
+
|
|
155
|
+
**Code:** `INVALID`. The envelope's DSN parameters (RFC 3461) are
|
|
156
|
+
checked at `enqueue`, and by every store at `add`, never at delivery:
|
|
157
|
+
|
|
158
|
+
- `ret must be 'FULL' or 'HDRS'`: `RET` is one of the two.
|
|
159
|
+
- `envid must be 1 to 100 printable ASCII characters, at most 100 once encoded as xtext`: the decoded
|
|
160
|
+
`ENVID`, with no control character, whose xtext is 100 characters or fewer
|
|
161
|
+
(RFC 3461 §4.4).
|
|
162
|
+
- `recipients names "…", which is not in to` and `recipients names … twice`:
|
|
163
|
+
each entry of `recipients` matches one address of `to`, once.
|
|
164
|
+
- `notify of … must be 'NEVER' or a list of 'SUCCESS', 'FAILURE', 'DELAY', each once`:
|
|
165
|
+
`NEVER` alone, or a non-empty list without repeats.
|
|
166
|
+
- `orcpt of … must be { type, address }`: both strings, the address not
|
|
167
|
+
empty, or the type is not a token (letters, digits, hyphens).
|
|
168
|
+
- `orcpt of … is over 500 characters once encoded`: `type;` and the xtext of
|
|
169
|
+
the address take at most 500 characters (RFC 3461 §4.2).
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
await queue.enqueue(message, {
|
|
173
|
+
from: 'mary@example.net',
|
|
174
|
+
to: ['joe@example.com'],
|
|
175
|
+
ret: 'HDRS',
|
|
176
|
+
envid: 'QQ314159',
|
|
177
|
+
recipients: [{ address: 'joe@example.com', notify: ['SUCCESS', 'FAILURE'] }],
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The SMTP server's `Envelope.recipients` is already in this shape.
|
|
182
|
+
|
|
151
183
|
### `QueueError: The envelope is an object: { from, to }`
|
|
152
184
|
|
|
153
185
|
**Code:** `INVALID`. `enqueue`'s second argument is missing.
|
|
@@ -372,7 +404,8 @@ recipient's: the recipients are deferred as `4.3.5` (system incorrectly
|
|
|
372
404
|
configured), not failed, so no DSN goes out for it until
|
|
373
405
|
`retry.giveUpAfter` (5 days by default), when they fail as `4.4.7` with a
|
|
374
406
|
DSN like any deferred recipient (for a recipient marked `mayHaveReachedMx`,
|
|
375
|
-
the failed DSN's diagnostic adds that the message may have been delivered
|
|
407
|
+
the failed DSN's diagnostic adds that the message may have been delivered,
|
|
408
|
+
and likewise for one marked `mayHaveReachedFallback`;
|
|
376
409
|
see [a recipient stays `deferred` with `4.4.1` or `4.4.2`](#a-recipient-stays-deferred-with-441-or-442)).
|
|
377
410
|
**Fix:** read the message, fix the option, restart: the deferred items
|
|
378
411
|
go out on their next attempt, or at once with `retryNow`.
|
|
@@ -469,6 +502,15 @@ should read the queue.
|
|
|
469
502
|
prefix's keys, whose layout version is `<prefix>schema`): upgrade this
|
|
470
503
|
one.
|
|
471
504
|
|
|
505
|
+
### `QueueError: The database is at schema version 2, newer than this store's 1`
|
|
506
|
+
|
|
507
|
+
**Code:** `INVALID`. A worker of a version before the DSN parameters was
|
|
508
|
+
started on a database that a newer one migrated (SQLite and PostgreSQL;
|
|
509
|
+
the second migration adds the `dsn` column). Upgrade it. When rolling
|
|
510
|
+
out, stop every old worker first: one still running is not refused, but
|
|
511
|
+
it ignores `NOTIFY`, `RET` and `ENVID` and drops the recipients' `notify`
|
|
512
|
+
and `orcpt` from the items it completes.
|
|
513
|
+
|
|
472
514
|
### `QueueError: busyTimeout must be an integer of at least 0, not …`
|
|
473
515
|
|
|
474
516
|
**Code:** `INVALID`. Milliseconds a write waits for another process.
|
|
@@ -742,6 +784,15 @@ A `4.4.x` that never falls back to the smarthost you configured is that
|
|
|
742
784
|
mark. Wait for the MX, or cancel the item; to send it by the smarthost
|
|
743
785
|
anyway, accepting a possible second copy, route the domain to it
|
|
744
786
|
explicitly (`routes`).
|
|
787
|
+
|
|
788
|
+
A `4.4.x` recipient that goes to the smarthost again and never to the MX is
|
|
789
|
+
the reverse case: the `fallback` smarthost's own session ended after the final
|
|
790
|
+
dot, so the recipient is marked `mayHaveReachedFallback: true` and the next
|
|
791
|
+
attempts skip the MX (a refusal by the smarthost keeps it `deferred`, with no
|
|
792
|
+
DSN, until `retry.giveUpAfter` fails it as `4.4.7`; the DSN says the fallback
|
|
793
|
+
smarthost may have delivered it). If you removed `fallback` meanwhile, it goes
|
|
794
|
+
by MX again.
|
|
795
|
+
|
|
745
796
|
**Fix:** route through a smarthost on port 587 or 465:
|
|
746
797
|
|
|
747
798
|
```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bumail/queue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
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, a bun:sqlite, a PostgreSQL (Bun.sql) and a Redis (Bun.redis) store, delivering through @bumail/smtp/client",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -67,12 +67,12 @@
|
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@bumail/dns": "^0.2.1",
|
|
69
69
|
"@bumail/mime": "^0.2.0",
|
|
70
|
-
"@bumail/smtp": "^0.
|
|
70
|
+
"@bumail/smtp": "^0.10.0",
|
|
71
71
|
"@types/bun": "^1.4.2"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"@bumail/mime": "^0.2.0",
|
|
75
|
-
"@bumail/smtp": "^0.
|
|
75
|
+
"@bumail/smtp": "^0.10.0",
|
|
76
76
|
"typescript": "^6.0.3 || ^7.0.0"
|
|
77
77
|
},
|
|
78
78
|
"peerDependenciesMeta": {
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/errors.ts", "../src/text.ts", "../src/contract/checks.ts"],
|
|
4
|
-
"sourcesContent": [
|
|
5
|
-
"/**\n * Why the queue or a store refused a call. `INVALID`: an option or an\n * argument it cannot take. `MESSAGE_TOO_BIG`, `TOO_MANY_RECIPIENTS` and\n * `QUEUE_FULL`: a limit `enqueue` enforces. `CLOSED`: a store used after\n * `close()`. `LEASE_LOST`: given to the `error` event when another worker\n * took an item before its outcome was recorded — or, once the lease's\n * expiry passed, when the item is gone: lost or cancelled.\n * `MESSAGE_UNREADABLE`: given to the `error` event when the store holds an\n * item but not its message, whose pending recipients then fail.\n */\nexport type QueueErrorCode =\n\t| 'INVALID'\n\t| 'MESSAGE_TOO_BIG'\n\t| 'TOO_MANY_RECIPIENTS'\n\t| 'QUEUE_FULL'\n\t| 'CLOSED'\n\t| 'LEASE_LOST'\n\t| 'MESSAGE_UNREADABLE';\n\n/** Thrown by the queue and by every store, whichever answers the contract, with the same codes. */\nexport class QueueError extends Error {\n\toverride readonly name = 'QueueError';\n\treadonly code: QueueErrorCode;\n\n\tconstructor(code: QueueErrorCode, message: string) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t}\n}\n\n/** `INVALID`, for an option or an argument. */\nexport const invalid = (message: string) => new QueueError('INVALID', message);\n",
|
|
6
|
-
"/** Text from the other side, made safe to keep and to write in a header field. */\n\n/** C0, DEL and C1: every control character, CR, LF and TAB included. */\nconst isControl = (code: number) =>\n\tcode < 0x20 || (code >= 0x7f && code <= 0x9f);\n\n/** Whether the text holds a control character. */\nexport function hasControl(text: string): boolean {\n\tfor (let i = 0; i < text.length; i++) {\n\t\tif (isControl(text.charCodeAt(i))) return true;\n\t}\n\treturn false;\n}\n\n/** Each control character replaced by `by`. */\nexport function replaceControls(text: string, by: string): string {\n\tlet out = '';\n\tfor (let i = 0; i < text.length; i++) {\n\t\tout += isControl(text.charCodeAt(i)) ? by : (text[i] as string);\n\t}\n\treturn out;\n}\n\n/**\n * Every control character (CR and LF included) as a space, a lone\n * surrogate as U+FFFD, runs of space as one, cut at `max` characters with\n * `...`, never inside a surrogate pair: what a reply leaves in an item,\n * an event and a DSN.\n */\nexport function cleanText(text: string, max: number): string {\n\tconst clean = replaceControls(text.toWellFormed(), ' ')\n\t\t.replace(/ {2,}/g, ' ')\n\t\t.trim();\n\treturn clean.length > max ? `${cut(clean, max - 3)}...` : clean;\n}\n\n/**\n * The first `length` UTF-16 units, never ending inside a surrogate pair:\n * a lone surrogate is text no store can keep (PostgreSQL's `jsonb` refuses\n * it), nor any UTF-8 encode.\n */\nfunction cut(text: string, length: number): string {\n\tconst head = text.slice(0, length);\n\tconst last = head.charCodeAt(head.length - 1);\n\treturn last >= 0xd800 && last <= 0xdbff ? head.slice(0, -1) : head;\n}\n\n/**\n * Text every store can keep: no NUL (PostgreSQL's `text` and `jsonb` hold\n * none) and no lone surrogate. `cleanText`'s output always is.\n */\nexport const isStorable = (text: string): boolean =>\n\ttext.isWellFormed() && !text.includes('\\0');\n\n/** `cleanText`, then anything not printable ASCII as `?`: for a `message/delivery-status` field. */\nexport const asciiText = (text: string, max: number): string =>\n\tcleanText(text, max).replace(/[^ -~]/g, '?');\n",
|
|
7
|
-
"import { invalid } from '../errors';\nimport { isStorable } from '../text';\nimport type {\n\tAttemptResult,\n\tClaimRequest,\n\tDiagnostic,\n\tNewQueueItem,\n\tQueueItem,\n\tQueueListOptions,\n\tRecipientState,\n\tRecipientUpdate,\n} from './types';\n\n/** What every store checks the same way, so each refuses the same input with the same error. */\n\nconst isTime = (value: unknown): value is number =>\n\ttypeof value === 'number' && Number.isFinite(value);\n\n/** A time in milliseconds since the epoch, or `INVALID` naming it. */\nexport function checkTime(name: string, value: unknown): number {\n\tif (!isTime(value)) throw invalid(`${name} must be a finite number`);\n\treturn value;\n}\n\n/**\n * Text a store keeps: refused, the same way by every store, when it holds\n * a NUL or a lone surrogate, which PostgreSQL cannot keep and no UTF-8\n * encode can carry.\n */\nexport function checkStorable(name: string, text: string): void {\n\tif (!isStorable(text)) {\n\t\tthrow invalid(\n\t\t\t`${name} holds a NUL or a lone surrogate, which a store cannot keep`,\n\t\t);\n\t}\n}\n\n/** A lease owner: a non-empty string. */\nexport function checkOwner(owner: unknown): string {\n\tif (typeof owner !== 'string' || owner === '') {\n\t\tthrow invalid('owner must be a non-empty string');\n\t}\n\tcheckStorable('owner', owner);\n\treturn owner;\n}\n\n/** Every text of a recipient's outcome. */\nfunction checkUpdate(update: RecipientUpdate): void {\n\tcheckStorable('A recipient', update.address);\n\tconst mark = update.mayHaveReachedMx as unknown;\n\tif (mark !== undefined && mark !== true) {\n\t\tthrow invalid('mayHaveReachedMx must be true or absent');\n\t}\n\tconst reply = update.reply as Partial<Diagnostic> | undefined;\n\tif (reply === undefined) return;\n\tif (\n\t\ttypeof reply !== 'object' ||\n\t\treply === null ||\n\t\ttypeof reply.text !== 'string'\n\t) {\n\t\tthrow invalid('A reply is an object with a text');\n\t}\n\tcheckStorable('reply.text', reply.text);\n\tfor (const name of ['status', 'host'] as const) {\n\t\tconst value = reply[name];\n\t\tif (value === undefined) continue;\n\t\tif (typeof value !== 'string')\n\t\t\tthrow invalid(`reply.${name} must be a string`);\n\t\tcheckStorable(`reply.${name}`, value);\n\t}\n}\n\nexport function checkNewItem(item: NewQueueItem): void {\n\tif (typeof item !== 'object' || item === null) {\n\t\tthrow invalid('A new item is an object');\n\t}\n\tif (typeof item.from !== 'string') throw invalid('from must be a string');\n\tcheckStorable('from', item.from);\n\tconst to = item.to as unknown;\n\tif (\n\t\t!Array.isArray(to) ||\n\t\tto.length === 0 ||\n\t\t!to.every((a) => typeof a === 'string' && a !== '')\n\t) {\n\t\tthrow invalid('to must be a non-empty array of addresses');\n\t}\n\tfor (const address of to as string[]) checkStorable('A recipient', address);\n\tif (!(item.message instanceof Uint8Array)) {\n\t\tthrow invalid('message must be a Uint8Array');\n\t}\n\tcheckTime('createdAt', item.createdAt);\n}\n\n/** `maxItems`: a positive integer, or nothing. */\nexport function checkMaxItems(max: unknown): number | undefined {\n\tif (max === undefined) return undefined;\n\tif (typeof max !== 'number' || !Number.isInteger(max) || max < 1) {\n\t\tthrow invalid(`maxItems must be an integer of at least 1, not ${max}`);\n\t}\n\treturn max;\n}\n\nexport function checkClaim(request: ClaimRequest): void {\n\tif (typeof request !== 'object' || request === null) {\n\t\tthrow invalid('A claim is an object');\n\t}\n\tcheckOwner(request.owner);\n\tcheckTime('now', request.now);\n\tif (!isTime(request.leaseMs) || request.leaseMs <= 0) {\n\t\tthrow invalid('leaseMs must be a positive number');\n\t}\n}\n\nexport function checkResult(result: AttemptResult): void {\n\tif (typeof result !== 'object' || result === null) {\n\t\tthrow invalid('An attempt result is an object');\n\t}\n\tcheckTime('now', result.now);\n\tcheckTime('nextAttemptAt', result.nextAttemptAt);\n\tif (!Number.isInteger(result.attempts) || result.attempts < 0) {\n\t\tthrow invalid('attempts must be an integer of at least 0');\n\t}\n\tif (typeof result.delayNotified !== 'boolean') {\n\t\tthrow invalid('delayNotified must be true or false');\n\t}\n\tconst statuses = ['delivered', 'deferred', 'failed'];\n\tif (\n\t\t!Array.isArray(result.recipients) ||\n\t\t!result.recipients.every(\n\t\t\t(u) => typeof u?.address === 'string' && statuses.includes(u.status),\n\t\t)\n\t) {\n\t\tthrow invalid(\n\t\t\t'recipients must be an array of { address, status: delivered, deferred or failed }',\n\t\t);\n\t}\n\tfor (const update of result.recipients) checkUpdate(update);\n}\n\n/** The offset and limit of a list: integers, the limit 1 to 1000. */\nexport function checkList(options: QueueListOptions = {}): {\n\toffset: number;\n\tlimit: number;\n} {\n\tconst offset = options.offset ?? 0;\n\tconst limit = options.limit ?? 100;\n\tif (!Number.isInteger(offset) || offset < 0) {\n\t\tthrow invalid(`offset must be an integer of at least 0, not ${offset}`);\n\t}\n\tif (!Number.isInteger(limit) || limit < 1 || limit > 1000) {\n\t\tthrow invalid(`limit must be an integer from 1 to 1000, not ${limit}`);\n\t}\n\treturn { offset, limit };\n}\n\n/** Every recipient is `delivered` or `failed`. */\nexport const isDone = (item: Pick<QueueItem, 'recipients'>): boolean =>\n\titem.recipients.every(\n\t\t(r) => r.status === 'delivered' || r.status === 'failed',\n\t);\n\n/**\n * The item once an attempt is recorded: its recipients' outcomes, a final\n * state never changed again, the schedule, and no lease.\n */\nexport function applyAttempt(\n\titem: QueueItem,\n\tresult: AttemptResult,\n): QueueItem {\n\tconst updates = new Map(result.recipients.map((u) => [u.address, u]));\n\tconst recipients = item.recipients.map((r): RecipientState => {\n\t\tconst update = updates.get(r.address);\n\t\tif (!update || r.status === 'delivered' || r.status === 'failed') return r;\n\t\treturn {\n\t\t\taddress: r.address,\n\t\t\tstatus: update.status,\n\t\t\t...(update.reply ? { reply: update.reply } : {}),\n\t\t\tupdatedAt: result.now,\n\t\t\t...(r.mayHaveReachedMx || update.mayHaveReachedMx\n\t\t\t\t? { mayHaveReachedMx: true as const }\n\t\t\t\t: {}),\n\t\t};\n\t});\n\tconst { lease: _, ...rest } = item;\n\treturn {\n\t\t...rest,\n\t\trecipients,\n\t\tnextAttemptAt: result.nextAttemptAt,\n\t\tattempts: result.attempts,\n\t\tdelayNotified: result.delayNotified,\n\t};\n}\n\n/** A new item as a store first keeps it. */\nexport function newItem(id: string, item: NewQueueItem): QueueItem {\n\treturn {\n\t\tid,\n\t\tfrom: item.from,\n\t\trecipients: item.to.map((address) => ({ address, status: 'pending' })),\n\t\tsize: item.message.length,\n\t\tcreatedAt: item.createdAt,\n\t\tnextAttemptAt: item.createdAt,\n\t\tattempts: 0,\n\t\tdelayNotified: false,\n\t};\n}\n\n/** Whether a lease, if any, leaves the item free to claim at `now`. */\nexport const claimable = (item: QueueItem, now: number): boolean =>\n\titem.nextAttemptAt <= now &&\n\t(item.lease === undefined || item.lease.expiresAt <= now);\n"
|
|
8
|
-
],
|
|
9
|
-
"mappings": "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",
|
|
10
|
-
"debugId": "8430FF293E3942D164756E2164756E21",
|
|
11
|
-
"names": []
|
|
12
|
-
}
|