@bumail/smtp 0.3.0 → 0.5.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 (52) hide show
  1. package/README.md +105 -7
  2. package/dist/chunks/{index-3423jxf9.js → index-n4xwep5m.js} +10 -4
  3. package/dist/chunks/index-n4xwep5m.js.map +14 -0
  4. package/dist/client/index.js +1 -1
  5. package/dist/errors.d.ts +3 -1
  6. package/dist/errors.d.ts.map +1 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +724 -126
  10. package/dist/index.js.map +19 -9
  11. package/dist/protocol/command.d.ts +4 -2
  12. package/dist/protocol/command.d.ts.map +1 -1
  13. package/dist/protocol/path.d.ts +13 -2
  14. package/dist/protocol/path.d.ts.map +1 -1
  15. package/dist/server/admission.d.ts +29 -0
  16. package/dist/server/admission.d.ts.map +1 -0
  17. package/dist/server/client-key.d.ts +15 -0
  18. package/dist/server/client-key.d.ts.map +1 -0
  19. package/dist/server/listener.d.ts +46 -0
  20. package/dist/server/listener.d.ts.map +1 -0
  21. package/dist/server/options.d.ts +45 -1
  22. package/dist/server/options.d.ts.map +1 -1
  23. package/dist/server/proxy/address.d.ts +17 -0
  24. package/dist/server/proxy/address.d.ts.map +1 -0
  25. package/dist/server/proxy/header.d.ts +26 -0
  26. package/dist/server/proxy/header.d.ts.map +1 -0
  27. package/dist/server/proxy/reader.d.ts +15 -0
  28. package/dist/server/proxy/reader.d.ts.map +1 -0
  29. package/dist/server/proxy/tls.d.ts +50 -0
  30. package/dist/server/proxy/tls.d.ts.map +1 -0
  31. package/dist/server/proxy/trusted.d.ts +9 -0
  32. package/dist/server/proxy/trusted.d.ts.map +1 -0
  33. package/dist/server/raw-socket.d.ts +9 -0
  34. package/dist/server/raw-socket.d.ts.map +1 -0
  35. package/dist/server/received.d.ts +2 -0
  36. package/dist/server/received.d.ts.map +1 -1
  37. package/dist/server/server.d.ts.map +1 -1
  38. package/dist/server/settings.d.ts +4 -0
  39. package/dist/server/settings.d.ts.map +1 -1
  40. package/dist/server/slots.d.ts +14 -0
  41. package/dist/server/slots.d.ts.map +1 -0
  42. package/dist/server/tls-context.d.ts +16 -0
  43. package/dist/server/tls-context.d.ts.map +1 -0
  44. package/dist/server/transaction.d.ts.map +1 -1
  45. package/dist/server/transport.d.ts +2 -2
  46. package/dist/server/transport.d.ts.map +1 -1
  47. package/docs/README.md +2 -2
  48. package/docs/guide.md +472 -12
  49. package/docs/roadmap.md +57 -0
  50. package/docs/troubleshooting.md +406 -4
  51. package/package.json +2 -2
  52. package/dist/chunks/index-3423jxf9.js.map +0 -14
package/docs/roadmap.md CHANGED
@@ -34,6 +34,63 @@ Nothing in progress.
34
34
 
35
35
  ### Unreleased — merged, not yet published
36
36
 
