@bumail/smtp 0.2.1 → 0.4.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.
@@ -1 +1 @@
1
- {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAWnD,MAAM,WAAW,UAAU;IAC1B,0FAA0F;IAC1F,MAAM,CAAC,OAAO,EAAE;QACf,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,wEAAwE;IACxE,IAAI,CAAC,gBAAgB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACvC,wBAAwB;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AA2ED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAqEvE"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAInD,MAAM,WAAW,UAAU;IAC1B,0FAA0F;IAC1F,MAAM,CAAC,OAAO,EAAE;QACf,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,wEAAwE;IACxE,IAAI,CAAC,gBAAgB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACvC,wBAAwB;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAkDvE"}
@@ -6,9 +6,11 @@ export interface Settings {
6
6
  readonly maxMessageSize: number;
7
7
  readonly maxRecipients: number;
8
8
  readonly maxConnections: number;
9
+ readonly maxConnectionsPerClient: number;
9
10
  readonly maxErrors: number;
10
11
  readonly timeout: number;
11
12
  readonly hookTimeout: number;
13
+ readonly handshakeTimeout: number;
12
14
  readonly greetingDelay: number;
13
15
  isLocal(domain: string): boolean | Promise<boolean>;
14
16
  }
@@ -1 +1 @@
1
- {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/server/settings.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,qDAAqD;AACrD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,YAAY,CAAC;IACnC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpD;AAkED,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,QAAQ,CA+C/D"}
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/server/settings.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,qDAAqD;AACrD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,YAAY,CAAC;IACnC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpD;AAkED,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,QAAQ,CAyD/D"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The connections a server holds, and how many each client holds, by
3
+ * `clientKey`. `release` is idempotent: every close handler calls it.
4
+ */
5
+ export declare class Slots<T> {
6
+ #private;
7
+ get size(): number;
8
+ /** How many connections the client `key` holds. */
9
+ of(key: string | undefined): number;
10
+ take(connection: T, key: string | undefined): void;
11
+ release(connection: T): void;
12
+ connections(): T[];
13
+ }
14
+ //# sourceMappingURL=slots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/server/slots.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,KAAK,CAAC,CAAC;;IAInB,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,mDAAmD;IACnD,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM;IAInC,IAAI,CAAC,UAAU,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAKlD,OAAO,CAAC,UAAU,EAAE,CAAC,GAAG,IAAI;IAU5B,WAAW,IAAI,CAAC,EAAE;CAGlB"}
@@ -1 +1 @@
1
- {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../../src/server/transaction.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAS/C,iFAAiF;AACjF,wBAAsB,IAAI,CACzB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAuDf;AAED,oFAAoF;AACpF,wBAAsB,IAAI,CACzB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAwCf"}
1
+ {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../../src/server/transaction.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAS/C,iFAAiF;AACjF,wBAAsB,IAAI,CACzB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAuDf;AAED,oFAAoF;AACpF,wBAAsB,IAAI,CACzB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAyCf"}
package/docs/guide.md CHANGED
@@ -8,6 +8,7 @@ with the client, and the RFCs behind each behaviour.
8
8
  - [Options](#options)
9
9
  - [The session, and what each reply means](#the-session-and-what-each-reply-means)
10
10
  - [Hooks, and their order](#hooks-and-their-order)
11
+ - [`RCPT TO:<postmaster>`](#rcpt-topostmaster)
11
12
  - [Session.data](#sessiondata)
12
13
  - [What onData receives](#what-ondata-receives)
13
14
  - [Delivering into @bumail/store](#delivering-into-bumailstore)
@@ -67,6 +68,8 @@ export interface SmtpServer {
67
68
  | `maxMessageSize` | `number` | 25 MiB | bytes; announced with SIZE |
68
69
  | `maxRecipients` | `number` | `100` | recipients per message |
69
70
  | `maxConnections` | `number` | `1000` | open connections at once |
71
+ | `maxConnectionsPerClient` | `number` | `10` | open connections at once from one client: an IPv4 address, or an IPv6 /64; see [Limits](#limits) |
72
+ | `handshakeTimeout` | `number` | `10` | seconds a client on implicit TLS has to complete its handshake, from the TCP connection on; past it, the socket is closed. See [TLS](#tls) |
70
73
  | `maxErrors` | `number` | `10` | failed commands before the server hangs up |
71
74
  | `timeout` | `number` | `300` | seconds since the client's last byte, or since the 220, before the server hangs up |
72
75
  | `hookTimeout` | `number` | `60` | seconds a hook, `authenticate` or `localDomains` has to settle, and `onData` to read on; past it, `451 4.3.0`. At most 2 147 483, what a timer can wait |
@@ -166,7 +169,7 @@ Every reply the server sends of its own:
166
169
  | `250 <hostname> greets <name>` | EHLO or HELO accepted; either one also ends a transaction in progress |
167
170
  | `501 Syntax: EHLO hostname` / `501 Syntax: HELO hostname` | the EHLO or HELO argument is missing, or is not a domain or an address literal; no enhanced code before an EHLO was accepted, `501 5.5.4 Syntax: EHLO hostname` after one |
168
171
  | `250 2.1.0 OK` | MAIL FROM accepted |
169
- | `250 2.1.5 OK` | RCPT TO accepted |
172
+ | `250 2.1.5 OK` | RCPT TO accepted; `RCPT TO:<postmaster>` with no domain is accepted without AUTH (see [below](#rcpt-topostmaster)) |
170
173
  | `354 End data with <CR><LF>.<CR><LF>` | DATA: send the message |
171
174
  | `250 2.0.0 OK queued as <id>` | the message was taken: `onData` read its content stream to the clean end and resolved without refusing |
172
175
  | `250 2.0.0 OK` | RSET (the transaction is forgotten) or NOOP |
@@ -193,6 +196,7 @@ Every reply the server sends of its own:
193
196
  | `500 5.5.2 Command unrecognized` | an unknown verb — EXPN, BDAT and the rest |
194
197
  | `500 5.5.6 Line too long` | a command line over 2048 bytes; the rest of it is skipped |
195
198
  | `421 4.3.2 <hostname> Too many connections, try later` | a connection past `maxConnections`, then the server hangs up |
199
+ | `421 4.7.0 <hostname> Too many connections from your address, try later` | a connection past `maxConnectionsPerClient` from one client, then the server hangs up |
196
200
  | `421 4.4.2 <hostname> Idle too long, closing` | nothing for `timeout` seconds |
197
201
  | `421 4.7.0 <hostname> Too many errors, closing` | `maxErrors` failed commands |
198
202
  | `421 4.3.0 Local error, closing` | the server failed itself |
@@ -234,7 +238,7 @@ In the order a session meets them:
234
238
 
235
239
  | Hook | Runs | After the server checked | A refusal |
236
240
  | --- | --- | --- | --- |
237
- | `onConnect` | when a client connects, before the greeting | the connection count | replaces the greeting, at once even with `greetingDelay`; then the server hangs up. When `onConnect` accepts, a client that talked before the greeting is refused with `554` |
241
+ | `onConnect` | when a client connects, before the greeting | the connection counts: `maxConnections`, then `maxConnectionsPerClient` | replaces the greeting, at once even with `greetingDelay`; then the server hangs up. When `onConnect` accepts, a client that talked before the greeting is refused with `554` |
238
242
  | `authenticate` | at AUTH | TLS, EHLO, no transaction in progress | see [Authentication](#authentication) |
239
243
  | `onMailFrom` | at each MAIL FROM | EHLO, AUTH in `submission`, syntax, parameters, SIZE, SMTPUTF8 | refuses the sender; the client may try another MAIL |
240
244
  | `onRcptTo` | at each RCPT TO | MAIL, syntax, SMTPUTF8, `maxRecipients`, **relaying** | refuses that recipient; the others stand |
@@ -285,12 +289,14 @@ await server.listen({ port: 25 });
285
289
 
286
290
  ```ts
287
291
  export interface Path {
288
- /** `local@domain`; `''` for the null reverse-path `<>`: a bounce. */
292
+ /** `local@domain`; `''` for the null reverse-path `<>`: a bounce; `'postmaster'` for `RCPT TO:<postmaster>`. */
289
293
  readonly address: string;
290
294
  /** As sent: case is kept. */
291
295
  readonly local: string;
292
- /** Lower case. */
296
+ /** Lower case; `''` for `<>` and `<postmaster>`. */
293
297
  readonly domain: string;
298
+ /** `true` for `RCPT TO:<postmaster>` with no domain; absent otherwise. */
299
+ readonly postmaster?: true;
294
300
  }
295
301
  ```
296
302
 
@@ -309,6 +315,49 @@ surrogates are refused anywhere in a path — the route, the local part
309
315
  quoted or not, the domain — even under SMTPUTF8. An IPv4 address literal
310
316
  takes octets up to 255: `[999.1.1.1]` is not one.
311
317
 
318
+ ## `RCPT TO:<postmaster>`
319
+
320
+ RFC 5321 §4.5.1 says every server that delivers mail must take mail for
321
+ `postmaster`, and §4.1.1.3 lets RCPT TO name it with no domain at all:
322
+ `RCPT TO:<Postmaster>`. The server takes it, in any case, as the
323
+ postmaster of this server, whatever domains it hosts:
324
+
325
+ - it is not relaying, so it needs no AUTH, and `localDomains` is not asked;
326
+ - `onRcptTo` is asked, as for any recipient, with the `Path`
327
+ `{ address: 'postmaster', local: 'postmaster', domain: '', postmaster: true }`,
328
+ so it can still refuse;
329
+ - `envelope.to` lists it as `'postmaster'`, with no `@`, and the Received
330
+ field says `for <postmaster>` when it is the only recipient.
331
+
332
+ `<postmaster@example.com>` is an ordinary path: it is checked against
333
+ `localDomains` like any other, and has no `postmaster` field. A path the
334
+ grammar would otherwise refuse stays refused: `<postmaster@>`,
335
+ `<"postmaster">` and `MAIL FROM:<postmaster>` get `501 5.5.4`.
336
+
337
+ Route it to a mailbox you keep: by `path.postmaster` in `onRcptTo`, by the
338
+ bare `'postmaster'` in `onData`.
339
+
340
+ ```ts
341
+ import { createSmtpServer, reply } from '@bumail/smtp';
342
+
343
+ const mailboxes = new Set(['alice@example.com', 'postmaster@example.com']);
344
+ const postmaster = 'postmaster@example.com';
345
+
346
+ const server = createSmtpServer({
347
+ hostname: 'mx.example.com',
348
+ localDomains: ['example.com'],
349
+ onRcptTo: (path) =>
350
+ path.postmaster || mailboxes.has(path.address.toLowerCase())
351
+ ? undefined
352
+ : reply(550, '5.1.1', 'No such user here'),
353
+ async onData(message) {
354
+ const recipients = message.envelope.to.map((to) => (to === 'postmaster' ? postmaster : to));
355
+ const bytes = await new Response(message.content).bytes();
356
+ for (const to of recipients) await Bun.write(`spool/${to}/${message.id}.eml`, bytes);
357
+ },
358
+ });
359
+ ```
360
+
312
361
  ## Session.data
313
362
 
314
363
  Every hook, and `authenticate`, receives the `Session`:
@@ -396,6 +445,7 @@ export interface ReceivedMessage {
396
445
  export interface Envelope {
397
446
  /** `''` for the null reverse-path `<>`: a bounce. */
398
447
  readonly from: string;
448
+ /** `local@domain` each; `'postmaster'`, with no domain, for `RCPT TO:<postmaster>`. */
399
449
  readonly to: readonly string[];
400
450
  /** The client asked for SMTPUTF8 (RFC 6531). */
401
451
  readonly smtputf8: boolean;
@@ -406,8 +456,9 @@ export interface Envelope {
406
456
 
407
457
  The **envelope** is what MAIL FROM and RCPT TO said — not the `From:` and
408
458
  `To:` of the message, which may differ (Bcc, mailing lists, forwarding).
409
- `to` holds only the recipients the server and `onRcptTo` accepted. Deliver
410
- to the envelope.
459
+ `to` holds only the recipients the server and `onRcptTo` accepted, each
460
+ `local@domain`, or `'postmaster'` for [`RCPT TO:<postmaster>`](#rcpt-topostmaster).
461
+ Deliver to the envelope.
411
462
 
412
463
  The **content** is the message as the client sent it, with the leading dot
413
464
  of each stuffed line removed (RFC 5321 §4.5.2), the terminating `.` line
@@ -767,6 +818,22 @@ STARTTLS, in clear, are dropped and never answered. The STARTTLS replies:
767
818
  with `tls`, usually on 465. STARTTLS is not offered there — the connection
768
819
  is already secure — and AUTH is offered at once.
769
820
 
821
+ On implicit TLS the server counts a socket from the TCP connection on, not
822
+ from the end of its handshake: a socket that has not finished its handshake
823
+ holds a slot of `maxConnections` and of `maxConnectionsPerClient`, and is
824
+ closed, without a reply, `handshakeTimeout` seconds (default 10) after it
825
+ connected, so clients that open TCP connections and never send a
826
+ ClientHello cannot fill the server. A refusal by a limit, `onConnect`, the
827
+ greeting and `greetingDelay` all wait for the handshake, so the client
828
+ reads them over TLS and `session.secure` is already `true`. A handshake
829
+ that fails is closed by Bun at once and counted out. A STARTTLS handshake
830
+ is bounded by the idle `timeout`, the connection already counted.
831
+
832
+ Bun 1.4.2 calls a TLS listener's `open` only once the handshake completed
833
+ unless the listener has a `handshake` handler; with one, `open` comes at
834
+ the TCP connection and the socket's timer runs during the handshake. The
835
+ server sets one for that reason.
836
+
770
837
  An MX takes mail in clear as well as encrypted: senders on the Internet that
771
838
  cannot do TLS still deliver. Whether to refuse them is policy — check
772
839
  `session.secure` in a hook:
@@ -837,6 +904,8 @@ await createSmtpServer({ ...submission, implicitTls: true }).listen({ port: 465
837
904
  | `maxMessageSize` | 25 MiB | `552 5.3.4` at MAIL when `SIZE=` says more; during DATA once the message is more — the content stream errors with `MESSAGE_TOO_BIG`, the rest is read and dropped, and the session goes on |
838
905
  | `maxRecipients` | 100, the least RFC 5321 §4.5.3.1.8 asks a server to take | `452 4.5.3` for each extra recipient (RFC 5321 §4.5.3.1.10); the client sends the rest in another transaction |
839
906
  | `maxConnections` | 1000 | `421 4.3.2` and the server hangs up, before the greeting and before `onConnect` |
907
+ | `maxConnectionsPerClient` | 10 | `421 4.7.0 <hostname> Too many connections from your address, try later` and the server hangs up, before the greeting and before `onConnect` |
908
+ | `handshakeTimeout` | 10 seconds, on implicit TLS; ticks of about 4 s, as `timeout` | the socket is closed without a reply: there is no TLS to write one on |
840
909
  | `maxErrors` | 10 | `421 4.7.0` and the server hangs up |
841
910
  | `timeout` | 300 seconds, RFC 5321 §4.5.3.2.7's, counted from the client's last byte, or from the 220; Bun's socket timer ticks in steps of about 4 s, so the hang-up comes up to that much later | `421 4.4.2` and the server hangs up |
842
911
  | `hookTimeout` | 60 seconds | `451 4.3.0` for that command; `onError` gets an `SmtpError` `HOOK_TIMEOUT` |
@@ -869,6 +938,50 @@ const server = createSmtpServer({
869
938
  await server.listen({ port: 25 });
870
939
  ```
871
940
 
941
+ `maxConnectionsPerClient` keeps one host from taking every slot of
942
+ `maxConnections`. A client is counted by `clientKey(session.remoteAddress)`:
943
+
944
+ | Remote address | Counted as |
945
+ | --- | --- |
946
+ | `192.0.2.1` | `192.0.2.1` |
947
+ | `::ffff:192.0.2.1`, `::ffff:c000:201` (IPv4-mapped, as a listener on `::` sees an IPv4 client) | `192.0.2.1` |
948
+ | `64:ff9b::c000:201` (NAT64) | `192.0.2.1` |
949
+ | `2001:db8:1:2::9`, `2001:db8:1:2:ffff::1` | `2001:db8:1:2::/64` |
950
+ | `2001:db8::1`, `2001:0db8:0000::ffff` | `2001:db8:0:0::/64` |
951
+ | anything else (a Unix socket) | not limited |
952
+
953
+ An IPv6 key is always the first four groups in lower-case hex, without
954
+ leading zeros, every group written out — zeros too — then `::/64`:
955
+ `2001:db8::1` is `2001:db8:0:0::/64`, never `2001:db8::/64`. Write the
956
+ keys you compare with it that way.
957
+
958
+ An IPv6 host usually holds a whole /64, so counting each address apart
959
+ would let it open as many connections as it likes. Every close frees the
960
+ slot as it frees one of `maxConnections`: QUIT, the client hanging up, a
961
+ socket error, the idle `timeout`, `maxErrors`, a refusal from `onConnect`,
962
+ and `stop(true)`. The count lives in the server's memory, for that one
963
+ server. For a policy of your own — a list, a per-network budget — use
964
+ `onConnect` with `clientKey`:
965
+
966
+ ```ts
967
+ import { clientKey, createSmtpServer, reply } from '@bumail/smtp';
968
+
969
+ // As clientKey writes them: '2001:db8:0:0::/64', not '2001:db8::/64'.
970
+ const blocked = new Set(['203.0.113.7', '2001:db8:0:0::/64', '2001:db8:dead:beef::/64']);
971
+
972
+ const server = createSmtpServer({
973
+ hostname: 'mx.example.com',
974
+ localDomains: ['example.com'],
975
+ maxConnections: 500,
976
+ maxConnectionsPerClient: 5,
977
+ onConnect: (session) =>
978
+ blocked.has(clientKey(session.remoteAddress) ?? '') ? reply(554, '5.7.1', 'Go away') : undefined,
979
+ onData: async (message) => {
980
+ await new Response(message.content).bytes();
981
+ },
982
+ });
983
+ ```
984
+
872
985
  Some limits are fixed: a command line is at most 2048 bytes (RFC 5321
873
986
  §4.5.3.1.4 asks for 512 at least); a session gets three AUTH attempts; and
874
987
  the server holds at most 64 KiB of a client's unread input, and of a
@@ -910,8 +1023,9 @@ TLS buffer. So a `221` behind replies that are never read holds the slot
910
1023
  5 seconds, not until the `timeout`.
911
1024
 
912
1025
  Stop a server with `stop()`; `stop(true)` also hangs up on every client,
913
- those moved to TLS by STARTTLS included, as the idle timeout does.
914
- `connections` counts the clients currently connected:
1026
+ those moved to TLS by STARTTLS included, as the idle timeout does; a
1027
+ socket still in its implicit TLS handshake is reset, with no 421, having
1028
+ no TLS to write one on. `connections` counts the clients currently connected:
915
1029
 
916
1030
  ```ts
917
1031
  import { createSmtpServer } from '@bumail/smtp';
@@ -976,6 +1090,21 @@ source route (`@a.example,@b.example:x@c.example`) is refused too, valid
976
1090
  or not: RFC 5321 §4.1.1.3 says a client should not send one, and the
977
1091
  address is written as it is given.
978
1092
 
1093
+ Code that keeps addresses to send to later — a queue — checks
1094
+ each one when it takes it with `isMailbox`, which says yes to exactly the
1095
+ addresses `sendMail` takes — it checks with the same predicate: an RFC 5321
1096
+ Mailbox with no source route, no control character (C0, DEL or C1), no
1097
+ `>`, no U+2028 or U+2029, no Unicode format character (`\p{Cf}`), no
1098
+ lone surrogate and no IPv4 literal octet above 255 — so the refusal
1099
+ comes back to whoever gave it:
1100
+
1101
+ ```ts
1102
+ import { isMailbox } from '@bumail/smtp/client';
1103
+
1104
+ isMailbox('mary@example.net'); // true
1105
+ isMailbox('a,b@example.net'); // false: sendMail would refuse it
1106
+ ```
1107
+
979
1108
  One call is one destination. With `{ domain }`, every recipient should be
980
1109
  at that domain: group them by domain first, one `sendMail` each.
981
1110
 
@@ -1276,6 +1405,7 @@ a server of your own.
1276
1405
 
1277
1406
  ```ts
1278
1407
  import {
1408
+ clientKey,
1279
1409
  DataReader,
1280
1410
  decodePlain,
1281
1411
  formatReply,
@@ -1295,6 +1425,12 @@ parsePath('<Joe.Bloggs@EXAMPLE.Org>', false)?.address; // 'Joe.Bloggs@example.or
1295
1425
  parsePath('<>', true); // { address: '', local: '', domain: '' }: the null path, where allowed
1296
1426
  parsePath('<>', false); // undefined
1297
1427
 
1428
+ parsePathCommand('TO:<Postmaster>', 'TO')?.path;
1429
+ // { address: 'postmaster', local: 'postmaster', domain: '', postmaster: true }
1430
+
1431
+ clientKey('::ffff:192.0.2.1'); // '192.0.2.1'
1432
+ clientKey('2001:db8:1:2::9'); // '2001:db8:1:2::/64'
1433
+
1298
1434
  formatReply(reply(550, '5.7.1', ['a', 'b'])); // '550-5.7.1 a\r\n550 5.7.1 b\r\n'
1299
1435
  formatReply(reply(250, '2.0.0', 'OK'), false); // '250 OK\r\n': no enhanced status
1300
1436
 
@@ -1328,6 +1464,7 @@ console.log(chunk.done, reader.bareLineBreaks);
1328
1464
  | Behaviour | RFC |
1329
1465
  | --- | --- |
1330
1466
  | the session, commands, replies, paths, the Received field | RFC 5321 |
1467
+ | `RCPT TO:<postmaster>` with no domain | RFC 5321 §4.1.1.3, §4.5.1 |
1331
1468
  | SIZE | RFC 1870 |
1332
1469
  | PIPELINING | RFC 2920 |
1333
1470
  | 8BITMIME | RFC 6152 |
package/docs/roadmap.md CHANGED
@@ -34,6 +34,50 @@ Nothing in progress.
34
34
 
35
35
  ### Unreleased — merged, not yet published
36
36
 
37
+ - **A connection limit per client**, `maxConnectionsPerClient` (default
38
+ 10): one host can no longer take every slot of `maxConnections` on an
39
+ MX. A client is an IPv4 address, or an IPv6 address by its /64, and an
40
+ IPv4-mapped or NAT64 address counts as its IPv4 address; one more is
41
+ answered `421 4.7.0` and closed, and every close frees its slot.
42
+ `clientKey(address)` is exported, to group clients the same way in
43
+ `onConnect`.
44
+ - **The implicit TLS handshake is bounded.** A socket on an
45
+ `implicitTls` port is counted by `maxConnections` and
46
+ `maxConnectionsPerClient` from the TCP connection on, and closed past
47
+ `handshakeTimeout` (default 10 seconds) if its handshake has not
48
+ completed. Before, a socket that never sent its ClientHello was counted
49
+ by no limit and closed by no timer.
50
+ - **`RCPT TO:<postmaster>` with no domain** (RFC 5321 §4.5.1) is taken
51
+ as this server's postmaster, without AUTH: `onRcptTo` gets a `Path`
52
+ with `postmaster: true`, and the envelope lists it as `'postmaster'`.
53
+
54
+ ### 0.3.0
55
+
56
+ - **`isMailbox(address)`**, from `@bumail/smtp/client` — whether an
57
+ address is exactly one `sendMail` takes, by the same check, so code that
58
+ keeps addresses for later (a queue) refuses a bad one when it takes it,
59
+ rather than at delivery.
60
+
61
+ ### 0.2.1
62
+
63
+ - **Security: no command injection through an address.** `sendMail`
64
+ refuses a source route (`@host:`) in `from` and `to`, and any control
65
+ character, lone surrogate or out-of-range IPv4 literal in an address,
66
+ before it connects; it used to drop the route unchecked and write the
67
+ address as given, so a crafted recipient put extra commands on the
68
+ wire. The server answers a path holding a control character, or a
69
+ malformed route, with `501 5.5.4`. Upgrade if an address can come from
70
+ user input.
71
+ - **A hang-up frees its slot within 500 ms**, not 5 seconds, for a client
72
+ the server had stopped reading because it pipelined more than it could
73
+ take; a client that went quiet still reads the last reply and a clean
74
+ end, on Linux too.
75
+ - **IPv6 address literals** written with their `IPv6:` tag, in `helo`,
76
+ `EHLO` and an address's domain, are taken in any case; one without the
77
+ tag is refused, as RFC 5321 §4.1.3 asks.
78
+
79
+ ### 0.2.0
80
+
37
81
  - **A client that stops reading no longer holds a connection slot.** When
38
82
  the server hangs up on its own — the idle `timeout`, `maxErrors`, failed
39
83
  AUTH, a refusal — it no longer waits for the client to read what is
@@ -83,6 +83,8 @@ A reply carries its enhanced status code (`5.7.1`, RFC 3463) only after
83
83
 
84
84
  - [`421 4.7.0 … Too many errors, closing`](#421-470--too-many-errors-closing)
85
85
  - [`421 4.3.2 … Too many connections, try later`](#421-432--too-many-connections-try-later)
86
+ - [`421 4.7.0 … Too many connections from your address, try later`](#421-470--too-many-connections-from-your-address-try-later)
87
+ - [On implicit TLS, the connection closes before any greeting](#on-implicit-tls-the-connection-closes-before-any-greeting)
86
88
  - [`421 4.4.2 … Idle too long, closing`](#421-442--idle-too-long-closing)
87
89
  - [`554 … Talked before the greeting`](#554--talked-before-the-greeting)
88
90
 
@@ -302,8 +304,9 @@ createSmtpServer({
302
304
 
303
305
  ### `SmtpError: createSmtpServer(): … must be a positive integer, not …`
304
306
 
305
- **When**: `maxMessageSize`, `maxRecipients`, `maxConnections`, `maxErrors`,
306
- `timeout` or `hookTimeout` is `0`, negative, a fraction, `NaN` or
307
+ **When**: `maxMessageSize`, `maxRecipients`, `maxConnections`,
308
+ `maxConnectionsPerClient`, `maxErrors`,
309
+ `timeout`, `hookTimeout` or `handshakeTimeout` is `0`, negative, a fraction, `NaN` or
307
310
  `Infinity`.
308
311
 
309
312
  **Why**: each is a bound on what a client can make the server do, and none
@@ -1352,7 +1355,11 @@ such as an MTA sending to many recipients of which most are refused.
1352
1355
  to retry later.
1353
1356
 
1354
1357
  **Fix**, as the operator: raise `maxConnections`, or look for clients that
1355
- hold connections open without `QUIT`. `server.connections` gives the
1358
+ hold connections open without `QUIT`. When one host takes many of them,
1359
+ lower [`maxConnectionsPerClient`](#421-470--too-many-connections-from-your-address-try-later)
1360
+ (default 10), so it is turned away before it fills the server. On
1361
+ implicit TLS, sockets still in their handshake count too, for
1362
+ `handshakeTimeout` seconds at most. `server.connections` gives the
1356
1363
  number open. A client that stopped reading holds its slot until its
1357
1364
  `timeout` at most: the server's own hang-ups — idle, too many errors, a
1358
1365
  refusal — never wait for it to read, and drop what it did not. In 0.1.0
@@ -1362,6 +1369,80 @@ active.
1362
1369
 
1363
1370
  **Fix**, as a client: retry later, and send `QUIT` when done.
1364
1371
 
1372
+ ### `421 4.7.0 … Too many connections from your address, try later`
1373
+
1374
+ **When**: on connecting, in place of the greeting, when the same client
1375
+ already holds `maxConnectionsPerClient` connections (10 by default). The
1376
+ server hangs up, before `onConnect`. A client is an IPv4 address, or an
1377
+ IPv6 address by its /64; an IPv4-mapped (`::ffff:192.0.2.1`) or NAT64
1378
+ (`64:ff9b::…`) address counts as the IPv4 address inside it.
1379
+
1380
+ **Why**: without it one host could take every slot of `maxConnections`
1381
+ and keep every other sender out. An MTA opens a few connections to one
1382
+ MX at most, and retries a 421 later.
1383
+
1384
+ **Fix**, as the operator: raise `maxConnectionsPerClient` for a known
1385
+ peer that needs more, such as your own relay sending in parallel, or for
1386
+ many users behind one address (a NAT, a /64 shared by a provider):
1387
+
1388
+ ```ts
1389
+ import { createSmtpServer } from '@bumail/smtp';
1390
+
1391
+ const server = createSmtpServer({
1392
+ hostname: 'mx.example.com',
1393
+ localDomains: ['example.com'],
1394
+ maxConnectionsPerClient: 50, // default 10
1395
+ onData: async (message) => {
1396
+ await new Response(message.content).bytes();
1397
+ },
1398
+ });
1399
+ ```
1400
+
1401
+ Check which client is counted with `clientKey(session.remoteAddress)` in
1402
+ `onConnect`. A slot is freed on every close — QUIT, the client hanging up,
1403
+ an error, the idle `timeout`, a refusal — so a count that stays at the
1404
+ limit means the client holds its connections open.
1405
+
1406
+ **Fix**, as a client: send several messages over one connection, close it
1407
+ with `QUIT`, and retry later.
1408
+
1409
+ ### On implicit TLS, the connection closes before any greeting
1410
+
1411
+ **When**: a client on an `implicitTls` port gets no `220` and the
1412
+ connection closes, with no reply, about `handshakeTimeout` seconds (10 by
1413
+ default, up to 4 s later as Bun's timer ticks) after it connected; or at
1414
+ once, when what it sent is not TLS.
1415
+
1416
+ **Why**: it did not complete its TLS handshake in time — it speaks clear
1417
+ SMTP to a TLS port, waits for a greeting before its ClientHello, or never
1418
+ sends one. A socket in its handshake holds a slot of `maxConnections`
1419
+ and `maxConnectionsPerClient`, so the server bounds it. There is no TLS
1420
+ yet to write a reply on.
1421
+
1422
+ **Fix**, as a client: start TLS at once on 465 (`secure: true` with
1423
+ `sendMail`), or use STARTTLS on 587 or 25. As the operator, for clients
1424
+ on slow links, raise the bound:
1425
+
1426
+ ```ts
1427
+ import { createSmtpServer } from '@bumail/smtp';
1428
+
1429
+ const server = createSmtpServer({
1430
+ hostname: 'smtp.example.com',
1431
+ mode: 'submission',
1432
+ implicitTls: true,
1433
+ localDomains: ['example.com'],
1434
+ tls: {
1435
+ key: await Bun.file('/etc/ssl/smtp.example.com.key').text(),
1436
+ cert: await Bun.file('/etc/ssl/smtp.example.com.crt').text(),
1437
+ },
1438
+ authenticate: ({ username, password }) => username === 'alice' && password === Bun.env['ALICE_PASSWORD'],
1439
+ handshakeTimeout: 30, // default 10
1440
+ onData: async (message) => {
1441
+ await new Response(message.content).bytes();
1442
+ },
1443
+ });
1444
+ ```
1445
+
1365
1446
  ### `421 4.4.2 … Idle too long, closing`
1366
1447
 
1367
1448
  **When**: nothing came from the client for `timeout` seconds (300 by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bumail/smtp",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "An SMTP server on Bun.listen — RFC 5321 with PIPELINING, SIZE, 8BITMIME, SMTPUTF8, STARTTLS and AUTH after TLS, policy hooks, and no open relay — and a client that delivers a message to a smarthost or by MX",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "devDependencies": {
50
50
  "@bumail/auth": "^0.3.0",
51
51
  "@bumail/dns": "^0.1.1",
52
- "@bumail/store": "^0.3.0",
52
+ "@bumail/store": "^0.4.0",
53
53
  "@types/bun": "^1.4.2"
54
54
  },
55
55
  "peerDependencies": {
@@ -1,14 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/errors.ts", "../src/protocol/path.ts", "../src/protocol/sasl.ts", "../src/io/outgoing.ts", "../src/protocol/lines.ts"],
4
- "sourcesContent": [
5
- "import type { Reply } from './protocol/reply';\n\n/** Why the server refused an option, why a message stream ended in error, or why `sendMail` failed. */\nexport type SmtpErrorCode =\n\t| 'INVALID_OPTION'\n\t| 'ALREADY_LISTENING'\n\t| 'MESSAGE_TOO_BIG'\n\t| 'BARE_LINE_BREAK'\n\t| 'CONNECTION_LOST'\n\t| 'HOOK_TIMEOUT'\n\t| 'INVALID_HOOK_REPLY'\n\t| 'MESSAGE_NOT_READ'\n\t| 'CONNECTION_FAILED'\n\t| 'TIMEOUT'\n\t| 'BAD_REPLY'\n\t| 'REFUSED'\n\t| 'RECIPIENTS_REFUSED'\n\t| 'TLS_UNAVAILABLE'\n\t| 'TLS_FAILED'\n\t| 'AUTH_UNAVAILABLE'\n\t| 'EXTENSION_MISSING'\n\t| 'NULL_MX'\n\t| 'DNS_FAILED';\n\n/** One recipient and the server's reply to its `RCPT TO`. */\nexport interface RecipientReply {\n\treadonly recipient: string;\n\treadonly reply: Reply;\n}\n\n/** What `sendMail` adds to an error: whether to try again, and the reply behind it. */\nexport interface SmtpErrorDetails {\n\t/** Trying again later may succeed: a 4xx, the network, a timeout. */\n\treadonly temporary?: boolean;\n\t/** The server's reply that failed the delivery, enhanced status code included. */\n\treadonly reply?: Reply;\n\t/** With `RECIPIENTS_REFUSED`: each recipient, and why it was refused. */\n\treadonly rejected?: readonly RecipientReply[];\n}\n\n/** The codes that are temporary unless a reply or `details` says otherwise. */\nconst TEMPORARY = new Set<SmtpErrorCode>([\n\t'CONNECTION_LOST',\n\t'CONNECTION_FAILED',\n\t'TIMEOUT',\n\t'BAD_REPLY',\n\t'TLS_UNAVAILABLE',\n\t'TLS_FAILED',\n]);\n\n/**\n * Thrown by `createSmtpServer` and `listen` for a wrong option or call; the\n * error a message's `content` stream ends with when the message must not be\n * delivered; what `onError` is given for a hook that broke a rule; and what\n * `sendMail` rejects with. `temporary` tells a failure worth trying again\n * later (a 4xx, the network, a timeout) from a permanent one (a 5xx, a null\n * MX, a message the server can never take).\n */\nexport class SmtpError extends Error {\n\toverride readonly name = 'SmtpError';\n\treadonly code: SmtpErrorCode;\n\t/** Trying again later may succeed. Meaningful for `sendMail`'s errors. */\n\treadonly temporary: boolean;\n\t/** The server reply behind a `sendMail` failure, enhanced status code included. */\n\treadonly reply?: Reply;\n\t/** With `RECIPIENTS_REFUSED`: each recipient, and the reply that refused it. */\n\treadonly rejected?: readonly RecipientReply[];\n\n\tconstructor(\n\t\tcode: SmtpErrorCode,\n\t\tmessage: string,\n\t\tdetails: SmtpErrorDetails = {},\n\t) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t\tthis.temporary =\n\t\t\tdetails.temporary ??\n\t\t\t(details.reply ? details.reply.code < 500 : TEMPORARY.has(code));\n\t\tif (details.reply) this.reply = details.reply;\n\t\tif (details.rejected) this.rejected = details.rejected;\n\t}\n}\n",
6
- "/** An envelope address, as `MAIL FROM` and `RCPT TO` give it. */\nexport interface Path {\n\t/** `local@domain`; `''` for the null reverse-path `<>`. */\n\treadonly address: string;\n\treadonly local: string;\n\t/** Lower case. */\n\treadonly domain: string;\n}\n\nconst ATOM = /^[A-Za-z0-9!#$%&'*+\\-/=?^_`{|}~\\u0080-\\u{10ffff}]+$/u;\nconst LABEL =\n\t/^(?:[A-Za-z0-9\\u0080-\\u{10ffff}](?:[A-Za-z0-9\\-\\u0080-\\u{10ffff}]*[A-Za-z0-9\\u0080-\\u{10ffff}])?)$/u;\n\n/**\n * C1 controls, Unicode format characters (zero-width, bidi, BOM), the\n * line and paragraph separators, and lone surrogates: none has a place in\n * an address, and each would land as it is in the Received field and the\n * app's records — a lone surrogate as U+FFFD, once encoded for the wire.\n */\nconst INVISIBLE = /[\\u0080-\\u009f\\u2028\\u2029\\p{Cf}\\p{Cs}]/u;\n\n/**\n * C0 controls (CR, LF and NUL among them), DEL and `>`, anywhere in the\n * path, quoted or not: a CR or an LF would end the command line and start\n * another, and a `>` would end the path where a reader of the line stops.\n */\nfunction hasControl(text: string): boolean {\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst code = text.charCodeAt(i);\n\t\tif (code < 0x20 || code === 0x7f || code === 0x3e) return true;\n\t}\n\treturn false;\n}\n\n/** One hop of a source route, `@domain`, and what follows it: `,` or `:`. */\nconst HOP = /^@(\\[[^\\]]*\\]|[^,:@[\\]]*)([,:])/;\n\nfunction isDotString(text: string): boolean {\n\treturn text.split('.').every((atom) => ATOM.test(atom));\n}\n\nfunction isQuotedString(text: string): boolean {\n\treturn /^\"(?:[\\x20\\x21\\x23-\\x5b\\x5d-\\x7e\\u0080-\\u{10ffff}]|\\\\[\\x20-\\x7e])*\"$/u.test(\n\t\ttext,\n\t);\n}\n\n/**\n * An address literal (§4.1.3): `[IPv6:…]`, its tag in any case (a quoted\n * string in RFC 5234 is), or `[a.b.c.d]` with each octet at most 255.\n */\nexport function isAddressLiteral(text: string): boolean {\n\tif (/^\\[IPv6:[0-9a-f:.]+\\]$/i.test(text)) return true;\n\tconst ipv4 = /^\\[(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\]$/.exec(text);\n\treturn ipv4?.slice(1).every((octet) => Number(octet) <= 255) === true;\n}\n\n/**\n * The argument of EHLO or HELO (RFC 5321 §4.1.1.1): a host name of\n * letters, digits and hyphens, or an address literal.\n */\nexport function isHelloName(text: string): boolean {\n\treturn (\n\t\t/^[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*$/.test(text) || isAddressLiteral(text)\n\t);\n}\n\nfunction isDomain(domain: string): boolean {\n\tif (isAddressLiteral(domain)) return true;\n\treturn (\n\t\tdomain.length <= 255 &&\n\t\tdomain.split('.').every((label) => label.length <= 63 && LABEL.test(label))\n\t);\n}\n\n/**\n * What `parsePath` does with a source route (`<@a,@b:local@domain>`, RFC 5321\n * Appendix C): a server accepts and discards it (`'discard'`), a client\n * refuses it, as §4.1.1.3 says clients should not send one (`'refuse'`).\n */\nexport type SourceRoute = 'discard' | 'refuse';\n\n/**\n * The mailbox after a source route, each of whose hops must be\n * `@domain` with a valid domain; `undefined` for any other route.\n */\nfunction afterRoute(inner: string): string | undefined {\n\tlet rest = inner;\n\tfor (;;) {\n\t\tconst hop = HOP.exec(rest);\n\t\tif (!hop || !isDomain(hop[1] as string)) return undefined;\n\t\trest = rest.slice(hop[0].length);\n\t\tif (hop[2] === ':') return rest;\n\t}\n}\n\n/**\n * Parses the `<path>` of RFC 5321 §4.1.2: `<local@domain>`, or `<>` when\n * `allowNull`. A source route (`<@a,@b:local@domain>`, §C) is dropped when\n * `sourceRoute` is `'discard'`, the default, provided each hop is\n * `@domain`; with `'refuse'` the path is not one. A C0 control (CR, LF,\n * NUL…), DEL or `>` anywhere refuses the path, as do C1 controls,\n * Unicode format characters and lone surrogates. An IPv4 address literal\n * takes octets up to 255.\n * Non-ASCII is allowed: whether the session may use it is SMTPUTF8's\n * question (RFC 6531), not the grammar's. `undefined` when it is not a path.\n *\n * The local part is kept as written, quotes included: `\"v@x.example\"` and\n * `v%x.example` are local parts of the domain after the last `@`, not\n * addresses elsewhere. Code that delivers must not split on the first `@`.\n */\nexport function parsePath(\n\ttext: string,\n\tallowNull: boolean,\n\tsourceRoute: SourceRoute = 'discard',\n): Path | undefined {\n\tif (!text.startsWith('<') || !text.endsWith('>')) return undefined;\n\tlet inner: string | undefined = text.slice(1, -1);\n\tif (hasControl(inner) || INVISIBLE.test(inner)) return undefined;\n\tif (inner === '')\n\t\treturn allowNull ? { address: '', local: '', domain: '' } : undefined;\n\tif (inner.startsWith('@')) {\n\t\tif (sourceRoute === 'refuse') return undefined;\n\t\tinner = afterRoute(inner);\n\t\tif (inner === undefined) return undefined;\n\t}\n\tconst at = inner.lastIndexOf('@');\n\tif (at <= 0) return undefined;\n\tconst local = inner.slice(0, at);\n\tconst domain = inner.slice(at + 1);\n\tif (local.length > 64 || !(isDotString(local) || isQuotedString(local)))\n\t\treturn undefined;\n\tif (!isDomain(domain)) return undefined;\n\treturn {\n\t\taddress: `${local}@${domain.toLowerCase()}`,\n\t\tlocal,\n\t\tdomain: domain.toLowerCase(),\n\t};\n}\n",
7
- "/** Credentials a client gave with `AUTH`. */\nexport interface Credentials {\n\treadonly mechanism: 'PLAIN' | 'LOGIN';\n\treadonly username: string;\n\treadonly password: string;\n\t/**\n\t * PLAIN's authorization identity, when it differs from the username.\n\t * The server refuses such credentials before `authenticate` is asked.\n\t */\n\treadonly authorizationId?: string;\n}\n\nfunction base64ToText(text: string): string | undefined {\n\tif (!/^[A-Za-z0-9+/]*={0,2}$/.test(text) || text.length % 4 !== 0)\n\t\treturn undefined;\n\ttry {\n\t\treturn new TextDecoder('utf-8', { fatal: true }).decode(\n\t\t\tUint8Array.fromBase64(text),\n\t\t);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** PLAIN (RFC 4616): `authzid NUL authcid NUL passwd`, in base64. */\nexport function decodePlain(response: string): Credentials | undefined {\n\tconst text = base64ToText(response);\n\tif (text === undefined) return undefined;\n\tconst parts = text.split('\\0');\n\tif (parts.length !== 3) return undefined;\n\tconst [authorizationId = '', username = '', password = ''] = parts;\n\tif (username === '' || password === '') return undefined;\n\treturn {\n\t\tmechanism: 'PLAIN',\n\t\tusername,\n\t\tpassword,\n\t\t...(authorizationId === '' || authorizationId === username\n\t\t\t? {}\n\t\t\t: { authorizationId }),\n\t};\n}\n\n/** One step of LOGIN: a base64 line to its text. */\nexport function decodeLoginStep(response: string): string | undefined {\n\treturn base64ToText(response);\n}\n\nconst base64 = (text: string) => new TextEncoder().encode(text).toBase64();\n\n/** A client's PLAIN response (RFC 4616): `NUL username NUL password`, in base64, no authorization identity. */\nexport function encodePlain(username: string, password: string): string {\n\treturn base64(`\\0${username}\\0${password}`);\n}\n\n/** One step of LOGIN, the username or the password, in base64. */\nexport function encodeLoginStep(text: string): string {\n\treturn base64(text);\n}\n",
8
- "/**\n * What waits to leave a Bun socket. Bun's sockets do not buffer: `write`\n * takes what the kernel takes and says how much. The rest waits here for\n * the socket's `drain` event, so nothing written is lost to a peer that\n * reads slowly. The server's transport and the client's socket share it.\n */\nexport class Outgoing {\n\treadonly #write: (bytes: Uint8Array) => number;\n\t#queue: Uint8Array[] = [];\n\t#waiters: (() => void)[] = [];\n\n\tconstructor(write: (bytes: Uint8Array) => number) {\n\t\tthis.#write = write;\n\t}\n\n\t/** Nothing waits to leave. */\n\tget empty(): boolean {\n\t\treturn this.#queue.length === 0;\n\t}\n\n\t/** Writes what the socket takes now, and keeps the rest for `drain`. */\n\twrite(bytes: Uint8Array): void {\n\t\tif (this.#queue.length > 0) {\n\t\t\tthis.#queue.push(bytes);\n\t\t\treturn;\n\t\t}\n\t\tconst written = this.#write(bytes);\n\t\tif (written < bytes.length) {\n\t\t\tthis.#queue.push(bytes.subarray(Math.max(written, 0)));\n\t\t}\n\t}\n\n\t/** The socket's `drain`: it can take more. True once everything has left. */\n\tdrain(): boolean {\n\t\twhile (this.#queue.length > 0) {\n\t\t\tconst bytes = this.#queue[0] as Uint8Array;\n\t\t\tconst written = this.#write(bytes);\n\t\t\tif (written < bytes.length) {\n\t\t\t\tthis.#queue[0] = bytes.subarray(Math.max(written, 0));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tthis.#queue.shift();\n\t\t}\n\t\tfor (const wake of this.#waiters.splice(0)) wake();\n\t\treturn true;\n\t}\n\n\t/** Resolves once everything written has left, or the socket closed. */\n\tdrained(): Promise<void> {\n\t\tif (this.#queue.length === 0) return Promise.resolve();\n\t\treturn new Promise((wake) => this.#waiters.push(wake));\n\t}\n\n\t/** The socket closed: nothing more will leave. */\n\tclear(): void {\n\t\tthis.#queue = [];\n\t\tfor (const wake of this.#waiters.splice(0)) wake();\n\t}\n}\n",
9
- "/**\n * The longest line taken, CRLF included: a command (RFC 5321 §4.5.3.1.4\n * asks for 512 at least) or a reply line (§4.5.3.1.5, 512 too).\n */\nexport const MAX_LINE = 2048;\n\n/** One line from the peer, or a line that was too long and was skipped. */\nexport type LineEvent = { line: string } | { tooLong: true };\n\n/**\n * Cuts a peer's bytes into lines — a client's commands, a server's\n * replies — ending at LF with or without CR. It holds one partial line at most: past `MAX_LINE`, the line is\n * skipped to its end and reported once.\n */\nexport class LineSplitter {\n\t#held: Uint8Array = new Uint8Array(0);\n\t#skipping = false;\n\t#rest: Uint8Array = new Uint8Array(0);\n\n\t/** Starts on a chunk; read its lines with `next`. */\n\tpush(chunk: Uint8Array): void {\n\t\tthis.#rest = this.#held.length > 0 ? concat(this.#held, chunk) : chunk;\n\t\tthis.#held = new Uint8Array(0);\n\t}\n\n\t/** The next line of the current chunk, or `undefined` when it has no more. */\n\tnext(): LineEvent | undefined {\n\t\tfor (;;) {\n\t\t\tconst data = this.#rest;\n\t\t\tconst lf = data.indexOf(0x0a);\n\t\t\tif (lf < 0) {\n\t\t\t\tthis.#rest = new Uint8Array(0);\n\t\t\t\tif (this.#skipping) return undefined;\n\t\t\t\tif (data.length > MAX_LINE) {\n\t\t\t\t\tthis.#skipping = true;\n\t\t\t\t\treturn { tooLong: true };\n\t\t\t\t}\n\t\t\t\tthis.#held = data.slice();\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tconst end = lf > 0 && data[lf - 1] === 0x0d ? lf - 1 : lf;\n\t\t\tthis.#rest = data.subarray(lf + 1);\n\t\t\tif (this.#skipping) {\n\t\t\t\tthis.#skipping = false;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (end > MAX_LINE) return { tooLong: true };\n\t\t\treturn { line: new TextDecoder().decode(data.subarray(0, end)) };\n\t\t}\n\t}\n\n\t/** Bytes are held or left: a line begun, or lines not yet read. */\n\tget holding(): boolean {\n\t\treturn this.#held.length > 0 || this.#rest.length > 0;\n\t}\n\n\t/** What is left of the current chunk: message content pipelined after DATA. */\n\ttakeRest(): Uint8Array {\n\t\tconst rest = this.#rest;\n\t\tthis.#rest = new Uint8Array(0);\n\t\treturn rest;\n\t}\n\n\t/** Forgets everything held: after STARTTLS, or after a refused DATA. */\n\tclear(): void {\n\t\tthis.#held = new Uint8Array(0);\n\t\tthis.#rest = new Uint8Array(0);\n\t\tthis.#skipping = false;\n\t}\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n\tconst out = new Uint8Array(a.length + b.length);\n\tout.set(a, 0);\n\tout.set(b, a.length);\n\treturn out;\n}\n"
10
- ],
11
- "mappings": "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",
12
- "debugId": "10D3BC4806ADB37C64756E2164756E21",
13
- "names": []
14
- }