@bumail/queue 0.12.0 → 0.14.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 (45) hide show
  1. package/README.md +65 -4
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +517 -32
  5. package/dist/index.js.map +23 -14
  6. package/dist/queue/attempt.d.ts.map +1 -1
  7. package/dist/queue/deliver.d.ts +2 -0
  8. package/dist/queue/deliver.d.ts.map +1 -1
  9. package/dist/queue/events.d.ts +10 -0
  10. package/dist/queue/events.d.ts.map +1 -1
  11. package/dist/queue/fallback-session.d.ts.map +1 -1
  12. package/dist/queue/fallback.d.ts.map +1 -1
  13. package/dist/queue/options.d.ts +25 -1
  14. package/dist/queue/options.d.ts.map +1 -1
  15. package/dist/queue/outcome.d.ts.map +1 -1
  16. package/dist/queue/session.d.ts +9 -1
  17. package/dist/queue/session.d.ts.map +1 -1
  18. package/dist/queue/settings/index.d.ts +8 -2
  19. package/dist/queue/settings/index.d.ts.map +1 -1
  20. package/dist/queue/settings/sender.d.ts +16 -0
  21. package/dist/queue/settings/sender.d.ts.map +1 -0
  22. package/dist/queue/settle.d.ts.map +1 -1
  23. package/dist/queue/worker.d.ts.map +1 -1
  24. package/dist/sts/attempt.d.ts +15 -0
  25. package/dist/sts/attempt.d.ts.map +1 -0
  26. package/dist/sts/bound.d.ts +15 -0
  27. package/dist/sts/bound.d.ts.map +1 -0
  28. package/dist/sts/cache.d.ts +27 -0
  29. package/dist/sts/cache.d.ts.map +1 -0
  30. package/dist/sts/discover.d.ts +52 -0
  31. package/dist/sts/discover.d.ts.map +1 -0
  32. package/dist/sts/options.d.ts +77 -0
  33. package/dist/sts/options.d.ts.map +1 -0
  34. package/dist/sts/refusal.d.ts +10 -0
  35. package/dist/sts/refusal.d.ts.map +1 -0
  36. package/dist/sts/run.d.ts +55 -0
  37. package/dist/sts/run.d.ts.map +1 -0
  38. package/dist/sts/settings.d.ts +5 -0
  39. package/dist/sts/settings.d.ts.map +1 -0
  40. package/dist/sts/tally.d.ts +20 -0
  41. package/dist/sts/tally.d.ts.map +1 -0
  42. package/docs/guide.md +98 -2
  43. package/docs/roadmap.md +15 -7
  44. package/docs/troubleshooting.md +75 -0
  45. package/package.json +5 -3
