@bumail/smtp 0.4.0 → 0.6.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 (43) hide show
  1. package/README.md +64 -5
  2. package/dist/chunks/{index-753q702a.js → index-n4xwep5m.js} +3 -3
  3. package/dist/chunks/{index-753q702a.js.map → index-n4xwep5m.js.map} +3 -3
  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 +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +617 -71
  10. package/dist/index.js.map +15 -8
  11. package/dist/server/admission.d.ts +37 -0
  12. package/dist/server/admission.d.ts.map +1 -0
  13. package/dist/server/listener.d.ts +17 -8
  14. package/dist/server/listener.d.ts.map +1 -1
  15. package/dist/server/options.d.ts +21 -1
  16. package/dist/server/options.d.ts.map +1 -1
  17. package/dist/server/proxy/address.d.ts +17 -0
  18. package/dist/server/proxy/address.d.ts.map +1 -0
  19. package/dist/server/proxy/header.d.ts +26 -0
  20. package/dist/server/proxy/header.d.ts.map +1 -0
  21. package/dist/server/proxy/reader.d.ts +15 -0
  22. package/dist/server/proxy/reader.d.ts.map +1 -0
  23. package/dist/server/proxy/tls.d.ts +50 -0
  24. package/dist/server/proxy/tls.d.ts.map +1 -0
  25. package/dist/server/proxy/trusted.d.ts +9 -0
  26. package/dist/server/proxy/trusted.d.ts.map +1 -0
  27. package/dist/server/raw-socket.d.ts +9 -0
  28. package/dist/server/raw-socket.d.ts.map +1 -0
  29. package/dist/server/received.d.ts +2 -0
  30. package/dist/server/received.d.ts.map +1 -1
  31. package/dist/server/server.d.ts +9 -1
  32. package/dist/server/server.d.ts.map +1 -1
  33. package/dist/server/settings.d.ts +5 -0
  34. package/dist/server/settings.d.ts.map +1 -1
  35. package/dist/server/tls-context.d.ts +46 -0
  36. package/dist/server/tls-context.d.ts.map +1 -0
  37. package/dist/server/transport.d.ts +2 -2
  38. package/dist/server/transport.d.ts.map +1 -1
  39. package/docs/README.md +2 -2
  40. package/docs/guide.md +417 -13
  41. package/docs/roadmap.md +19 -0
  42. package/docs/troubleshooting.md +368 -1
  43. package/package.json +1 -1
@@ -24,7 +24,17 @@ 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: setTls(): tls: { key, cert } cannot be used: …`](#smtperror-settls-tls--key-cert--cannot-be-used-)
36
+ - [`SmtpError: setTls(): the server was made without tls, so it has none to replace`](#smtperror-settls-the-server-was-made-without-tls-so-it-has-none-to-replace)
37
+ - [`SmtpError: listen(): stop() was called before the server bound its port`](#smtperror-listen-stop-was-called-before-the-server-bound-its-port)
28
38
 
29
39
  **Relaying and authentication**
30
40
 
@@ -85,6 +95,10 @@ A reply carries its enhanced status code (`5.7.1`, RFC 3463) only after
85
95
  - [`421 4.3.2 … Too many connections, try later`](#421-432--too-many-connections-try-later)
86
96
  - [`421 4.7.0 … Too many connections from your address, try later`](#421-470--too-many-connections-from-your-address-try-later)
87
97
  - [On implicit TLS, the connection closes before any greeting](#on-implicit-tls-the-connection-closes-before-any-greeting)
98
+ - [Connections through the proxy close at once, with no greeting](#connections-through-the-proxy-close-at-once-with-no-greeting)
99
+ - [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)
100
+ - [Every client shows the proxy's address](#every-client-shows-the-proxys-address)
101
+ - [Health checks from the proxy show its own address](#health-checks-from-the-proxy-show-its-own-address)
88
102
  - [`421 4.4.2 … Idle too long, closing`](#421-442--idle-too-long-closing)
89
103
  - [`554 … Talked before the greeting`](#554--talked-before-the-greeting)
90
104
 
@@ -409,6 +423,124 @@ createSmtpServer({
409
423
  });
410
424
  ```
411
425
 
