@bumail/smtp 0.11.0 → 0.12.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 (40) hide show
  1. package/README.md +30 -2
  2. package/dist/client/connect.d.ts +18 -0
  3. package/dist/client/connect.d.ts.map +1 -0
  4. package/dist/client/inbox.d.ts +2 -0
  5. package/dist/client/inbox.d.ts.map +1 -1
  6. package/dist/client/index.d.ts +2 -0
  7. package/dist/client/index.d.ts.map +1 -1
  8. package/dist/client/index.js +594 -299
  9. package/dist/client/index.js.map +19 -11
  10. package/dist/client/options.d.ts +2 -0
  11. package/dist/client/options.d.ts.map +1 -1
  12. package/dist/client/pool/idle.d.ts +10 -0
  13. package/dist/client/pool/idle.d.ts.map +1 -0
  14. package/dist/client/pool/index.d.ts +18 -0
  15. package/dist/client/pool/index.d.ts.map +1 -0
  16. package/dist/client/pool/options.d.ts +26 -0
  17. package/dist/client/pool/options.d.ts.map +1 -0
  18. package/dist/client/pool/reset.d.ts +9 -0
  19. package/dist/client/pool/reset.d.ts.map +1 -0
  20. package/dist/client/pool/send.d.ts +8 -0
  21. package/dist/client/pool/send.d.ts.map +1 -0
  22. package/dist/client/pool/state.d.ts +28 -0
  23. package/dist/client/pool/state.d.ts.map +1 -0
  24. package/dist/client/send.d.ts +10 -1
  25. package/dist/client/send.d.ts.map +1 -1
  26. package/dist/client/session.d.ts +2 -2
  27. package/dist/client/session.d.ts.map +1 -1
  28. package/dist/client/settings.d.ts +11 -5
  29. package/dist/client/settings.d.ts.map +1 -1
  30. package/dist/client/socket-events.d.ts +15 -0
  31. package/dist/client/socket-events.d.ts.map +1 -0
  32. package/dist/client/socket.d.ts +12 -2
  33. package/dist/client/socket.d.ts.map +1 -1
  34. package/dist/client/transaction.d.ts +9 -3
  35. package/dist/client/transaction.d.ts.map +1 -1
  36. package/docs/README.md +3 -3
  37. package/docs/guide.md +25 -3
  38. package/docs/roadmap.md +26 -5
  39. package/docs/troubleshooting.md +84 -0
  40. package/package.json +3 -3
package/README.md CHANGED
@@ -300,6 +300,33 @@ It resolves once the server took the message for one recipient at least:
300
300
  encrypted and the certificate checked. Anything else rejects with an
301
301
  `SmtpError` whose `temporary` says whether trying later may succeed.
302
302
 
303
+ ### Reuse connections with a pool
304
+
305
+ `createSmtpPool(options?)` keeps connections to the same server between
306
+ messages, so a burst to one host pays for one greeting, one STARTTLS
307
+ and one AUTH. The pool's own options, all optional, are `maxMessages`
308
+ (100 per connection), `idleSeconds` (5), `maxAgeSeconds` (300) and
309
+ `maxIdlePerHost` (4). `pool.sendMail(message, options)` takes the options
310
+ of `sendMail`.
311
+
312
+ ```ts
313
+ import { createSmtpPool } from '@bumail/smtp/client';
314
+
315
+ const pool = createSmtpPool(); // or createSmtpPool({ idleSeconds: 10 })
316
+ const first = await pool.sendMail(message, { host: 'smtp.example.com', from: 'alice@example.com', to: 'bob@example.org' });
317
+ const second = await pool.sendMail(message, { host: 'smtp.example.com', from: 'alice@example.com', to: 'carol@example.org' });
318
+ console.log(first.reused, second.reused); // false, then true: the second went over the first's connection
319
+ await pool.close(); // QUITs the idle connections, waits for the busy ones
320
+ ```
321
+
322
+ A connection is reset (RSET) each time it is lent, kept apart by
323
+ server address, greeting name, TLS and credentials, and never reused
324
+ after a 421, a timeout, a bad reply, a lost stream or bytes it was not
325
+ asked for. A reused connection that turns out dead before MAIL FROM is
326
+ replaced by a fresh one without the caller seeing it. `@bumail/queue` uses a
327
+ pool by default (its `reuse` option), and so does the `bumail serve` outbound
328
+ queue of `@bumail/server`.
329
+
303
330
  ## Send mail: direct to a domain's MX
304
331
 
305
332
  `{ domain, resolver }` instead of `{ host }` looks up the domain's MX
@@ -484,7 +511,7 @@ host; a 5xx stops. A null MX (RFC 7505) fails at once with `NULL_MX`.
484
511
  | `SmtpServer` | `listen({ port, hostname? })` (once; again, even before the first resolved, throws `ALREADY_LISTENING`; on implicit TLS, a key or certificate it cannot read or use throws `INVALID_OPTION`), `setTls({ key, cert })` (a renewed pair for new STARTTLS upgrades and new implicit TLS connections; open sessions keep theirs; a pair that cannot be used throws `INVALID_OPTION` and the old one stays), `stop(closeConnections?)` (`true` hangs up on every client, after STARTTLS too), `connections` |