37
+ - **Behind a TCP proxy, the client's own address**, with `proxyProtocol:
38
+ { trusted: [...] }`: the server reads the PROXY protocol, versions 1 and
39
+ 2, from the proxies listed — a Traefik TCP router, HAProxy — so
40
+ `session.remoteAddress`, every hook, the Received field and
41
+ `maxConnectionsPerClient` see the client, not the proxy. It works on 25
42
+ and 587 with STARTTLS and on 465 with implicit TLS. A listed peer that
43
+ sends no valid header within `handshakeTimeout` is reset without a word
44
+ and holds no slot while it waits; a peer not listed is served as before,
45
+ and a header it sends never sets an address. `ProxyProtocolOptions` is
46
+ exported.
47
+
48
+ ### 0.4.0
49
+
50
+ - **A connection limit per client**, `maxConnectionsPerClient` (default
51
+ 10): one host can no longer take every slot of `maxConnections` on an
52
+ MX. A client is an IPv4 address, or an IPv6 address by its /64, and an
53
+ IPv4-mapped or NAT64 address counts as its IPv4 address; one more is
54
+ answered `421 4.7.0` and closed, and every close frees its slot.
55
+ `clientKey(address)` is exported, to group clients the same way in
56
+ `onConnect`.
57
+ - **The implicit TLS handshake is bounded.** A socket on an
58
+ `implicitTls` port is counted by `maxConnections` and
59
+ `maxConnectionsPerClient` from the TCP connection on, and closed past
60
+ `handshakeTimeout` (default 10 seconds) if its handshake has not
61
+ completed. Before, a socket that never sent its ClientHello was counted
62
+ by no limit and closed by no timer.
63
+ - **`RCPT TO:<postmaster>` with no domain** (RFC 5321 §4.5.1) is taken
64
+ as this server's postmaster, without AUTH: `onRcptTo` gets a `Path`
65
+ with `postmaster: true`, and the envelope lists it as `'postmaster'`.
66
+
67
+ ### 0.3.0
68
+
69
+ - **`isMailbox(address)`**, from `@bumail/smtp/client` — whether an
70
+ address is exactly one `sendMail` takes, by the same check, so code that
71
+ keeps addresses for later (a queue) refuses a bad one when it takes it,
72
+ rather than at delivery.
73
+
74
+ ### 0.2.1
75
+
76
+ - **Security: no command injection through an address.** `sendMail`
77
+ refuses a source route (`@host:`) in `from` and `to`, and any control
78
+ character, lone surrogate or out-of-range IPv4 literal in an address,
79
+ before it connects; it used to drop the route unchecked and write the
80
+ address as given, so a crafted recipient put extra commands on the
81
+ wire. The server answers a path holding a control character, or a
82
+ malformed route, with `501 5.5.4`. Upgrade if an address can come from
83
+ user input.
84
+ - **A hang-up frees its slot within 500 ms**, not 5 seconds, for a client
85
+ the server had stopped reading because it pipelined more than it could
86
+ take; a client that went quiet still reads the last reply and a clean
87
+ end, on Linux too.
88
+ - **IPv6 address literals** written with their `IPv6:` tag, in `helo`,
89
+ `EHLO` and an address's domain, are taken in any case; one without the
90
+ tag is refused, as RFC 5321 §4.1.3 asks.
91
+
92
+ ### 0.2.0
93
+
37
94
  - **A client that stops reading no longer holds a connection slot.** When
38
95
  the server hangs up on its own — the idle `timeout`, `maxErrors`, failed
39
96
  AUTH, a refusal — it no longer waits for the client to read what is
@@ -24,7 +24,15 @@ A reply carries its enhanced status code (`5.7.1`, RFC 3463) only after
24
24
  - [`SmtpError: createSmtpServer(): hookTimeout must be at most 2147483 seconds, not …`](#smtperror-createsmtpserver-hooktimeout-must-be-at-most-2147483-seconds-not-)
25
25
  - [`SmtpError: createSmtpServer(): greetingDelay must be a number of seconds, 0 or more, not …`](#smtperror-createsmtpserver-greetingdelay-must-be-a-number-of-seconds-0-or-more-not-)
26
26
  - [`SmtpError: createSmtpServer(): greetingDelay (… s) must be shorter than timeout (… s), or every client times out before the greeting`](#smtperror-createsmtpserver-greetingdelay--s-must-be-shorter-than-timeout--s-or-every-client-times-out-before-the-greeting)
27
+ - [`SmtpError: createSmtpServer(): proxyProtocol.trusted must list the addresses or CIDRs of the proxies, at least one`](#smtperror-createsmtpserver-proxyprotocoltrusted-must-list-the-addresses-or-cidrs-of-the-proxies-at-least-one)
28
+ - [`SmtpError: createSmtpServer(): proxyProtocol.trusted: "…" is neither an IP address nor a CIDR`](#smtperror-createsmtpserver-proxyprotocoltrusted--is-neither-an-ip-address-nor-a-cidr)
29
+ - [`SmtpError: createSmtpServer(): proxyProtocol.trusted: "…" has a prefix length out of range`](#smtperror-createsmtpserver-proxyprotocoltrusted--has-a-prefix-length-out-of-range)
30
+ - [`SmtpError: createSmtpServer(): proxyProtocol.trusted: "…" has a prefix length of 0, which trusts every peer`](#smtperror-createsmtpserver-proxyprotocoltrusted--has-a-prefix-length-of-0-which-trusts-every-peer)
31
+ - [`SmtpError: createSmtpServer(): proxyProtocol.trusted: … is not a string`](#smtperror-createsmtpserver-proxyprotocoltrusted--is-not-a-string)
27
32
  - [`SmtpError: listen(): the server is already listening on …`](#smtperror-listen-the-server-is-already-listening-on-)
33
+ - [`SmtpError: listen(): the server is already starting to listen`](#smtperror-listen-the-server-is-already-starting-to-listen)
34
+ - [`SmtpError: listen(): tls: { key, cert } cannot be used: …`](#smtperror-listen-tls--key-cert--cannot-be-used-)
35
+ - [`SmtpError: listen(): stop() was called before the server bound its port`](#smtperror-listen-stop-was-called-before-the-server-bound-its-port)
28
36
 
29
37
  **Relaying and authentication**
30
38
 
@@ -83,6 +91,12 @@ A reply carries its enhanced status code (`5.7.1`, RFC 3463) only after
83
91
 
84
92
  - [`421 4.7.0 … Too many errors, closing`](#421-470--too-many-errors-closing)
85
93
  - [`421 4.3.2 … Too many connections, try later`](#421-432--too-many-connections-try-later)
94
+ - [`421 4.7.0 … Too many connections from your address, try later`](#421-470--too-many-connections-from-your-address-try-later)
95
+ - [On implicit TLS, the connection closes before any greeting](#on-implicit-tls-the-connection-closes-before-any-greeting)
96
+ - [Connections through the proxy close at once, with no greeting](#connections-through-the-proxy-close-at-once-with-no-greeting)
97
+ - [Every client through the proxy is refused with `554 … Talked before the greeting`](#every-client-through-the-proxy-is-refused-with-554--talked-before-the-greeting)
98
+ - [Every client shows the proxy's address](#every-client-shows-the-proxys-address)
99
+ - [Health checks from the proxy show its own address](#health-checks-from-the-proxy-show-its-own-address)
86
100
  - [`421 4.4.2 … Idle too long, closing`](#421-442--idle-too-long-closing)
87
101
  - [`554 … Talked before the greeting`](#554--talked-before-the-greeting)
88
102
 
@@ -302,8 +316,9 @@ createSmtpServer({
302
316
 
303
317
  ### `SmtpError: createSmtpServer(): … must be a positive integer, not …`
304
318
 
305
- **When**: `maxMessageSize`, `maxRecipients`, `maxConnections`, `maxErrors`,
306
- `timeout` or `hookTimeout` is `0`, negative, a fraction, `NaN` or
319
+ **When**: `maxMessageSize`, `maxRecipients`, `maxConnections`,
320
+ `maxConnectionsPerClient`, `maxErrors`,
321
+ `timeout`, `hookTimeout` or `handshakeTimeout` is `0`, negative, a fraction, `NaN` or
307
322
  `Infinity`.
308
323
 
309
324
  **Why**: each is a bound on what a client can make the server do, and none
@@ -406,6 +421,124 @@ createSmtpServer({
406
421
  });
407
422
  ```