426
+ ### `SmtpError: createSmtpServer(): proxyProtocol.trusted must list the addresses or CIDRs of the proxies, at least one`
427
+
428
+ **When**: `proxyProtocol` is set without `trusted`, with an empty array, or
429
+ with something that is not an array — a single string included.
430
+
431
+ **Why**: `trusted` names the only peers whose PROXY header is read. Empty,
432
+ it would trust nobody, which is no proxy at all; a server not behind a
433
+ proxy leaves `proxyProtocol` out.
434
+
435
+ **Fix**: list the proxy's own address, in an array:
436
+
437
+ ```ts
438
+ import { createSmtpServer } from '@bumail/smtp';
439
+
440
+ createSmtpServer({
441
+ hostname: 'mx.example.com',
442
+ localDomains: ['example.com'],
443
+ proxyProtocol: { trusted: ['10.0.0.5'] }, // not trusted: '10.0.0.5'
444
+ onData: async (message) => {
445
+ await new Response(message.content).bytes();
446
+ },
447
+ });
448
+ ```
449
+
450
+ ### `SmtpError: createSmtpServer(): proxyProtocol.trusted: "…" is neither an IP address nor a CIDR`
451
+
452
+ **When**: an entry of `trusted` is a host name (`traefik`), an address with
453
+ a port (`10.0.0.5:25`), a malformed address, an address with a zone
454
+ (`fe80::1%eth0`: a zone names an interface of this host, never a client),
455
+ or holds more than one `/`.
456
+ `…` is the entry.
457
+
458
+ **Why**: the server matches the TCP address a connection comes from; it
459
+ resolves no name, since a name could resolve to anything later.
460
+
461
+ **Fix**: give the proxy's IP address, or its network as a CIDR. In a
462
+ container network, give the proxy a fixed address and list that:
463
+
464
+ ```ts
465
+ import { createSmtpServer } from '@bumail/smtp';
466
+
467
+ createSmtpServer({
468
+ hostname: 'mx.example.com',
469
+ localDomains: ['example.com'],
470
+ proxyProtocol: { trusted: ['172.20.0.2', 'fd00:20::2'] },
471
+ onData: async (message) => {
472
+ await new Response(message.content).bytes();
473
+ },
474
+ });
475
+ ```
476
+
477
+ ### `SmtpError: createSmtpServer(): proxyProtocol.trusted: "…" has a prefix length out of range`
478
+
479
+ **When**: an entry's prefix is past 32 for IPv4 (`10.0.0.0/33`) or 128 for
480
+ IPv6, is empty or not a number (`10.0.0.0/`, `10.0.0.0/8a`), or is below
481
+ 96 for an IPv4-mapped address (`::ffff:10.0.0.0/8`).
482
+
483
+ **Why**: an IPv4-mapped entry is taken as its IPv4 address, and its
484
+ prefix counts over the whole IPv6 address, the 96 bits of `::ffff:`
485
+ first: `::ffff:10.0.0.0/104` is `10.0.0.0/8`.
486
+
487
+ **Fix**: write an IPv4 network as IPv4; it matches mapped peers too:
488
+
489
+ ```ts
490
+ import { createSmtpServer } from '@bumail/smtp';
491
+
492
+ createSmtpServer({
493
+ hostname: 'mx.example.com',
494
+ localDomains: ['example.com'],
495
+ proxyProtocol: { trusted: ['10.0.0.0/24'] }, // matches ::ffff:10.0.0.5 as well
496
+ onData: async (message) => {
497
+ await new Response(message.content).bytes();
498
+ },
499
+ });
500
+ ```
501
+
502
+ ### `SmtpError: createSmtpServer(): proxyProtocol.trusted: "…" has a prefix length of 0, which trusts every peer`
503
+
504
+ **When**: an entry has a prefix of 0: `0.0.0.0/0`, `::/0`, or
505
+ `::ffff:0.0.0.0/96`, which is the same. `…` is the entry.
506
+
507
+ **Why**: such an entry matches every peer, so any client could send a
508
+ PROXY header and claim any address: the option would defend nothing.
509
+
510
+ **Fix**: list the proxy's own address, or the smallest network it is in:
511
+
512
+ ```ts
513
+ proxyProtocol: { trusted: ['172.20.0.2'] }, // not '0.0.0.0/0'
514
+ ```
515
+
516
+ ### `SmtpError: createSmtpServer(): proxyProtocol.trusted: … is not a string`
517
+
518
+ **When**: an entry of `trusted` is not a string: a number, `null`,
519
+ `undefined`, an object. `…` is the value.
520
+
521
+ **Why**: each entry is an address or a CIDR, written as text.
522
+
523
+ **Fix**: write each entry as a string, and filter out what is not set when
524
+ the list comes from the environment:
525
+
526
+ ```ts
527
+ import { createSmtpServer } from '@bumail/smtp';
528
+
529
+ const trusted = (Bun.env['SMTP_TRUSTED_PROXIES'] ?? '')
530
+ .split(',')
531
+ .map((entry) => entry.trim())
532
+ .filter((entry) => entry !== '');
533
+
534
+ createSmtpServer({
535
+ hostname: 'mx.example.com',
536
+ localDomains: ['example.com'],
537
+ ...(trusted.length > 0 ? { proxyProtocol: { trusted } } : {}),
538
+ onData: async (message) => {
539
+ await new Response(message.content).bytes();
540
+ },
541
+ });
542
+ ```
543
+
412
544
  ### `SmtpError: listen(): the server is already listening on …`