package/docs/guide.md CHANGED
@@ -7,6 +7,7 @@ workers share it.
7
7
  - [The smallest queue](#the-smallest-queue)
8
8
  - [Enqueuing](#enqueuing)
9
9
  - [Delivery and routing](#delivery-and-routing)
10
+ - [MTA-STS](#mta-sts)
10
11
  - [Retries](#retries)
11
12
  - [Delivery status notifications](#delivery-status-notifications)
12
13
  - [Several workers, leases and stopping](#several-workers-leases-and-stopping)
@@ -164,7 +165,7 @@ outcome of the first session. A plain `5xx` (`5.1.x` unknown mailbox,
164
165
  the MX (an `SmtpError` coded `TLS_*`, a refused STARTTLS `454 4.7.0` under
165
166
  `mxTls: 'required'` included) never use it. A greylisting (a `4.7.x` whose
166
167
  text says grey/greylist) is the MX's to retry, and does not either. Only
167
- domains whose route is `'mx'` can fall back.
168
+ domains whose route is `'mx'` can fall back. Under an `enforce` MTA-STS policy (`mtaSts`) nothing falls back, whatever failed: when every MX refuses the connection (4.4.1, 4.4.2) the recipient is deferred for the next attempt, since the smarthost would send the mail without the policy's TLS and name checks. In `testing` mode, or with no policy, it falls back as above. That holds for a reply too: under `enforce` no failure falls back, even a `5.7.x` reputation refusal from an MX that met the policy.
168
169
 
169
170
  A connection lost or a timeout *after* the final dot of `DATA` does not
170
171
  fall back either, though it reads as `4.4.2`: the MX may already have taken
@@ -260,6 +261,39 @@ createQueue({
260
261
 
261
262
  No resolver is needed when no route is `'mx'`.
262
263
 
264
+ ### Reusing connections
265
+
266
+ By default the queue sends through a pool of connections it owns: a
267
+ session to a server the queue already has an idle connection to (same
268
+ host, address, port, EHLO name, TLS and credentials) sends its message
269
+ over it, after an `RSET`, instead of opening a connection, a TLS
270
+ handshake and an `AUTH` again. A connection carries 100 messages and
271
+ waits 5 seconds idle, then closes. A `delivered`, `deferred` or `failed`
272
+ event carries `reused: true` when the recipient's last session went over a
273
+ connection that had carried a message before, `false` when it opened one.
274
+
275
+ ```ts
276
+ createQueue({
277
+ store,
278
+ hostname: 'mail.example.net',
279
+ resolver,
280
+ reuse: { maxMessages: 50, idleSeconds: 10 }, // or the defaults, or `false`
281
+ });
282
+ ```
283
+
284
+ `reuse: false` opens one connection per session, as `sendMail` does. A
285
+ `send` of your own is called as given: the queue never wraps it in a pool,
286
+ and `reuse` does nothing with it, though `createQueue` still checks it.
287
+ `reused` is on the events only if your `send` returns it.
288
+
289
+ `stop()` closes the pool once the deliveries under way end, so no socket
290
+ keeps the process alive; `start()` after it makes a new one. A connection
291
+ that went stale while idle (the server closed it) is never a reason to
292
+ fall back to a smarthost: the pool opens a new one to the same server.
293
+ `@bumail/server` builds its own pool for its `send` and closes it after
294
+ `queue.stop()`. The rules of [Falling back](#falling-back-to-a-smarthost) are
295
+ the same with or without the pool.
296
+
263
297
  ### Timeouts
264
298
 
265
299
  `timeouts` and `deadline` pass to `sendMail`: each step's timeout in
@@ -316,6 +350,65 @@ A store keeps an item from being claimed before its message can be read
316
350
  at a claim is always damage, never an add still under way. Every store
317
351
  here adds both in one step.
318
352
 
353
+ ## MTA-STS
354
+
355
+ `mtaSts` turns on MTA-STS (RFC 8461) for the recipient domains that go by MX. It needs `@bumail/mta-sts` (a peer) and a resolver with `txt`: `mtaSts.resolver`, or the queue's own `resolver` when it has `txt` (`@bumail/dns`'s does).
356
+
357
+ ```ts
358
+ const REPORT_ENVID = 'my-report-7f3a'; // what you enqueue your own reports with: `{ from, to, envid: REPORT_ENVID }`
359
+ const hosted = new Set(['example.net']); // domains you deliver in-process
360
+
361
+ createQueue({
362
+ store,
363
+ hostname: 'mail.example.net',
364
+ resolver: nodeResolver(),
365
+ mtaSts: {
366
+ cache: createMemoryPolicyCache(), // the default; a PolicyCache of yours survives restarts
367
+ fetch: globalThis.fetch, // the default
368
+ fetchTimeoutMs: 60_000, // the whole policy fetch
369
+ maxAgeCap: 31_557_600, // seconds
370
+ skip: (item, domain) => item.envid === REPORT_ENVID || hosted.has(domain), // exempt from MTA-STS
371
+ },
372
+ });
373
+ ```
374
+
375
+ **Finding the policy.** The queue reads the TXT record `_mta-sts.<domain>`. A record whose `id` is the cached policy's, with the cache not expired, uses the cache. A new `id`, or an expired cache, fetches the policy and caches it until its `max_age` (on the queue's `clock`). No record and nothing cached means no policy. A fetch that fails (or a record that cannot be read) leaves a cached policy in force while it has not expired, as RFC 8461 §5.1 asks, and else no policy: the message is sent as without MTA-STS, and a `tls` event with `tlsResult: 'sts-policy-fetch-error'` (or `sts-policy-invalid`, `sts-webpki-invalid`) is told. A policy in mode `none` is no policy. The whole discovery (the cache, the TXT lookup and the fetch together) is bounded by one `fetchTimeoutMs`, not each step by its own (a lookup that times out is no record, a cache that times out, given at most a quarter of it, is empty). A policy fetch that fails is remembered for 5 minutes, per domain and record `id`, in the queue's memory: the sessions in between wait no longer for it, and each still tells the `sts-policy-fetch-error`. A new `id`, or a restart, fetches again. `stop()` aborts a lookup or fetch under way; the group is then left for later like one not started: nothing is sent, no attempt is counted, and the item stays pending. A cache entry that is malformed is ignored, and whatever else breaks the lookup is "no policy" and an `sts-policy-fetch-error` event, never a failed delivery.
376
+
377
+ **Modes.**
378
+
379
+ | mode | the session | on a failure |
380
+ | --- | --- | --- |
381
+ | `enforce` | `tls: 'required'` (over `mxTls`), `mxNames` = the policy's `mx` | an error with a `tlsResult` (an MX outside the policy: `MX_NOT_ALLOWED`; a certificate or STARTTLS failure) defers as `4.7.5`; never the `fallback` smarthost |
382
+ | `testing` | as without a policy: your `mxTls`, every MX host | none; each try is told on `tls`, with what `enforce` would have refused (a name outside the policy, an unchecked certificate, no TLS) |
383
+ | `none`, or no policy | as without MTA-STS | none |
384
+
385
+ **Exemptions.** `skip(item, domain)` returns `true` for a session that must not be under MTA-STS: no discovery, no enforcement and no `tls` event. Use it for mail that must go despite a TLS failure and must not be counted (a mailed TLS-RPT report, RFC 8460 §5.3) and for domains you deliver in-process; mark the first with the `envid` of `enqueue`, which the item keeps. A predicate that throws exempts nothing.
386
+
387
+ **Bouncing.** An `enforce` failure defers with the enhanced status `4.7.5` and the usual back-off. An attempt that may run past `retry.giveUpAfter` (it starts within `deadline` plus `fetchTimeoutMs` of it, which bounds the discovery) fetches the policy again, whatever the cache holds; whether the recipient fails as `5.7.5` or defers as `4.7.5` is decided when the failure happens, by the clock. A session in which any try broke the policy is a refusal even if its last error did not (a refused MX, then a connection failure). With that fresh policy: a policy that is gone (or in mode `none`) delivers the message normally; one still in `enforce` that fails bounces the recipient as `5.7.5` (a `failed` event and a DSN) instead of `4.4.7`. A cached policy that has not expired stays in force even when the TXT record has been removed; a domain that leaves MTA-STS publishes mode `none` first.
388
+
389
+ **The `tls` event** has one payload per try at an MX address, and one for a policy that could not be fetched (no `mxHost`):
390
+
391
+ ```ts
392
+ queue.on('tls', (event) => {
393
+ event.kind; // 'try' | 'policy' (a policy that could not be fetched)
394
+ event.at; // ms since the epoch, on the queue's clock
395
+ event.domain; // the recipient domain
396
+ event.policy; // { type: 'sts', domain, mode: 'enforce' | 'testing', mx, text } | { type: 'no-policy-found' }
397
+ event.mxHost; // absent for a fetch failure
398
+ event.address; // absent for a fetch failure and for an MX host the policy refused without a try
399
+ event.sendingAddress; // the local address of the connection, when the sender says
400
+ event.reused; // on the try that delivered: a connection the pool had used before
401
+ event.ok; // TLS held up; under a policy the MX name and the certificate matched
402
+ event.tlsResult; // RFC 8460 result type, absent when ok
403
+ });
404
+ ```
405
+
406
+ A try that failed before any TLS (the connection itself) is not told. In `enforce`, an MX host the policy skipped (or all of them, when none matches) is told once, without `address`, with `validation-failure`. Tries are told once the send ends.
407
+
408
+ **Your own `send`.** The queue builds the MX options and gives them to `send`; with `mtaSts` they hold `mxNames`, `tls`, `onAttempt` and `onSkip` on top. A `send` that hands them to `sendMail` (or `createSmtpPool().sendMail`) is under the policy; one that drops them is not, and the queue cannot tell.
409
+
410
+ **Not covered.** DANE, TLS reports (they are built from `tls` events by `@bumail/mta-sts`'s `tlsReport`), and the smarthost routes.
411
+
319
412
  ## Retries
320
413
 
321
414
  ```ts
@@ -543,6 +636,7 @@ off(); // stops listening
543
636
  | `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 |
544
637
  | `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)) |
545
638
  | `failed` | the same as `delivered`, and `mayHaveReachedMx` and `mayHaveReachedFallback` as for `deferred` |
639
+ | `tls` | `{ domain, policy, mxHost?, address?, ok, tlsResult? }`: one per try at an MX address under `mtaSts`, or per policy fetch failure; see [MTA-STS](#mta-sts) |
546
640
  | `dsn` | `{ kind: 'delayed' \| 'failed' \| 'relayed', id, of, to, recipients }`: `id` is the DSN's own item, `of` the item it reports on |
547
641
  | `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`) |
548
642
 
@@ -1139,6 +1233,7 @@ connection or remove a file. It may be async.
1139
1233
  | `hostname` | `string` | — | EHLO, Reporting-MTA, the DSN's From |
1140
1234
  | `route` | `'mx' \| Smarthost` | `'mx'` | the default route |
1141
1235
  | `routes` | `Record<string, Route>` | — | a route per recipient domain |
1236
+ | `mtaSts` | `{ cache?, fetch?, resolver?, fetchTimeoutMs?, maxAgeCap?, skip? }` | off | MTA-STS on the MX route; see [MTA-STS](#mta-sts) |
1142
1237
  | `fallback` | `Smarthost` | — | where a recipient goes at once when MX fails by connection, timeout or a reputation refusal; see [Falling back](#falling-back-to-a-smarthost) |
1143
1238
  | `resolver` | `MxResolver` | — | needed by `'mx'` |
1144
1239
  | `mxPort` | `number`, 1–65535 | 25 | the port of MX hosts |
@@ -1159,5 +1254,6 @@ connection or remove a file. It may be async.
1159
1254
  | `pollInterval` | `number`, to 2147483647 | 5000 | ms between passes of `start()` |
1160
1255
  | `owner` | `string` | a random UUID | this worker's name on its leases |
1161
1256
  | `clock` | `{ now(): number }` | `Date.now` | the time |
1162
- | `send` | `Sender` | `sendMail` | delivers one session; a custom one should return `dsnOffered` (leave it out and no `relayed` DSN is ever sent) |
1257
+ | `reuse` | `false \| { maxMessages?, idleSeconds? }` | `{ maxMessages: 100, idleSeconds: 5 }` | connection pool the queue owns and `stop()` closes; `false`: one connection per session; see [Reusing connections](#reusing-connections) |
1258
+ | `send` | `Sender` | the queue's pool, or `sendMail` with `reuse: false` | delivers one session; a custom one should return `dsnOffered` (leave it out and no `relayed` DSN is ever sent) |
1163
1259
  | `random` | `() => number` | `Math.random` | the jitter |
package/docs/roadmap.md CHANGED
@@ -6,18 +6,17 @@ only number on it.
6
6
 
7
7
  ## Now
8
8
 
9
- Nothing under way.
9
+ - **MTA-STS (RFC 8461) and TLS-RPT (RFC 8460)** ([#292](https://github.com/softistx/bumail/issues/292)) — a recipient
10
+ domain's policy fetched with `@bumail/mta-sts`, cached and enforced on MX
11
+ delivery: the certificate is checked against the policy's MX names. Opt-in
12
+ in the library, on in `@bumail/server`. An enforce failure defers the
13
+ message (`4.7.5`) and bounces it only at `giveUpAfter`. TLS reports are
14
+ recorded and sent opt-in.
10
15
 
11
16
  ## Next
12
17
 
13
- - **MTA-STS (RFC 8461) and TLS-RPT (RFC 8460)** — a recipient domain's
14
- policy fetched, cached and enforced, `mxTls: 'required'` with the
15
- certificate checked against the policy's MX names, and the reports.
16
18
  - **DANE (RFC 7672)** — TLSA records checked for MX hosts, once a
17
19
  resolver that validates DNSSEC can be given to the queue.
18
- - **One session for several messages** — the queue hands several items
19
- for one domain to one SMTP session, once `@bumail/smtp`'s client reuses
20
- connections.
21
20
 
22
21
  ## Later
23
22
 
@@ -46,6 +45,15 @@ Nothing under way.
46
45
 
47
46
  ## Shipped
48
47
 
48
+ ### 0.13.0
49
+
50
+ - **One session for several messages** — the queue delivers several
51
+ messages for one MX over one connection, using `@bumail/smtp`'s pool of
52
+ reusable connections. On by default (100 messages per connection, idle 5
53
+ seconds); `reuse: false` turns it off, and a `send` of your own is never
54
+ wrapped. `stop()` closes the pool, and the events may carry `reused`. The
55
+ `QueueStore` contract does not change.
56
+
49
57
  ### 0.12.0
50
58
 
51
59
  - **The contract's specs, exported** — `@bumail/queue/conformance` exports
@@ -28,6 +28,9 @@ parts shown as … vary.
28
28
  - [`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-)
29
29
  - [`QueueError: store must be a QueueStore, such as MemoryQueueStore`](#queueerror-store-must-be-a-queuestore-such-as-memoryqueuestore)
30
30
  - [`QueueError: … must be an integer of at least …, not …`](#queueerror--must-be-an-integer-of-at-least--not-)
31
+ - [`QueueError: reuse must be false or an object of limits, not …`](#queueerror-reuse-must-be-false-or-an-object-of-limits-not-)
32
+ - [`QueueError: reuse.idleSeconds must be a number of seconds above 0 and at most 2147483, not …`](#queueerror-reuseidleseconds-must-be-a-number-of-seconds-above-0-and-at-most-2147483-not-)
33
+ - [`QueueError: reuse.maxMessages must be an integer of at least 1, not …`](#queueerror-reusemaxmessages-must-be-an-integer-of-at-least-1-not-)
31
34
  - [`QueueError: dsn.returnContent must be 'headers' or 'full', not …`](#queueerror-dsnreturncontent-must-be-headers-or-full-not-)
32
35
  - [`QueueError: createQueue() takes an options object`](#queueerror-createqueue-takes-an-options-object)
33
36
  - [`QueueError: owner must be a non-empty string`](#queueerror-owner-must-be-a-non-empty-string)
@@ -96,6 +99,12 @@ parts shown as … vary.
96
99
 
97
100
  **Delivery**
98
101
 
102
+ - [A recipient is deferred with `4.7.5`, or fails with `5.7.5`, `MTA-STS policy of … (enforce)`](#a-recipient-is-deferred-with-475-or-fails-with-575-mta-sts-policy-of--enforce)
103
+ - [`MTA-STS discovery took more than N ms`](#mta-sts-discovery-took-more-than-n-ms-a-tls-event-with-tlsresult-sts-policy-fetch-error)
104
+ - [`QueueError: mtaSts must be an object of options…`, `mtaSts.cache must be a PolicyCache with get and set`, `mtaSts.fetch must be a function`](#queueerror-mtasts-must-be-an-object-of-options-mtastscache-must-be-a-policycache-with-get-and-set-mtastsfetch-must-be-a-function)
105
+ - [`QueueError: mtaSts.fetchTimeoutMs must be a number of milliseconds…`, `mtaSts.maxAgeCap must be a whole number of seconds…`, `mtaSts.skip must be a function`](#queueerror-mtastsfetchtimeoutms-must-be-a-number-of-milliseconds-mtastsmaxagecap-must-be-a-whole-number-of-seconds-mtastsskip-must-be-a-function)
106
+ - [`QueueError: mtaSts needs a resolver with txt(name)`](#queueerror-mtasts-needs-a-resolver-with-txtname)
107
+ - [A `tls` event with `sts-policy-fetch-error` and the mail goes out unprotected](#a-tls-event-with-sts-policy-fetch-error-and-the-mail-goes-out-unprotected)
99
108
  - [A recipient stays `deferred` with `4.4.1` or `4.4.2`](#a-recipient-stays-deferred-with-441-or-442)
100
109
  - [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)
101
110
  - [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)
@@ -302,6 +311,29 @@ or `… must be a number from … to …, not …`. A number option out of its r
302
311
  1), `dsn.delayAfter`, `limits.*` (`limits.maxReplyText` from 64 to
303
312
  900). Times are milliseconds.
304
313
 
314
+ ### `QueueError: reuse must be false or an object of limits, not …`
315
+
316
+ **Code:** `INVALID`. `reuse` was `true`, `null` or another value. Give
317
+ `false` for one connection per session, an object of limits, or leave it out
318
+ for the defaults.
319
+
320
+ ```ts
321
+ createQueue({ store, hostname, resolver, reuse: { maxMessages: 50 } });
322
+ createQueue({ store, hostname, resolver, reuse: false });
323
+ ```
324
+
325
+ ### `QueueError: reuse.idleSeconds must be a number of seconds above 0 and at most 2147483, not …`
326
+
327
+ **Code:** `INVALID`. `reuse.idleSeconds` is in seconds, not milliseconds:
328
+ `5`, not `5000`. `reuse.maxMessages` has its own entry below.
329
+
330
+ ### `QueueError: reuse.maxMessages must be an integer of at least 1, not …`
331
+
332
+ **Code:** `INVALID`. `reuse.maxMessages` is `0`, negative, fractional or not a
333
+ number. The pool owns the limits and checks them when `createQueue` runs;
334
+ the message is the pool's, prefixed with `reuse.`. Pass an integer of at
335
+ least 1, or leave it out for the default (100).
336
+
305
337
  ### `QueueError: dsn.returnContent must be 'headers' or 'full', not …`
306
338
 
307
339
  **Code:** `INVALID`.
@@ -870,6 +902,49 @@ const clean = text.replaceAll('\0', '').toWellFormed();
870
902
 
871
903
  ## Delivery
872
904
 
905
+ ### A recipient is deferred with `4.7.5`, or fails with `5.7.5`, `MTA-STS policy of … (enforce)`
906
+
907
+ **When:** `mtaSts` is set, the recipient domain publishes a policy in mode `enforce`, and the `deferred` event's `reply.status` is `4.7.5` (or the `failed` one's `5.7.5`) with a text such as `MTA-STS policy of example.com (enforce): No mail host of example.com (mx.other.net) is allowed by the policy's MX names (mail.example.com)`.
908
+ **Why:** a session broke the policy: no MX host of the domain matches the policy's `mx` list (`MX_NOT_ALLOWED`), the MX does not offer STARTTLS, or its certificate does not check out for the MX name. The message is not sent anywhere else, `fallback` included (and under an `enforce` policy a plain connection failure of every MX is deferred too, never sent through the `fallback`). It is deferred with the usual back-off and bounced as `5.7.5` only at `retry.giveUpAfter`, after the policy is fetched once more.
909
+ **Fix:** the `tls` events name the host and the `tlsResult` (`validation-failure` for a name outside the policy, `certificate-host-mismatch`, `certificate-expired`, `certificate-not-trusted`, `starttls-not-supported`). It is the receiving domain's to fix; if its policy is wrong, the message is delivered once the policy is. To send meanwhile, `retryNow(id)` after the domain changed its policy to `testing` or `none` and its record `id`.
910
+
911
+ ### `MTA-STS discovery took more than N ms` (a `tls` event with `tlsResult: 'sts-policy-fetch-error'`)
912
+
913
+ **When:** `mtaSts` is set and the cache, the `_mta-sts` lookup and the policy fetch together used the whole `fetchTimeoutMs`.
914
+ **Why:** the discovery has one deadline, not one per step, so a slow resolver or cache leaves the fetch less time. The message is sent without the policy (a cached one that has not expired stays in force). A policy fetch that failed is remembered for 5 minutes per domain and record `id`: a remote policy fixed under the same `id` is not fetched again for up to 5 minutes, and each session in between tells the same event. A timeout caused only by a budget the earlier steps had already used is not remembered.
915
+ **Fix:** raise `fetchTimeoutMs`, or fix the slow resolver or cache; the domain's owner publishes a new `id` to have the policy fetched at once, or waits the 5 minutes.
916
+
917
+ ### `QueueError: mtaSts must be an object of options…`, `mtaSts.cache must be a PolicyCache with get and set`, `mtaSts.fetch must be a function`
918
+
919
+ **Code:** `INVALID`. `mtaSts` is the options object, `cache` an object with `get` and `set` (`createMemoryPolicyCache()` fits), `fetch` a function.
920
+
921
+ ```ts
922
+ mtaSts: { cache: createMemoryPolicyCache(), fetch: globalThis.fetch }
923
+ ```
924
+
925
+ ### `QueueError: mtaSts.fetchTimeoutMs must be a number of milliseconds…`, `mtaSts.maxAgeCap must be a whole number of seconds…`, `mtaSts.skip must be a function`
926
+
927
+ **Code:** `INVALID`. `fetchTimeoutMs` is above 0 and at most 2147483647; `maxAgeCap` is a whole number of seconds from 0 to 31557600 (`MAX_AGE_LIMIT` of `@bumail/mta-sts`); `skip` is a function.
928
+
929
+ ```ts
930
+ mtaSts: { fetchTimeoutMs: 30_000, maxAgeCap: 7 * 86_400 }
931
+ ```
932
+
933
+ ### `QueueError: mtaSts needs a resolver with txt(name)`
934
+
935
+ **Code:** `INVALID`. `mtaSts` reads the `_mta-sts` TXT record, and neither `mtaSts.resolver` nor the queue's `resolver` has `txt`.
936
+
937
+ ```ts
938
+ createQueue({ store, hostname, resolver: nodeResolver(), mtaSts: {} }); // nodeResolver() has txt
939
+ createQueue({ store, hostname, resolver, mtaSts: { resolver: nodeResolver() } });
940
+ ```
941
+
942
+ ### A `tls` event with `sts-policy-fetch-error` and the mail goes out unprotected
943
+
944
+ **When:** `mtaSts` is set, and the `tls` event has no `mxHost` and a `tlsResult` of `sts-policy-fetch-error`, `sts-policy-invalid` or `sts-webpki-invalid`.
945
+ **Why:** the domain has an `_mta-sts` record, but `https://mta-sts.<domain>/.well-known/mta-sts.txt` could not be had (a timeout, a redirect, a status other than 200, a wrong content type, an invalid policy, a certificate the system does not trust). RFC 8461 §5.1 then applies a cached policy that has not expired, and with none sends without a policy: that session is not protected.
946
+ **Fix:** the domain's to fix. Check the policy URL yourself, and that the `fetch` you gave (`mtaSts.fetch`) can reach it.
947
+
873
948
  ### A recipient stays `deferred` with `4.4.1` or `4.4.2`
874
949
 
875
950
  **When:** the `deferred` event's `reply` has no `code`, a `status` of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bumail/queue",
3
- "version": "0.12.0",
3
+ "version": "0.14.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",
@@ -73,12 +73,14 @@
73
73
  "devDependencies": {
74
74
  "@bumail/dns": "^0.2.2",
75
75
  "@bumail/mime": "^0.2.0",
76
- "@bumail/smtp": "^0.11.1",
76
+ "@bumail/mta-sts": "^0.1.0",
77
+ "@bumail/smtp": "^0.13.0",
77
78
  "@types/bun": "^1.4.2"
78
79
  },
79
80
  "peerDependencies": {
80
81
  "@bumail/mime": "^0.2.0",
81
- "@bumail/smtp": "^0.11.1",
82
+ "@bumail/mta-sts": "^0.1.0",
83
+ "@bumail/smtp": "^0.13.0",
82
84
  "typescript": "^6.0.3 || ^7.0.0"
83
85
  },
84
86
  "peerDependenciesMeta": {