408
423
 
424
+ ### `SmtpError: createSmtpServer(): proxyProtocol.trusted must list the addresses or CIDRs of the proxies, at least one`
425
+
426
+ **When**: `proxyProtocol` is set without `trusted`, with an empty array, or
427
+ with something that is not an array — a single string included.
428
+
429
+ **Why**: `trusted` names the only peers whose PROXY header is read. Empty,
430
+ it would trust nobody, which is no proxy at all; a server not behind a
431
+ proxy leaves `proxyProtocol` out.
432
+
433
+ **Fix**: list the proxy's own address, in an array:
434
+
435
+ ```ts
436
+ import { createSmtpServer } from '@bumail/smtp';
437
+
438
+ createSmtpServer({
439
+ hostname: 'mx.example.com',
440
+ localDomains: ['example.com'],
441
+ proxyProtocol: { trusted: ['10.0.0.5'] }, // not trusted: '10.0.0.5'
442
+ onData: async (message) => {
443
+ await new Response(message.content).bytes();
444
+ },
445
+ });
446
+ ```
447
+
448
+ ### `SmtpError: createSmtpServer(): proxyProtocol.trusted: "…" is neither an IP address nor a CIDR`
449
+
450
+ **When**: an entry of `trusted` is a host name (`traefik`), an address with
451
+ a port (`10.0.0.5:25`), a malformed address, an address with a zone
452
+ (`fe80::1%eth0`: a zone names an interface of this host, never a client),
453
+ or holds more than one `/`.
454
+ `…` is the entry.
455
+
456
+ **Why**: the server matches the TCP address a connection comes from; it
457
+ resolves no name, since a name could resolve to anything later.
458
+
459
+ **Fix**: give the proxy's IP address, or its network as a CIDR. In a
460
+ container network, give the proxy a fixed address and list that:
461
+
462
+ ```ts
463
+ import { createSmtpServer } from '@bumail/smtp';
464
+
465
+ createSmtpServer({
466
+ hostname: 'mx.example.com',
467
+ localDomains: ['example.com'],
468
+ proxyProtocol: { trusted: ['172.20.0.2', 'fd00:20::2'] },
469
+ onData: async (message) => {
470
+ await new Response(message.content).bytes();
471
+ },
472
+ });
473
+ ```
474
+
475
+ ### `SmtpError: createSmtpServer(): proxyProtocol.trusted: "…" has a prefix length out of range`
476
+
477
+ **When**: an entry's prefix is past 32 for IPv4 (`10.0.0.0/33`) or 128 for
478
+ IPv6, is empty or not a number (`10.0.0.0/`, `10.0.0.0/8a`), or is below
479
+ 96 for an IPv4-mapped address (`::ffff:10.0.0.0/8`).
480
+
481
+ **Why**: an IPv4-mapped entry is taken as its IPv4 address, and its
482
+ prefix counts over the whole IPv6 address, the 96 bits of `::ffff:`
483
+ first: `::ffff:10.0.0.0/104` is `10.0.0.0/8`.
484
+
485
+ **Fix**: write an IPv4 network as IPv4; it matches mapped peers too:
486
+
487
+ ```ts
488
+ import { createSmtpServer } from '@bumail/smtp';
489
+
490
+ createSmtpServer({
491
+ hostname: 'mx.example.com',
492
+ localDomains: ['example.com'],
493
+ proxyProtocol: { trusted: ['10.0.0.0/24'] }, // matches ::ffff:10.0.0.5 as well
494
+ onData: async (message) => {
495
+ await new Response(message.content).bytes();
496
+ },
497
+ });
498
+ ```
499
+
500
+ ### `SmtpError: createSmtpServer(): proxyProtocol.trusted: "…" has a prefix length of 0, which trusts every peer`
501
+
502
+ **When**: an entry has a prefix of 0: `0.0.0.0/0`, `::/0`, or
503
+ `::ffff:0.0.0.0/96`, which is the same. `…` is the entry.
504
+
505
+ **Why**: such an entry matches every peer, so any client could send a
506
+ PROXY header and claim any address: the option would defend nothing.
507
+
508
+ **Fix**: list the proxy's own address, or the smallest network it is in:
509
+
510
+ ```ts
511
+ proxyProtocol: { trusted: ['172.20.0.2'] }, // not '0.0.0.0/0'
512
+ ```
513
+
514
+ ### `SmtpError: createSmtpServer(): proxyProtocol.trusted: … is not a string`
515
+
516
+ **When**: an entry of `trusted` is not a string: a number, `null`,
517
+ `undefined`, an object. `…` is the value.
518
+
519
+ **Why**: each entry is an address or a CIDR, written as text.
520
+
521
+ **Fix**: write each entry as a string, and filter out what is not set when
522
+ the list comes from the environment:
523
+
524
+ ```ts
525
+ import { createSmtpServer } from '@bumail/smtp';
526
+
527
+ const trusted = (Bun.env['SMTP_TRUSTED_PROXIES'] ?? '')
528
+ .split(',')
529
+ .map((entry) => entry.trim())
530
+ .filter((entry) => entry !== '');
531
+
532
+ createSmtpServer({
533
+ hostname: 'mx.example.com',
534
+ localDomains: ['example.com'],
535
+ ...(trusted.length > 0 ? { proxyProtocol: { trusted } } : {}),
536
+ onData: async (message) => {
537
+ await new Response(message.content).bytes();
538
+ },
539
+ });
540
+ ```
541
+
409
542
  ### `SmtpError: listen(): the server is already listening on …`
