@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.
- package/README.md +64 -5
- package/dist/chunks/{index-753q702a.js → index-n4xwep5m.js} +3 -3
- package/dist/chunks/{index-753q702a.js.map → index-n4xwep5m.js.map} +3 -3
- package/dist/client/index.js +1 -1
- package/dist/errors.d.ts +3 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +617 -71
- package/dist/index.js.map +15 -8
- package/dist/server/admission.d.ts +37 -0
- package/dist/server/admission.d.ts.map +1 -0
- package/dist/server/listener.d.ts +17 -8
- package/dist/server/listener.d.ts.map +1 -1
- package/dist/server/options.d.ts +21 -1
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/proxy/address.d.ts +17 -0
- package/dist/server/proxy/address.d.ts.map +1 -0
- package/dist/server/proxy/header.d.ts +26 -0
- package/dist/server/proxy/header.d.ts.map +1 -0
- package/dist/server/proxy/reader.d.ts +15 -0
- package/dist/server/proxy/reader.d.ts.map +1 -0
- package/dist/server/proxy/tls.d.ts +50 -0
- package/dist/server/proxy/tls.d.ts.map +1 -0
- package/dist/server/proxy/trusted.d.ts +9 -0
- package/dist/server/proxy/trusted.d.ts.map +1 -0
- package/dist/server/raw-socket.d.ts +9 -0
- package/dist/server/raw-socket.d.ts.map +1 -0
- package/dist/server/received.d.ts +2 -0
- package/dist/server/received.d.ts.map +1 -1
- package/dist/server/server.d.ts +9 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/settings.d.ts +5 -0
- package/dist/server/settings.d.ts.map +1 -1
- package/dist/server/tls-context.d.ts +46 -0
- package/dist/server/tls-context.d.ts.map +1 -0
- package/dist/server/transport.d.ts +2 -2
- package/dist/server/transport.d.ts.map +1 -1
- package/docs/README.md +2 -2
- package/docs/guide.md +417 -13
- package/docs/roadmap.md +19 -0
- package/docs/troubleshooting.md +368 -1
- package/package.json +1 -1
package/docs/troubleshooting.md
CHANGED
|
@@ -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.
|
|
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",
|