413
545
 
414
546
  **When**: `listen` on a server that is already listening. Its `code` is
@@ -434,6 +566,111 @@ await createSmtpServer(options).listen({ port: 25 });
434
566
  await createSmtpServer(options).listen({ port: 2525 });
435
567
  ```
436
568
 
569
+ ### `SmtpError: listen(): the server is already starting to listen`
570
+
571
+ **When**: a second `listen` on the same server while the first has not
572
+ resolved yet. With `implicitTls`, `listen` reads the key and certificate
573
+ before it binds, so two calls not awaited in turn meet here. Its `code` is
574
+ `ALREADY_LISTENING`; the first call binds, this one binds nothing.
575
+
576
+ **Fix**: await `listen` once per server, and create one server per port:
577
+
578
+ ```ts
579
+ const server = createSmtpServer({ ...options, tls, implicitTls: true });
580
+ await server.listen({ port: 465 }); // not Promise.all([server.listen(…), server.listen(…)])
581
+ ```
582
+
583
+ ### `SmtpError: listen(): stop() was called before the server bound its port`
584
+
585
+ **When**: `stop()` ran before `listen` resolved: on an `implicitTls` server
586
+ while it reads the TLS key and certificate, on any other at once, since
587
+ `listen` resolves a tick after it binds. `listen` rejects with
588
+ `code: 'STOPPED'` and leaves nothing listening; a later `listen` binds as
589
+ usual.
590
+
591
+ **Why**: `stop()` means the server is not to serve; binding afterwards would
592
+ leave a listener nothing could stop.
593
+
594
+ **Fix**: nothing is wrong if the stop was meant. To start again, call
595
+ `listen` again:
596
+
597
+ ```ts
598
+ await server.listen({ port: 465 }).catch((error) => {
599
+ if (error.code !== 'STOPPED') throw error;
600
+ });
601
+ ```
602
+
603
+ ### `SmtpError: listen(): tls: { key, cert } cannot be used: …`
604
+
605
+ **When**: `listen` on an `implicitTls` server whose `tls.key` or
606
+ `tls.cert` cannot be read — a `Bun.file` that does not exist — or is not
607
+ a PEM key or certificate. The same error, word for word, with
608
+ `proxyProtocol` or without. Its `code` is `INVALID_OPTION`; the rest of
609
+ the message, and its `cause`, are the reason from the file system or
610
+ `node:tls` (`ENOENT: no such file or directory, open '…'`,
611
+ `…PEM routines…NO_START_LINE`). Nothing is bound.
612
+
613
+ **Why**: `listen` reads both and checks them as a TLS context before it
614
+ binds, so a bad key fails at start, not at the first client.
615
+
616
+ **Fix**: pass the PEM text, or a `Bun.file` of a path that exists:
617
+
618
+ ```ts
619
+ const tls = {
620
+ key: Bun.file('/etc/bumail/tls/privkey.pem'),
621
+ cert: Bun.file('/etc/bumail/tls/fullchain.pem'),
622
+ };
623
+ try {
624
+ await createSmtpServer({ ...options, tls, implicitTls: true }).listen({ port: 465 });
625
+ } catch (error) {
626
+ console.error(error, (error as Error).cause);
627
+ }
628
+ ```
629
+
630
+ ### `SmtpError: setTls(): tls: { key, cert } cannot be used: …`
631
+
632
+ **When**: `setTls` was given a renewed pair whose `key` or `cert` cannot
633
+ be read — a `Bun.file` that does not exist, or one caught half-written —
634
+ or is empty (`… the key is empty`, `… the cert is empty`), or is not a
635
+ PEM key or certificate, or a key that is not the certificate's. Its `code` is `INVALID_OPTION`; the rest of the message,
636
+ and its `cause`, are the reason from the file system or `node:tls`
637
+ (`ENOENT: no such file or directory, open '…'`, `…PEM routines…`,
638
+ `key values mismatch`). The pair in use stays, for new connections too.
639
+
640
+ **Why**: `setTls` reads both and checks them as a TLS context before it
641
+ swaps anything, so a bad pair never reaches a client.
642
+
643
+ **Fix**: write both files before calling it — a renewal that writes the key
644
+ first and the certificate after fails here, which is the right answer —
645
+ and retry:
646
+
647
+ ```ts
648
+ try {
649
+ await server.setTls({
650
+ key: Bun.file('/etc/bumail/tls/privkey.pem'),
651
+ cert: Bun.file('/etc/bumail/tls/fullchain.pem'),
652
+ });
653
+ } catch (error) {
654
+ console.error(error, (error as Error).cause); // the old pair is still served
655
+ }
656
+ ```
657
+
658
+ ### `SmtpError: setTls(): the server was made without tls, so it has none to replace`
659
+
660
+ **When**: `setTls` on a server created without the `tls` option. Its `code`
661
+ is `INVALID_OPTION`.
662
+
663
+ **Why**: TLS is what turns on STARTTLS, AUTH and implicit TLS, and each
664
+ changes what the server says: `setTls` replaces a pair, it does not turn
665
+ TLS on.
666
+
667
+ **Fix**: create the server with `tls`, the pair it starts with:
668
+
669
+ ```ts
670
+ const server = createSmtpServer({ ...options, tls: { key, cert } });
671
+ await server.setTls({ key: renewedKey, cert: renewedCert });
672
+ ```
673
+
437
674
  ## Relaying and authentication
438
675
 
439
676
  ### `554 5.7.1 Relay access denied`
@@ -1443,6 +1680,134 @@ const server = createSmtpServer({
1443
1680
  });
1444
1681
  ```