485
512
  | `SmtpServerOptions` | `hostname`, `mode`, `localDomains`, `tls`, `implicitTls`, `authenticate`, the limits (`maxConnectionsPerClient` among them), `hookTimeout`, `handshakeTimeout`, `greetingDelay`, `proxyProtocol`, `onError`, and the hooks |
486
513
  | `ProxyProtocolOptions` | `trusted`: the proxies' IPv4 and IPv6 addresses and CIDRs, the only peers whose PROXY header (v1 or v2) is read |
487
- | `SmtpError`, `SmtpErrorCode` | `code`: `INVALID_OPTION`, `ALREADY_LISTENING`, `STOPPED` (a `stop()` came before `listen` resolved), and what a content stream or `onError` can get: `MESSAGE_TOO_BIG`, `BARE_LINE_BREAK`, `NUL_BYTE`, `CONNECTION_LOST`, `HOOK_TIMEOUT`, `INVALID_HOOK_REPLY`, `MESSAGE_NOT_READ`; `sendMail`'s are listed below. `temporary`, `reply` and `rejected` are set for `sendMail`'s errors (`rejected` also after the final dot), and `afterData` when the final dot was written and no reply came (the server may have taken the message; `accepted` and `rejected` then list the recipients taken and refused at `RCPT TO`) |
514
+ | `SmtpError`, `SmtpErrorCode` | `code`: `INVALID_OPTION`, `ALREADY_LISTENING`, `STOPPED` (a `stop()` came before `listen` resolved, or `sendMail` on a closed pool), and what a content stream or `onError` can get: `MESSAGE_TOO_BIG`, `BARE_LINE_BREAK`, `NUL_BYTE`, `CONNECTION_LOST`, `HOOK_TIMEOUT`, `INVALID_HOOK_REPLY`, `MESSAGE_NOT_READ`; `sendMail`'s are listed below. `temporary`, `reply` and `rejected` are set for `sendMail`'s errors (`rejected` also after the final dot), and `afterData` when the final dot was written and no reply came (the server may have taken the message; `accepted` and `rejected` then list the recipients taken and refused at `RCPT TO`) |
488
515
  | `SmtpHooks` | `onConnect`, `onMailFrom`, `onRcptTo`, `onData` |
489
516
  | `HookResult` | what a hook returns: `undefined` to accept, a `Reply` to refuse |
490
517
  | `Session` | `id`, `remoteAddress`, `secure`, `helo`, `esmtp`, `user`, and `data` for your own state |
@@ -505,9 +532,10 @@ host; a 5xx stops. A null MX (RFC 7505) fails at once with `NULL_MX`.
505
532
  | export | |
506
533
  | --- | --- |
507
534
  | `sendMail(message, options)` | delivers one message; resolves to a `SendMailResult`, rejects with an `SmtpError` |