410
543
 
411
544
  **When**: `listen` on a server that is already listening. Its `code` is
@@ -431,6 +564,67 @@ await createSmtpServer(options).listen({ port: 25 });
431
564
  await createSmtpServer(options).listen({ port: 2525 });
432
565
  ```
433
566
 
567
+ ### `SmtpError: listen(): the server is already starting to listen`
568
+
569
+ **When**: a second `listen` on the same server while the first has not
570
+ resolved yet. With `implicitTls`, `listen` reads the key and certificate
571
+ before it binds, so two calls not awaited in turn meet here. Its `code` is
572
+ `ALREADY_LISTENING`; the first call binds, this one binds nothing.
573
+
574
+ **Fix**: await `listen` once per server, and create one server per port:
575
+
576
+ ```ts
577
+ const server = createSmtpServer({ ...options, tls, implicitTls: true });
578
+ await server.listen({ port: 465 }); // not Promise.all([server.listen(…), server.listen(…)])
579
+ ```
580
+
581
+ ### `SmtpError: listen(): stop() was called before the server bound its port`
582
+
583
+ **When**: `stop()` ran before `listen` resolved: on an `implicitTls` server
584
+ while it reads the TLS key and certificate, on any other at once, since
585
+ `listen` resolves a tick after it binds. `listen` rejects with
586
+ `code: 'STOPPED'` and leaves nothing listening; a later `listen` binds as
587
+ usual.
588
+
589
+ **Why**: `stop()` means the server is not to serve; binding afterwards would
590
+ leave a listener nothing could stop.
591
+
592
+ **Fix**: nothing is wrong if the stop was meant. To start again, call
593
+ `listen` again:
594
+
595
+ ```ts
596
+ await server.listen({ port: 465 }).catch((error) => {
597
+ if (error.code !== 'STOPPED') throw error;
598
+ });
599
+ ```
600
+
601
+ ### `SmtpError: listen(): tls: { key, cert } cannot be used: …`
602
+
603
+ **When**: `listen` on an `implicitTls` server whose `tls.key` or
604
+ `tls.cert` cannot be read — a `Bun.file` that does not exist — or is not
605
+ a PEM key or certificate. The same error, word for word, with
606
+ `proxyProtocol` or without. Its `code` is `INVALID_OPTION`; the rest of
607
+ the message, and its `cause`, are the reason from the file system or
608
+ `node:tls` (`ENOENT: no such file or directory, open '…'`,
609
+ `…PEM routines…NO_START_LINE`). Nothing is bound.
610
+
611
+ **Why**: `listen` reads both and checks them as a TLS context before it
612
+ binds, so a bad key fails at start, not at the first client.
613
+
614
+ **Fix**: pass the PEM text, or a `Bun.file` of a path that exists:
615
+
616
+ ```ts
617
+ const tls = {
618
+ key: Bun.file('/etc/bumail/tls/privkey.pem'),
619
+ cert: Bun.file('/etc/bumail/tls/fullchain.pem'),
620
+ };
621
+ try {
622
+ await createSmtpServer({ ...options, tls, implicitTls: true }).listen({ port: 465 });
623
+ } catch (error) {
624
+ console.error(error, (error as Error).cause);
625
+ }
626
+ ```
627
+
434
628
  ## Relaying and authentication
435
629
 
436
630
  ### `554 5.7.1 Relay access denied`
@@ -1352,7 +1546,11 @@ such as an MTA sending to many recipients of which most are refused.
1352
1546
  to retry later.
1353
1547
 
1354
1548
  **Fix**, as the operator: raise `maxConnections`, or look for clients that
1355
- hold connections open without `QUIT`. `server.connections` gives the
1549
+ hold connections open without `QUIT`. When one host takes many of them,
1550
+ lower [`maxConnectionsPerClient`](#421-470--too-many-connections-from-your-address-try-later)
1551
+ (default 10), so it is turned away before it fills the server. On
1552
+ implicit TLS, sockets still in their handshake count too, for
1553
+ `handshakeTimeout` seconds at most. `server.connections` gives the
1356
1554
  number open. A client that stopped reading holds its slot until its
1357
1555
  `timeout` at most: the server's own hang-ups — idle, too many errors, a
1358
1556
  refusal — never wait for it to read, and drop what it did not. In 0.1.0
@@ -1362,6 +1560,208 @@ active.
1362
1560
 
1363
1561
  **Fix**, as a client: retry later, and send `QUIT` when done.
1364
1562
 
1563
+ ### `421 4.7.0 … Too many connections from your address, try later`
1564
+
1565
+ **When**: on connecting, in place of the greeting, when the same client
1566
+ already holds `maxConnectionsPerClient` connections (10 by default). The
1567
+ server hangs up, before `onConnect`. A client is an IPv4 address, or an
1568
+ IPv6 address by its /64; an IPv4-mapped (`::ffff:192.0.2.1`) or NAT64
1569
+ (`64:ff9b::…`) address counts as the IPv4 address inside it.
1570
+
1571
+ **Why**: without it one host could take every slot of `maxConnections`
1572
+ and keep every other sender out. An MTA opens a few connections to one
1573
+ MX at most, and retries a 421 later.
1574
+
1575
+ **Fix**, as the operator: raise `maxConnectionsPerClient` for a known
1576
+ peer that needs more, such as your own relay sending in parallel, or for
1577
+ many users behind one address (a NAT, a /64 shared by a provider):
1578
+
1579
+ ```ts
1580
+ import { createSmtpServer } from '@bumail/smtp';
1581
+
1582
+ const server = createSmtpServer({
1583
+ hostname: 'mx.example.com',
1584
+ localDomains: ['example.com'],
1585
+ maxConnectionsPerClient: 50, // default 10
1586
+ onData: async (message) => {
1587
+ await new Response(message.content).bytes();
1588
+ },
1589
+ });
1590
+ ```
1591
+
1592
+ Check which client is counted with `clientKey(session.remoteAddress)` in
1593
+ `onConnect`. A slot is freed on every close — QUIT, the client hanging up,
1594
+ an error, the idle `timeout`, a refusal — so a count that stays at the
1595
+ limit means the client holds its connections open.
1596
+
1597
+ **Fix**, as a client: send several messages over one connection, close it
1598
+ with `QUIT`, and retry later.
1599
+
1600
+ ### On implicit TLS, the connection closes before any greeting
1601
+
1602
+ **When**: a client on an `implicitTls` port gets no `220` and the
1603
+ connection closes, with no reply, about `handshakeTimeout` seconds (10 by
1604
+ default, up to 4 s later as Bun's timer ticks) after it connected; or at
1605
+ once, when what it sent is not TLS.
1606
+
1607
+ **Why**: it did not complete its TLS handshake in time — it speaks clear
1608
+ SMTP to a TLS port, waits for a greeting before its ClientHello, or never
1609
+ sends one. A socket in its handshake holds a slot of `maxConnections`
1610
+ and `maxConnectionsPerClient`, so the server bounds it. There is no TLS
1611
+ yet to write a reply on.
1612
+
1613
+ **Fix**, as a client: start TLS at once on 465 (`secure: true` with
1614
+ `sendMail`), or use STARTTLS on 587 or 25. As the operator, for clients
1615
+ on slow links, raise the bound:
1616
+
1617
+ ```ts
1618
+ import { createSmtpServer } from '@bumail/smtp';
1619
+
1620
+ const server = createSmtpServer({
1621
+ hostname: 'smtp.example.com',
1622
+ mode: 'submission',
1623
+ implicitTls: true,
1624
+ localDomains: ['example.com'],
1625
+ tls: {
1626
+ key: await Bun.file('/etc/ssl/smtp.example.com.key').text(),
1627
+ cert: await Bun.file('/etc/ssl/smtp.example.com.crt').text(),
1628
+ },
1629
+ authenticate: ({ username, password }) => username === 'alice' && password === Bun.env['ALICE_PASSWORD'],
1630
+ handshakeTimeout: 30, // default 10
1631
+ onData: async (message) => {
1632
+ await new Response(message.content).bytes();
1633
+ },
1634
+ });
1635
+ ```
1636
+
1637
+ ### Connections through the proxy close at once, with no greeting
1638
+
1639
+ **When**: a client connecting through a TCP proxy gets no `220`, and the
1640
+ connection closes without a reply — at once, or `handshakeTimeout` seconds
1641
+ (10 by default) after it opened. `onConnect` is not called, `onError` is
1642
+ told nothing, and `connections` does not count the socket.
1643
+
1644
+ **Why**: the server trusts the proxy (`proxyProtocol.trusted` lists it),
1645
+ so it waits for a PROXY header first, and resets a socket whose header is
1646
+ missing, late or not valid, writing nothing. Usually:
1647
+
1648
+ - the proxy is not configured to send the header at all, so the client's
1649
+ bytes, or nothing, come first;
1650
+ - it sends a version 1 line longer than 107 bytes, or a version 2 header
1651
+ with more than 2048 bytes of TLVs;
1652
+ - something other than the proxy connects from a listed address, a health
1653
+ checker that opens a bare TCP connection for instance.
1654
+
1655
+ On an `implicitTls` port, the reverse also closes at once: the proxy sends
1656
+ a header to a server that does not trust it, and the header is not a TLS
1657
+ ClientHello, so the handshake fails.
1658
+
1659
+ **Fix**: send the header from the proxy (with Traefik, a TCP
1660
+ `serversTransport` with `proxyProtocol: { version: 2 }`, named by the
1661
+ service), and list the proxy on the server. Both
1662
+ sides together, or neither:
1663
+
1664
+ ```ts
1665
+ import { createSmtpServer } from '@bumail/smtp';
1666
+
1667
+ createSmtpServer({
1668
+ hostname: 'mx.example.com',
1669
+ localDomains: ['example.com'],
1670
+ proxyProtocol: { trusted: ['10.0.0.5'] }, // the address the proxy connects from
1671
+ handshakeTimeout: 10, // the header must arrive within it
1672
+ onData: async (message) => {
1673
+ await new Response(message.content).bytes();
1674
+ },
1675
+ });
1676
+ ```
1677
+
1678
+ The [guide](guide.md#with-traefik) has a Traefik configuration.
1679
+
1680
+ ### Every client through the proxy is refused with `554 … Talked before the greeting`
1681
+
1682
+ **When**: on a clear port (25, 587), every connection through the proxy
1683
+ gets `554 … Talked before the greeting`, and the server sees every client
1684
+ with the proxy's address. When the header came after the greeting, the
1685
+ client gets replies to input it never sent instead: `500` to a version 1
1686
+ line starting `PROXY`, error replies to the binary of a version 2 header.
1687
+
1688
+ **Why**: the proxy sends a PROXY header, but the server does not trust it:
1689
+ `proxyProtocol` is not set, or `trusted` does not list the address the
1690
+ proxy connects from. To a peer not listed, the header is the client's own
1691
+ input, sent before the greeting, and it never sets an address.
1692
+
1693
+ **Fix**: list the proxy's address in `proxyProtocol.trusted`, as the
1694
+ address the server sees it connect from; see the next entry for how to
1695
+ find it.
1696
+
1697
+ ### Every client shows the proxy's address
1698
+
1699
+ **When**: `session.remoteAddress`, the Received field and
1700
+ `maxConnectionsPerClient` see one address, the proxy's, for every client:
1701
+ one more client past `maxConnectionsPerClient` is refused with `421 4.7.0
1702
+ … Too many connections from your address, try later`.
1703
+
1704
+ **Why**: the server is not reading a PROXY header from that peer. Either
1705
+ the proxy sends none — the server then serves its connections as any
1706
+ other's — or the server does not list it, and the header it sends is
1707
+ refused as bad input (the entry above). A common case is a proxy that
1708
+ connects from another address than the one listed: another container
1709
+ network, an IPv6 address, or a new address after a restart. An
1710
+ IPv4-mapped peer (`::ffff:172.20.0.2`) matches an IPv4 entry; it needs no
1711
+ entry of its own.
1712
+
1713
+ **Fix**: log the peer the server sees, then list exactly that address —
1714
+ not the range clients come from:
1715
+
1716
+ ```ts
1717
+ import { createSmtpServer } from '@bumail/smtp';
1718
+
1719
+ createSmtpServer({
1720
+ hostname: 'mx.example.com',
1721
+ localDomains: ['example.com'],
1722
+ // Without proxyProtocol, every connection through the proxy shows its address.
1723
+ onConnect: (session) => console.log('peer', session.remoteAddress),
1724
+ onData: async (message) => {
1725
+ await new Response(message.content).bytes();
1726
+ },
1727
+ });
1728
+ ```
1729
+
1730
+ Then configure the proxy to send the header, and set
1731
+ `proxyProtocol: { trusted: ['<that address>'] }`.
1732
+
1733
+ ### Health checks from the proxy show its own address
1734
+
1735
+ **When**: with `proxyProtocol` set and working, some connections still have
1736
+ the proxy's address in `session.remoteAddress`: they open and close
1737
+ without a transaction, at a steady pace.
1738
+
1739
+ **Why**: by design. The proxy's own connections — health checks — come
1740
+ with a version 2 `LOCAL` header, which names no client, so the server
1741
+ keeps the peer's address, the proxy's. So do a v1 `PROXY UNKNOWN` and a
1742
+ v2 header with an UNSPEC or UNIX address, or over UDP.
1743
+
1744
+ **Fix**: none needed. To keep them out of your logs or your policy, skip
1745
+ the proxy's address in `onConnect`:
1746
+
1747
+ ```ts
1748
+ import { createSmtpServer } from '@bumail/smtp';
1749
+
1750
+ const proxies = ['10.0.0.5'];
1751
+
1752
+ createSmtpServer({
1753
+ hostname: 'mx.example.com',
1754
+ localDomains: ['example.com'],
1755
+ proxyProtocol: { trusted: proxies },
1756
+ onConnect: (session) => {
1757
+ if (!proxies.includes(session.remoteAddress)) console.log('client', session.remoteAddress);
1758
+ },
1759
+ onData: async (message) => {
1760
+ await new Response(message.content).bytes();
1761
+ },
1762
+ });
1763
+ ```
1764
+
1365
1765
  ### `421 4.4.2 … Idle too long, closing`
1366
1766
 
1367
1767
  **When**: nothing came from the client for `timeout` seconds (300 by
@@ -1415,7 +1815,9 @@ client library does; a hand-written client or a test script may not.
1415
1815
 
1416
1816
  **Fix**, as the operator: if a legitimate sender is refused, lower
1417
1817
  `greetingDelay`, or leave it at `0`; a test client that writes at once
1418
- needs it at `0`.
1818
+ needs it at `0`. If every client through a TCP proxy gets it, the proxy
1819
+ sends a PROXY header the server does not trust: see
1820
+ [that entry](#every-client-through-the-proxy-is-refused-with-554--talked-before-the-greeting).
1419
1821
 
1420
1822
  ## Sending mail: options
1421
1823
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bumail/smtp",
3
- "version": "0.3.0",
3
+ "version": "0.5.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 * U+2028 and U+2029, 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
- }