1445
1682
 
1683
+ ### Connections through the proxy close at once, with no greeting
1684
+
1685
+ **When**: a client connecting through a TCP proxy gets no `220`, and the
1686
+ connection closes without a reply — at once, or `handshakeTimeout` seconds
1687
+ (10 by default) after it opened. `onConnect` is not called, `onError` is
1688
+ told nothing, and `connections` does not count the socket.
1689
+
1690
+ **Why**: the server trusts the proxy (`proxyProtocol.trusted` lists it),
1691
+ so it waits for a PROXY header first, and resets a socket whose header is
1692
+ missing, late or not valid, writing nothing. Usually:
1693
+
1694
+ - the proxy is not configured to send the header at all, so the client's
1695
+ bytes, or nothing, come first;
1696
+ - it sends a version 1 line longer than 107 bytes, or a version 2 header
1697
+ with more than 2048 bytes of TLVs;
1698
+ - something other than the proxy connects from a listed address, a health
1699
+ checker that opens a bare TCP connection for instance.
1700
+
1701
+ On an `implicitTls` port, the reverse also closes at once: the proxy sends
1702
+ a header to a server that does not trust it, and the header is not a TLS
1703
+ ClientHello, so the handshake fails.
1704
+
1705
+ **Fix**: send the header from the proxy (with Traefik, a TCP
1706
+ `serversTransport` with `proxyProtocol: { version: 2 }`, named by the
1707
+ service), and list the proxy on the server. Both
1708
+ sides together, or neither:
1709
+
1710
+ ```ts
1711
+ import { createSmtpServer } from '@bumail/smtp';
1712
+
1713
+ createSmtpServer({
1714
+ hostname: 'mx.example.com',
1715
+ localDomains: ['example.com'],
1716
+ proxyProtocol: { trusted: ['10.0.0.5'] }, // the address the proxy connects from
1717
+ handshakeTimeout: 10, // the header must arrive within it
1718
+ onData: async (message) => {
1719
+ await new Response(message.content).bytes();
1720
+ },
1721
+ });
1722
+ ```
1723
+
1724
+ The [guide](guide.md#with-traefik) has a Traefik configuration.
1725
+
1726
+ ### Every client through the proxy is refused with `554 … Talked before the greeting`
1727
+
1728
+ **When**: on a clear port (25, 587), every connection through the proxy
1729
+ gets `554 … Talked before the greeting`, and the server sees every client
1730
+ with the proxy's address. When the header came after the greeting, the
1731
+ client gets replies to input it never sent instead: `500` to a version 1
1732
+ line starting `PROXY`, error replies to the binary of a version 2 header.
1733
+
1734
+ **Why**: the proxy sends a PROXY header, but the server does not trust it:
1735
+ `proxyProtocol` is not set, or `trusted` does not list the address the
1736
+ proxy connects from. To a peer not listed, the header is the client's own
1737
+ input, sent before the greeting, and it never sets an address.
1738
+
1739
+ **Fix**: list the proxy's address in `proxyProtocol.trusted`, as the
1740
+ address the server sees it connect from; see the next entry for how to
1741
+ find it.
1742
+
1743
+ ### Every client shows the proxy's address
1744
+
1745
+ **When**: `session.remoteAddress`, the Received field and
1746
+ `maxConnectionsPerClient` see one address, the proxy's, for every client:
1747
+ one more client past `maxConnectionsPerClient` is refused with `421 4.7.0
1748
+ … Too many connections from your address, try later`.
1749
+
1750
+ **Why**: the server is not reading a PROXY header from that peer. Either
1751
+ the proxy sends none — the server then serves its connections as any
1752
+ other's — or the server does not list it, and the header it sends is
1753
+ refused as bad input (the entry above). A common case is a proxy that
1754
+ connects from another address than the one listed: another container
1755
+ network, an IPv6 address, or a new address after a restart. An
1756
+ IPv4-mapped peer (`::ffff:172.20.0.2`) matches an IPv4 entry; it needs no
1757
+ entry of its own.
1758
+
1759
+ **Fix**: log the peer the server sees, then list exactly that address —
1760
+ not the range clients come from:
1761
+
1762
+ ```ts
1763
+ import { createSmtpServer } from '@bumail/smtp';
1764
+
1765
+ createSmtpServer({
1766
+ hostname: 'mx.example.com',
1767
+ localDomains: ['example.com'],
1768
+ // Without proxyProtocol, every connection through the proxy shows its address.
1769
+ onConnect: (session) => console.log('peer', session.remoteAddress),
1770
+ onData: async (message) => {
1771
+ await new Response(message.content).bytes();
1772
+ },
1773
+ });
1774
+ ```
1775
+
1776
+ Then configure the proxy to send the header, and set
1777
+ `proxyProtocol: { trusted: ['<that address>'] }`.
1778
+
1779
+ ### Health checks from the proxy show its own address
1780
+
1781
+ **When**: with `proxyProtocol` set and working, some connections still have
1782
+ the proxy's address in `session.remoteAddress`: they open and close
1783
+ without a transaction, at a steady pace.
1784
+
1785
+ **Why**: by design. The proxy's own connections — health checks — come
1786
+ with a version 2 `LOCAL` header, which names no client, so the server
1787
+ keeps the peer's address, the proxy's. So do a v1 `PROXY UNKNOWN` and a
1788
+ v2 header with an UNSPEC or UNIX address, or over UDP.
1789
+
1790
+ **Fix**: none needed. To keep them out of your logs or your policy, skip
1791
+ the proxy's address in `onConnect`:
1792
+
1793
+ ```ts
1794
+ import { createSmtpServer } from '@bumail/smtp';
1795
+
1796
+ const proxies = ['10.0.0.5'];
1797
+
1798
+ createSmtpServer({
1799
+ hostname: 'mx.example.com',
1800
+ localDomains: ['example.com'],
1801
+ proxyProtocol: { trusted: proxies },
1802
+ onConnect: (session) => {
1803
+ if (!proxies.includes(session.remoteAddress)) console.log('client', session.remoteAddress);
1804
+ },
1805
+ onData: async (message) => {
1806
+ await new Response(message.content).bytes();
1807
+ },
1808
+ });
1809
+ ```
1810
+
1446
1811
  ### `421 4.4.2 … Idle too long, closing`
1447
1812
 
1448
1813
  **When**: nothing came from the client for `timeout` seconds (300 by
@@ -1496,7 +1861,9 @@ client library does; a hand-written client or a test script may not.
1496
1861
 
1497
1862
  **Fix**, as the operator: if a legitimate sender is refused, lower
1498
1863
  `greetingDelay`, or leave it at `0`; a test client that writes at once
1499
- needs it at `0`.
1864
+ needs it at `0`. If every client through a TCP proxy gets it, the proxy
1865
+ sends a PROXY header the server does not trust: see
1866
+ [that entry](#every-client-through-the-proxy-is-refused-with-554--talked-before-the-greeting).
1500
1867
 
1501
1868
  ## Sending mail: options
1502
1869
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bumail/smtp",
3
- "version": "0.4.0",
3
+ "version": "0.6.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",