535
+ | `createSmtpPool(options?)`, `SmtpPool`, `SmtpPoolOptions` | a pool of reusable connections: `pool.sendMail(message, options)` (as `sendMail`, `result.reused`) and `pool.close()`; `maxMessages` (100), `idleSeconds` (5), `maxAgeSeconds` (300), `maxIdlePerHost` (4) |
508
536
  | `SendMailOptions` | `from`, `to`, and `{ host, port? }` or `{ domain, resolver, port? }`; `helo`, `tls`, `secure`, `ca`, `auth`, `allowPlaintextAuth`, `size`, `smtputf8`, `ret`, `envid`, `recipients` (DSN, see [Sending DSN parameters](./docs/guide.md#sending-dsn-parameters)), `normalizeLineEnds`, `timeouts`, `deadline` |
509
537
  | `HostDestination`, `MxDestination`, `SendMailEnvelope` | the parts of `SendMailOptions` |
510
- | `SendMailResult` | `accepted`, `rejected`, `reply`, `host`, `port`, `tls` (`false` or `{ verified }`), `authenticated`, `dsnOffered` (the server listed DSN in EHLO, so the DSN options were sent) |
538
+ | `SendMailResult` | `accepted`, `rejected`, `reply`, `host`, `port`, `tls` (`false` or `{ verified }`), `authenticated`, `dsnOffered` (the server listed DSN in EHLO, so the DSN options were sent), `reused?` (a pool only: the connection had carried a message before) |
511
539
  | `SendMailAuth` | `username`, `password`, `mechanism?` (`PLAIN` or `LOGIN`) |
512
540
  | `SendMailTimeouts` | seconds: `connect`, `greeting`, `command`, `mail`, `rcpt`, `dataStart`, `dataBlock`, `dataEnd` |
513
541
  | `TlsMode` | `'opportunistic'`, `'required'` or `'none'` |
@@ -0,0 +1,18 @@
1
+ import type { SendMailResult } from './options';
2
+ import { type Ready } from './session';
3
+ import type { ClientSettings } from './settings';
4
+ import { ClientSocket } from './socket';
5
+ import type { Clock, Endpoint } from './target';
6
+ import type { Delivered } from './transaction';
7
+ /** Errors from before MAIL FROM: another MX host may do better. */
8
+ export declare const early: WeakSet<object>;
9
+ /** A connection, past its greeting, EHLO, STARTTLS and AUTH. */
10
+ export interface Session {
11
+ readonly socket: ClientSocket;
12
+ readonly ready: Ready;
13
+ }
14
+ /** Connects and opens a session; a failure is marked `early`. The socket's clock is the delivery's. */
15
+ export declare function connect(endpoint: Endpoint, settings: Pick<ClientSettings, 'tls' | 'secure' | 'ca' | 'timeouts' | 'helo' | 'auth' | 'allowPlaintextAuth'>, clock: Clock): Promise<Session>;
16
+ /** What a delivery reports: the server's answers, where they came from and how the session was secured. */
17
+ export declare function resultOf(delivered: Delivered, { socket, ready }: Session, endpoint: Pick<Endpoint, 'host' | 'port'>): SendMailResult;
18
+ //# sourceMappingURL=connect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/client/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAQ,KAAK,KAAK,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAa,MAAM,UAAU,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,mEAAmE;AACnE,eAAO,MAAM,KAAK,iBAAwB,CAAC;AAE3C,gEAAgE;AAChE,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACtB;AAED,uGAAuG;AACvG,wBAAsB,OAAO,CAC5B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,IAAI,CACb,cAAc,EACZ,KAAK,GACL,QAAQ,GACR,IAAI,GACJ,UAAU,GACV,MAAM,GACN,MAAM,GACN,oBAAoB,CACtB,EACD,KAAK,EAAE,KAAK,GACV,OAAO,CAAC,OAAO,CAAC,CA2BlB;AAED,2GAA2G;AAC3G,wBAAgB,QAAQ,CACvB,SAAS,EAAE,SAAS,EACpB,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,OAAO,EAC1B,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,GACvC,cAAc,CAQhB"}
@@ -10,6 +10,8 @@ export declare class Inbox {
10
10
  #private;
11
11
  /** Takes the next bytes; says what is wrong with them, if they are not replies. */
12
12
  push(chunk: Uint8Array): string | undefined;
13
+ /** The bytes counted against `MAX_REPLY_BYTES` start over: a new message on a reused connection. */
14
+ reset(): void;
13
15
  /** The next complete reply. */
14
16
  shift(): Reply | undefined;
15
17
  /** Bytes or replies not yet read. */
@@ -1 +1 @@
1
- {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../src/client/inbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG/C,6EAA6E;AAC7E,eAAO,MAAM,eAAe,QAAc,CAAC;AAE3C;;;;GAIG;AACH,qBAAa,KAAK;;IAMjB,mFAAmF;IACnF,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS;IAiB3C,+BAA+B;IAC/B,KAAK,IAAI,KAAK,GAAG,SAAS;IAI1B,qCAAqC;IACrC,IAAI,OAAO,IAAI,OAAO,CAErB;CACD"}
1
+ {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../src/client/inbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG/C,6EAA6E;AAC7E,eAAO,MAAM,eAAe,QAAc,CAAC;AAE3C;;;;GAIG;AACH,qBAAa,KAAK;;IAMjB,mFAAmF;IACnF,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS;IAiB3C,oGAAoG;IACpG,KAAK,IAAI,IAAI;IAIb,+BAA+B;IAC/B,KAAK,IAAI,KAAK,GAAG,SAAS;IAI1B,qCAAqC;IACrC,IAAI,OAAO,IAAI,OAAO,CAErB;CACD"}
@@ -3,5 +3,7 @@ export type { Reply } from '../protocol/reply';
3
3
  export { isMailbox } from './address';
4
4
  export { type MailHost, resolveMx } from './mx';
5
5
  export type { HostDestination, MessageSource, MxDestination, MxResolver, SendMailAuth, SendMailEnvelope, SendMailOptions, SendMailResult, SendMailTimeouts, TlsMode, } from './options';
6
+ export { createSmtpPool, type SmtpPool } from './pool';
7
+ export type { SmtpPoolOptions } from './pool/options';
6
8
  export { sendMail } from './send';
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,cAAc,EACnB,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACrB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAChD,YAAY,EACX,eAAe,EACf,aAAa,EACb,aAAa,EACb,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,OAAO,GACP,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,cAAc,EACnB,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACrB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAChD,YAAY,EACX,eAAe,EACf,aAAa,EACb,aAAa,EACb,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,OAAO,GACP,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvD,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC"}