@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.
- package/README.md +105 -7
- package/dist/chunks/{index-3423jxf9.js → index-n4xwep5m.js} +10 -4
- package/dist/chunks/index-n4xwep5m.js.map +14 -0
- 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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +724 -126
- package/dist/index.js.map +19 -9
- package/dist/protocol/command.d.ts +4 -2
- package/dist/protocol/command.d.ts.map +1 -1
- package/dist/protocol/path.d.ts +13 -2
- package/dist/protocol/path.d.ts.map +1 -1
- package/dist/server/admission.d.ts +29 -0
- package/dist/server/admission.d.ts.map +1 -0
- package/dist/server/client-key.d.ts +15 -0
- package/dist/server/client-key.d.ts.map +1 -0
- package/dist/server/listener.d.ts +46 -0
- package/dist/server/listener.d.ts.map +1 -0
- package/dist/server/options.d.ts +45 -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.map +1 -1
- package/dist/server/settings.d.ts +4 -0
- package/dist/server/settings.d.ts.map +1 -1
- package/dist/server/slots.d.ts +14 -0
- package/dist/server/slots.d.ts.map +1 -0
- package/dist/server/tls-context.d.ts +16 -0
- package/dist/server/tls-context.d.ts.map +1 -0
- package/dist/server/transaction.d.ts.map +1 -1
- 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 +472 -12
- package/docs/roadmap.md +57 -0
- package/docs/troubleshooting.md +406 -4
- package/package.json +2 -2
- package/dist/chunks/index-3423jxf9.js.map +0 -14
package/docs/guide.md
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
# Guide
|
|
2
2
|
|
|
3
3
|
How to run `@bumail/smtp` and what it does on the wire: the session and its
|
|
4
|
-
replies, the hooks, what a delivered message looks like, TLS,
|
|
5
|
-
with the client, and the RFCs behind each
|
|
4
|
+
replies, the hooks, what a delivered message looks like, TLS, running
|
|
5
|
+
behind a TCP proxy, sending mail with the client, and the RFCs behind each
|
|
6
|
+
behaviour.
|
|
6
7
|
|
|
7
8
|
- [The smallest server](#the-smallest-server)
|
|
8
9
|
- [Options](#options)
|
|
9
10
|
- [The session, and what each reply means](#the-session-and-what-each-reply-means)
|
|
10
11
|
- [Hooks, and their order](#hooks-and-their-order)
|
|
12
|
+
- [`RCPT TO:<postmaster>`](#rcpt-topostmaster)
|
|
11
13
|
- [Session.data](#sessiondata)
|
|
12
14
|
- [What onData receives](#what-ondata-receives)
|
|
13
15
|
- [Delivering into @bumail/store](#delivering-into-bumailstore)
|
|
14
16
|
- [Authentication](#authentication)
|
|
15
17
|
- [TLS](#tls)
|
|
16
18
|
- [Limits](#limits)
|
|
19
|
+
- [Running behind a TCP proxy](#running-behind-a-tcp-proxy)
|
|
17
20
|
- [Sending mail: the client](#sending-mail-the-client)
|
|
18
21
|
- [Protocol helpers](#protocol-helpers)
|
|
19
22
|
- [The RFCs implemented](#the-rfcs-implemented)
|
|
@@ -67,6 +70,9 @@ export interface SmtpServer {
|
|
|
67
70
|
| `maxMessageSize` | `number` | 25 MiB | bytes; announced with SIZE |
|
|
68
71
|
| `maxRecipients` | `number` | `100` | recipients per message |
|
|
69
72
|
| `maxConnections` | `number` | `1000` | open connections at once |
|
|
73
|
+
| `maxConnectionsPerClient` | `number` | `10` | open connections at once from one client: an IPv4 address, or an IPv6 /64; see [Limits](#limits) |
|
|
74
|
+
| `handshakeTimeout` | `number` | `10` | seconds a client on implicit TLS has to complete its handshake, from the TCP connection on; past it, the socket is closed. See [TLS](#tls). With `proxyProtocol`, also the seconds a trusted proxy has to send its PROXY header; see [Running behind a TCP proxy](#running-behind-a-tcp-proxy) |
|
|
75
|
+
| `proxyProtocol` | `{ trusted: string[] }` | none, off | reads the PROXY protocol, v1 and v2, from the peers `trusted` lists (IPv4 and IPv6 addresses and CIDRs), so the client's address replaces the proxy's; see [Running behind a TCP proxy](#running-behind-a-tcp-proxy) |
|
|
70
76
|
| `maxErrors` | `number` | `10` | failed commands before the server hangs up |
|
|
71
77
|
| `timeout` | `number` | `300` | seconds since the client's last byte, or since the 220, before the server hangs up |
|
|
72
78
|
| `hookTimeout` | `number` | `60` | seconds a hook, `authenticate` or `localDomains` has to settle, and `onData` to read on; past it, `451 4.3.0`. At most 2 147 483, what a timer can wait |
|
|
@@ -112,9 +118,16 @@ await server.listen({ port: 25 });
|
|
|
112
118
|
| `createSmtpServer(): hookTimeout must be at most 2147483 seconds, not <value>` | `hookTimeout` past what `setTimeout` can wait (about 24.8 days) |
|
|
113
119
|
| `createSmtpServer(): greetingDelay must be a number of seconds, 0 or more, not <value>` | `greetingDelay` negative, `NaN` or `Infinity` |
|
|
114
120
|
| `createSmtpServer(): greetingDelay (<n> s) must be shorter than timeout (<n> s), or every client times out before the greeting` | `greetingDelay` as long as `timeout`, or longer |
|
|
121
|
+
| `createSmtpServer(): proxyProtocol.trusted must list the addresses or CIDRs of the proxies, at least one` | `proxyProtocol` without `trusted`, with an empty one, or one that is not an array |
|
|
122
|
+
| `createSmtpServer(): proxyProtocol.trusted: "<entry>" is neither an IP address nor a CIDR` | an entry that is a host name, a malformed address, an address with a zone (`fe80::1%eth0`), or has more than one `/` |
|
|
123
|
+
| `createSmtpServer(): proxyProtocol.trusted: "<entry>" has a prefix length out of range` | a prefix past 32 for IPv4 or 128 for IPv6, not a number, or below 96 for an IPv4-mapped address (`::ffff:10.0.0.0/8`) |
|
|
124
|
+
| `createSmtpServer(): proxyProtocol.trusted: "<entry>" has a prefix length of 0, which trusts every peer` | `0.0.0.0/0`, `::/0`, `::ffff:0.0.0.0/96`: the option would trust anyone |
|
|
125
|
+
| `createSmtpServer(): proxyProtocol.trusted: <value> is not a string` | an entry that is a number, `null`, an object |
|
|
115
126
|
|
|
116
127
|
`listen` a second time throws an `SmtpError` with `code:
|
|
117
|
-
'ALREADY_LISTENING'`: create another server for another port.
|
|
128
|
+
'ALREADY_LISTENING'`: create another server for another port. A `stop()`
|
|
129
|
+
called before `listen` resolved makes that `listen` reject with
|
|
130
|
+
`code: 'STOPPED'`, and nothing is left listening.
|
|
118
131
|
|
|
119
132
|
What to do about each is in [Troubleshooting](troubleshooting.md).
|
|
120
133
|
|
|
@@ -166,7 +179,7 @@ Every reply the server sends of its own:
|
|
|
166
179
|
| `250 <hostname> greets <name>` | EHLO or HELO accepted; either one also ends a transaction in progress |
|
|
167
180
|
| `501 Syntax: EHLO hostname` / `501 Syntax: HELO hostname` | the EHLO or HELO argument is missing, or is not a domain or an address literal; no enhanced code before an EHLO was accepted, `501 5.5.4 Syntax: EHLO hostname` after one |
|
|
168
181
|
| `250 2.1.0 OK` | MAIL FROM accepted |
|
|
169
|
-
| `250 2.1.5 OK` | RCPT TO accepted |
|
|
182
|
+
| `250 2.1.5 OK` | RCPT TO accepted; `RCPT TO:<postmaster>` with no domain is accepted without AUTH (see [below](#rcpt-topostmaster)) |
|
|
170
183
|
| `354 End data with <CR><LF>.<CR><LF>` | DATA: send the message |
|
|
171
184
|
| `250 2.0.0 OK queued as <id>` | the message was taken: `onData` read its content stream to the clean end and resolved without refusing |
|
|
172
185
|
| `250 2.0.0 OK` | RSET (the transaction is forgotten) or NOOP |
|
|
@@ -193,6 +206,7 @@ Every reply the server sends of its own:
|
|
|
193
206
|
| `500 5.5.2 Command unrecognized` | an unknown verb — EXPN, BDAT and the rest |
|
|
194
207
|
| `500 5.5.6 Line too long` | a command line over 2048 bytes; the rest of it is skipped |
|
|
195
208
|
| `421 4.3.2 <hostname> Too many connections, try later` | a connection past `maxConnections`, then the server hangs up |
|
|
209
|
+
| `421 4.7.0 <hostname> Too many connections from your address, try later` | a connection past `maxConnectionsPerClient` from one client, then the server hangs up |
|
|
196
210
|
| `421 4.4.2 <hostname> Idle too long, closing` | nothing for `timeout` seconds |
|
|
197
211
|
| `421 4.7.0 <hostname> Too many errors, closing` | `maxErrors` failed commands |
|
|
198
212
|
| `421 4.3.0 Local error, closing` | the server failed itself |
|
|
@@ -234,7 +248,7 @@ In the order a session meets them:
|
|
|
234
248
|
|
|
235
249
|
| Hook | Runs | After the server checked | A refusal |
|
|
236
250
|
| --- | --- | --- | --- |
|
|
237
|
-
| `onConnect` | when a client connects, before the greeting | the connection
|
|
251
|
+
| `onConnect` | when a client connects, before the greeting | the connection counts: `maxConnections`, then `maxConnectionsPerClient` | replaces the greeting, at once even with `greetingDelay`; then the server hangs up. When `onConnect` accepts, a client that talked before the greeting is refused with `554` |
|
|
238
252
|
| `authenticate` | at AUTH | TLS, EHLO, no transaction in progress | see [Authentication](#authentication) |
|
|
239
253
|
| `onMailFrom` | at each MAIL FROM | EHLO, AUTH in `submission`, syntax, parameters, SIZE, SMTPUTF8 | refuses the sender; the client may try another MAIL |
|
|
240
254
|
| `onRcptTo` | at each RCPT TO | MAIL, syntax, SMTPUTF8, `maxRecipients`, **relaying** | refuses that recipient; the others stand |
|
|
@@ -285,12 +299,14 @@ await server.listen({ port: 25 });
|
|
|
285
299
|
|
|
286
300
|
```ts
|
|
287
301
|
export interface Path {
|
|
288
|
-
/** `local@domain`; `''` for the null reverse-path `<>`: a bounce
|
|
302
|
+
/** `local@domain`; `''` for the null reverse-path `<>`: a bounce; `'postmaster'` for `RCPT TO:<postmaster>`. */
|
|
289
303
|
readonly address: string;
|
|
290
304
|
/** As sent: case is kept. */
|
|
291
305
|
readonly local: string;
|
|
292
|
-
/** Lower case
|
|
306
|
+
/** Lower case; `''` for `<>` and `<postmaster>`. */
|
|
293
307
|
readonly domain: string;
|
|
308
|
+
/** `true` for `RCPT TO:<postmaster>` with no domain; absent otherwise. */
|
|
309
|
+
readonly postmaster?: true;
|
|
294
310
|
}
|
|
295
311
|
```
|
|
296
312
|
|
|
@@ -309,6 +325,49 @@ surrogates are refused anywhere in a path — the route, the local part
|
|
|
309
325
|
quoted or not, the domain — even under SMTPUTF8. An IPv4 address literal
|
|
310
326
|
takes octets up to 255: `[999.1.1.1]` is not one.
|
|
311
327
|
|
|
328
|
+
## `RCPT TO:<postmaster>`
|
|
329
|
+
|
|
330
|
+
RFC 5321 §4.5.1 says every server that delivers mail must take mail for
|
|
331
|
+
`postmaster`, and §4.1.1.3 lets RCPT TO name it with no domain at all:
|
|
332
|
+
`RCPT TO:<Postmaster>`. The server takes it, in any case, as the
|
|
333
|
+
postmaster of this server, whatever domains it hosts:
|
|
334
|
+
|
|
335
|
+
- it is not relaying, so it needs no AUTH, and `localDomains` is not asked;
|
|
336
|
+
- `onRcptTo` is asked, as for any recipient, with the `Path`
|
|
337
|
+
`{ address: 'postmaster', local: 'postmaster', domain: '', postmaster: true }`,
|
|
338
|
+
so it can still refuse;
|
|
339
|
+
- `envelope.to` lists it as `'postmaster'`, with no `@`, and the Received
|
|
340
|
+
field says `for <postmaster>` when it is the only recipient.
|
|
341
|
+
|
|
342
|
+
`<postmaster@example.com>` is an ordinary path: it is checked against
|
|
343
|
+
`localDomains` like any other, and has no `postmaster` field. A path the
|
|
344
|
+
grammar would otherwise refuse stays refused: `<postmaster@>`,
|
|
345
|
+
`<"postmaster">` and `MAIL FROM:<postmaster>` get `501 5.5.4`.
|
|
346
|
+
|
|
347
|
+
Route it to a mailbox you keep: by `path.postmaster` in `onRcptTo`, by the
|
|
348
|
+
bare `'postmaster'` in `onData`.
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { createSmtpServer, reply } from '@bumail/smtp';
|
|
352
|
+
|
|
353
|
+
const mailboxes = new Set(['alice@example.com', 'postmaster@example.com']);
|
|
354
|
+
const postmaster = 'postmaster@example.com';
|
|
355
|
+
|
|
356
|
+
const server = createSmtpServer({
|
|
357
|
+
hostname: 'mx.example.com',
|
|
358
|
+
localDomains: ['example.com'],
|
|
359
|
+
onRcptTo: (path) =>
|
|
360
|
+
path.postmaster || mailboxes.has(path.address.toLowerCase())
|
|
361
|
+
? undefined
|
|
362
|
+
: reply(550, '5.1.1', 'No such user here'),
|
|
363
|
+
async onData(message) {
|
|
364
|
+
const recipients = message.envelope.to.map((to) => (to === 'postmaster' ? postmaster : to));
|
|
365
|
+
const bytes = await new Response(message.content).bytes();
|
|
366
|
+
for (const to of recipients) await Bun.write(`spool/${to}/${message.id}.eml`, bytes);
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
312
371
|
## Session.data
|
|
313
372
|
|
|
314
373
|
Every hook, and `authenticate`, receives the `Session`:
|
|
@@ -396,6 +455,7 @@ export interface ReceivedMessage {
|
|
|
396
455
|
export interface Envelope {
|
|
397
456
|
/** `''` for the null reverse-path `<>`: a bounce. */
|
|
398
457
|
readonly from: string;
|
|
458
|
+
/** `local@domain` each; `'postmaster'`, with no domain, for `RCPT TO:<postmaster>`. */
|
|
399
459
|
readonly to: readonly string[];
|
|
400
460
|
/** The client asked for SMTPUTF8 (RFC 6531). */
|
|
401
461
|
readonly smtputf8: boolean;
|
|
@@ -406,8 +466,9 @@ export interface Envelope {
|
|
|
406
466
|
|
|
407
467
|
The **envelope** is what MAIL FROM and RCPT TO said — not the `From:` and
|
|
408
468
|
`To:` of the message, which may differ (Bcc, mailing lists, forwarding).
|
|
409
|
-
`to` holds only the recipients the server and `onRcptTo` accepted
|
|
410
|
-
|
|
469
|
+
`to` holds only the recipients the server and `onRcptTo` accepted, each
|
|
470
|
+
`local@domain`, or `'postmaster'` for [`RCPT TO:<postmaster>`](#rcpt-topostmaster).
|
|
471
|
+
Deliver to the envelope.
|
|
411
472
|
|
|
412
473
|
The **content** is the message as the client sent it, with the leading dot
|
|
413
474
|
of each stuffed line removed (RFC 5321 §4.5.2), the terminating `.` line
|
|
@@ -423,7 +484,8 @@ Subject: hi
|
|
|
423
484
|
body
|
|
424
485
|
```
|
|
425
486
|
|
|
426
|
-
- `from` is the EHLO or HELO name, then the client's address in brackets
|
|
487
|
+
- `from` is the EHLO or HELO name, then the client's address in brackets:
|
|
488
|
+
an IPv6 address is the literal of RFC 5321 §4.1.3, `([IPv6:2001:db8::7])`.
|
|
427
489
|
Any character of the name that is not printable ASCII is written `?`.
|
|
428
490
|
- `with` is the protocol of RFC 3848: `SMTP` after HELO; `ESMTP` after EHLO,
|
|
429
491
|
`ESMTPS` with TLS, `ESMTPA` with AUTH, `ESMTPSA` with both.
|
|
@@ -767,6 +829,26 @@ STARTTLS, in clear, are dropped and never answered. The STARTTLS replies:
|
|
|
767
829
|
with `tls`, usually on 465. STARTTLS is not offered there — the connection
|
|
768
830
|
is already secure — and AUTH is offered at once.
|
|
769
831
|
|
|
832
|
+
On implicit TLS the server counts a socket from the TCP connection on, not
|
|
833
|
+
from the end of its handshake: a socket that has not finished its handshake
|
|
834
|
+
holds a slot of `maxConnections` and of `maxConnectionsPerClient`, and is
|
|
835
|
+
closed, without a reply, `handshakeTimeout` seconds (default 10) after it
|
|
836
|
+
connected, so clients that open TCP connections and never send a
|
|
837
|
+
ClientHello cannot fill the server. A refusal by a limit, `onConnect`, the
|
|
838
|
+
greeting and `greetingDelay` all wait for the handshake, so the client
|
|
839
|
+
reads them over TLS and `session.secure` is already `true`. A handshake
|
|
840
|
+
that fails is closed by Bun at once and counted out. A STARTTLS handshake
|
|
841
|
+
is bounded by the idle `timeout`, the connection already counted.
|
|
842
|
+
|
|
843
|
+
Bun 1.4.2 calls a TLS listener's `open` only once the handshake completed
|
|
844
|
+
unless the listener has a `handshake` handler; with one, `open` comes at
|
|
845
|
+
the TCP connection and the socket's timer runs during the handshake. The
|
|
846
|
+
server sets one for that reason.
|
|
847
|
+
|
|
848
|
+
Behind a TCP proxy that sends the PROXY protocol, implicit TLS starts after
|
|
849
|
+
the proxy's header, and runs differently: see
|
|
850
|
+
[Running behind a TCP proxy](#running-behind-a-tcp-proxy).
|
|
851
|
+
|
|
770
852
|
An MX takes mail in clear as well as encrypted: senders on the Internet that
|
|
771
853
|
cannot do TLS still deliver. Whether to refuse them is policy — check
|
|
772
854
|
`session.secure` in a hook:
|
|
@@ -837,6 +919,8 @@ await createSmtpServer({ ...submission, implicitTls: true }).listen({ port: 465
|
|
|
837
919
|
| `maxMessageSize` | 25 MiB | `552 5.3.4` at MAIL when `SIZE=` says more; during DATA once the message is more — the content stream errors with `MESSAGE_TOO_BIG`, the rest is read and dropped, and the session goes on |
|
|
838
920
|
| `maxRecipients` | 100, the least RFC 5321 §4.5.3.1.8 asks a server to take | `452 4.5.3` for each extra recipient (RFC 5321 §4.5.3.1.10); the client sends the rest in another transaction |
|
|
839
921
|
| `maxConnections` | 1000 | `421 4.3.2` and the server hangs up, before the greeting and before `onConnect` |
|
|
922
|
+
| `maxConnectionsPerClient` | 10 | `421 4.7.0 <hostname> Too many connections from your address, try later` and the server hangs up, before the greeting and before `onConnect` |
|
|
923
|
+
| `handshakeTimeout` | 10 seconds, on implicit TLS; ticks of about 4 s, as `timeout`. For a trusted proxy's PROXY header, to the millisecond | the socket is closed without a reply: there is no TLS to write one on, and a proxy's socket has no client yet |
|
|
840
924
|
| `maxErrors` | 10 | `421 4.7.0` and the server hangs up |
|
|
841
925
|
| `timeout` | 300 seconds, RFC 5321 §4.5.3.2.7's, counted from the client's last byte, or from the 220; Bun's socket timer ticks in steps of about 4 s, so the hang-up comes up to that much later | `421 4.4.2` and the server hangs up |
|
|
842
926
|
| `hookTimeout` | 60 seconds | `451 4.3.0` for that command; `onError` gets an `SmtpError` `HOOK_TIMEOUT` |
|
|
@@ -869,6 +953,50 @@ const server = createSmtpServer({
|
|
|
869
953
|
await server.listen({ port: 25 });
|
|
870
954
|
```
|
|
871
955
|
|
|
956
|
+
`maxConnectionsPerClient` keeps one host from taking every slot of
|
|
957
|
+
`maxConnections`. A client is counted by `clientKey(session.remoteAddress)`:
|
|
958
|
+
|
|
959
|
+
| Remote address | Counted as |
|
|
960
|
+
| --- | --- |
|
|
961
|
+
| `192.0.2.1` | `192.0.2.1` |
|
|
962
|
+
| `::ffff:192.0.2.1`, `::ffff:c000:201` (IPv4-mapped, as a listener on `::` sees an IPv4 client) | `192.0.2.1` |
|
|
963
|
+
| `64:ff9b::c000:201` (NAT64) | `192.0.2.1` |
|
|
964
|
+
| `2001:db8:1:2::9`, `2001:db8:1:2:ffff::1` | `2001:db8:1:2::/64` |
|
|
965
|
+
| `2001:db8::1`, `2001:0db8:0000::ffff` | `2001:db8:0:0::/64` |
|
|
966
|
+
| anything else (a Unix socket) | not limited |
|
|
967
|
+
|
|
968
|
+
An IPv6 key is always the first four groups in lower-case hex, without
|
|
969
|
+
leading zeros, every group written out — zeros too — then `::/64`:
|
|
970
|
+
`2001:db8::1` is `2001:db8:0:0::/64`, never `2001:db8::/64`. Write the
|
|
971
|
+
keys you compare with it that way.
|
|
972
|
+
|
|
973
|
+
An IPv6 host usually holds a whole /64, so counting each address apart
|
|
974
|
+
would let it open as many connections as it likes. Every close frees the
|
|
975
|
+
slot as it frees one of `maxConnections`: QUIT, the client hanging up, a
|
|
976
|
+
socket error, the idle `timeout`, `maxErrors`, a refusal from `onConnect`,
|
|
977
|
+
and `stop(true)`. The count lives in the server's memory, for that one
|
|
978
|
+
server. For a policy of your own — a list, a per-network budget — use
|
|
979
|
+
`onConnect` with `clientKey`:
|
|
980
|
+
|
|
981
|
+
```ts
|
|
982
|
+
import { clientKey, createSmtpServer, reply } from '@bumail/smtp';
|
|
983
|
+
|
|
984
|
+
// As clientKey writes them: '2001:db8:0:0::/64', not '2001:db8::/64'.
|
|
985
|
+
const blocked = new Set(['203.0.113.7', '2001:db8:0:0::/64', '2001:db8:dead:beef::/64']);
|
|
986
|
+
|
|
987
|
+
const server = createSmtpServer({
|
|
988
|
+
hostname: 'mx.example.com',
|
|
989
|
+
localDomains: ['example.com'],
|
|
990
|
+
maxConnections: 500,
|
|
991
|
+
maxConnectionsPerClient: 5,
|
|
992
|
+
onConnect: (session) =>
|
|
993
|
+
blocked.has(clientKey(session.remoteAddress) ?? '') ? reply(554, '5.7.1', 'Go away') : undefined,
|
|
994
|
+
onData: async (message) => {
|
|
995
|
+
await new Response(message.content).bytes();
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
```
|
|
999
|
+
|
|
872
1000
|
Some limits are fixed: a command line is at most 2048 bytes (RFC 5321
|
|
873
1001
|
§4.5.3.1.4 asks for 512 at least); a session gets three AUTH attempts; and
|
|
874
1002
|
the server holds at most 64 KiB of a client's unread input, and of a
|
|
@@ -910,8 +1038,11 @@ TLS buffer. So a `221` behind replies that are never read holds the slot
|
|
|
910
1038
|
5 seconds, not until the `timeout`.
|
|
911
1039
|
|
|
912
1040
|
Stop a server with `stop()`; `stop(true)` also hangs up on every client,
|
|
913
|
-
those moved to TLS by STARTTLS included, as the idle timeout does
|
|
914
|
-
|
|
1041
|
+
those moved to TLS by STARTTLS included, as the idle timeout does; a
|
|
1042
|
+
socket still in its implicit TLS handshake is reset, with no 421, having
|
|
1043
|
+
no TLS to write one on, and so is a socket still awaiting a proxy's PROXY
|
|
1044
|
+
header, nothing written and nothing passed to `onError`. `connections`
|
|
1045
|
+
counts the clients currently connected:
|
|
915
1046
|
|
|
916
1047
|
```ts
|
|
917
1048
|
import { createSmtpServer } from '@bumail/smtp';
|
|
@@ -931,6 +1062,326 @@ process.on('SIGTERM', () => {
|
|
|
931
1062
|
});
|
|
932
1063
|
```
|
|
933
1064
|
|
|
1065
|
+
## Running behind a TCP proxy
|
|
1066
|
+
|
|
1067
|
+
`proxyProtocol` lets the server sit behind a TCP proxy — a Traefik TCP
|
|
1068
|
+
router, HAProxy, a cloud load balancer — and still see each client's own
|
|
1069
|
+
address, as the proxy reports it with the PROXY protocol (HAProxy's
|
|
1070
|
+
`proxy-protocol.txt`, versions 1 and 2):
|
|
1071
|
+
|
|
1072
|
+
```ts
|
|
1073
|
+
import { createSmtpServer } from '@bumail/smtp';
|
|
1074
|
+
|
|
1075
|
+
const server = createSmtpServer({
|
|
1076
|
+
hostname: 'mx.example.com',
|
|
1077
|
+
localDomains: ['example.com'],
|
|
1078
|
+
proxyProtocol: { trusted: ['10.0.0.5'] },
|
|
1079
|
+
onConnect: (session) => console.log('client', session.remoteAddress), // the client's, not 10.0.0.5
|
|
1080
|
+
async onData(message) {
|
|
1081
|
+
await Bun.write(`spool/${message.id}.eml`, await new Response(message.content).bytes());
|
|
1082
|
+
},
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
await server.listen({ port: 25 });
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
```ts
|
|
1089
|
+
export interface ProxyProtocolOptions {
|
|
1090
|
+
/** The proxies' addresses or CIDRs, IPv4 or IPv6: `['10.0.0.5', '172.16.0.0/12']`. */
|
|
1091
|
+
readonly trusted: readonly string[];
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
export interface SmtpServerOptions {
|
|
1095
|
+
// …
|
|
1096
|
+
readonly proxyProtocol?: ProxyProtocolOptions; // off by default
|
|
1097
|
+
readonly handshakeTimeout?: number; // also bounds a trusted proxy's header; default 10
|
|
1098
|
+
}
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
- [Who is trusted](#who-is-trusted)
|
|
1102
|
+
- [The header, and `handshakeTimeout`](#the-header-and-handshaketimeout)
|
|
1103
|
+
- [Which address the client gets](#which-address-the-client-gets)
|
|
1104
|
+
- [STARTTLS on 25 and 587](#starttls-on-25-and-587)
|
|
1105
|
+
- [Implicit TLS on 465, behind a proxy](#implicit-tls-on-465-behind-a-proxy)
|
|
1106
|
+
- [With Traefik](#with-traefik)
|
|
1107
|
+
|
|
1108
|
+
### Who is trusted
|
|
1109
|
+
|
|
1110
|
+
`trusted` lists the peers whose header is read: IPv4 and IPv6 addresses,
|
|
1111
|
+
and CIDRs of either. A peer is the TCP address the connection comes from,
|
|
1112
|
+
the proxy's.
|
|
1113
|
+
|
|
1114
|
+
| Entry | Trusts |
|
|
1115
|
+
| --- | --- |
|
|
1116
|
+
| `'10.0.0.5'`, `'2001:db8::5'` | that address alone |
|
|
1117
|
+
| `'172.20.0.0/16'`, `'2001:db8:1::/48'` | every address in the network |
|
|
1118
|
+
| `'::ffff:10.0.0.5'` | `10.0.0.5`: an IPv4-mapped entry is its IPv4 address |
|
|
1119
|
+
| `'::ffff:10.0.0.0/104'` | `10.0.0.0/8`: a mapped CIDR counts its prefix over the whole IPv6 address, so 96 and up |
|
|
1120
|
+
|
|
1121
|
+
A peer reported IPv4-mapped (`::ffff:10.0.0.5`), as a listener on `::`
|
|
1122
|
+
sees an IPv4 client, matches as its IPv4 address, so `'10.0.0.5'` covers
|
|
1123
|
+
both. An address with a zone (`fe80::1%eth0`) is refused, as an entry and as a
|
|
1124
|
+
peer: a zone names an interface of this host, not a client. A prefix of 0
|
|
1125
|
+
(`0.0.0.0/0`, `::/0`) is refused too, since it trusts every peer. A bad entry throws an
|
|
1126
|
+
`SmtpError` (`INVALID_OPTION`) from `createSmtpServer`; the messages are in
|
|
1127
|
+
[Options](#options).
|
|
1128
|
+
|
|
1129
|
+
**List the proxies' own addresses, and nothing else.** A trusted peer can
|
|
1130
|
+
claim any client address it likes, and every check that uses the address
|
|
1131
|
+
— `maxConnectionsPerClient`, your `onConnect` policy, the Received field —
|
|
1132
|
+
believes it. Never list a range clients connect from, and never
|
|
1133
|
+
`0.0.0.0/0` or `::/0` on a port the Internet reaches. A container network
|
|
1134
|
+
is safe to list only when nothing but the proxy reaches the server
|
|
1135
|
+
through it.
|
|
1136
|
+
|
|
1137
|
+
A peer not listed is served exactly as without the option. A PROXY header
|
|
1138
|
+
it sends is just bad input and never sets an address: after the greeting,
|
|
1139
|
+
`PROXY …` is a command the server does not know (`500`); before it, the
|
|
1140
|
+
client talked first (`554 <hostname> Talked before the greeting`). On
|
|
1141
|
+
implicit TLS the header is not a ClientHello: the handshake fails and the
|
|
1142
|
+
socket is closed.
|
|
1143
|
+
|
|
1144
|
+
### The header, and `handshakeTimeout`
|
|
1145
|
+
|
|
1146
|
+
A trusted peer must send a valid header first, before any byte of the
|
|
1147
|
+
client's, and all of it within `handshakeTimeout` seconds (default 10) of
|
|
1148
|
+
the TCP connection. That bound is a timer of its own, to the millisecond,
|
|
1149
|
+
not Bun's socket timer with its 4-second ticks; a header trickled a byte at
|
|
1150
|
+
a time does not stretch it. Past it, or on a header that is not valid, the
|
|
1151
|
+
socket is reset:
|
|
1152
|
+
|
|
1153
|
+
- nothing is written to it, not even a `421`;
|
|
1154
|
+
- `onConnect` is never called, and `onError` is told nothing;
|
|
1155
|
+
- the proxy's address is never taken for a client's.
|
|
1156
|
+
|
|
1157
|
+
While it waits for its header, a socket holds no slot of `maxConnections`
|
|
1158
|
+
nor of `maxConnectionsPerClient`: it is counted, by the client's address,
|
|
1159
|
+
once the header named it. `stop(true)` resets the sockets still awaiting
|
|
1160
|
+
their header, writing nothing and calling no `onError`.
|
|
1161
|
+
|
|
1162
|
+
What is read, and what is refused:
|
|
1163
|
+
|
|
1164
|
+
| Header | Refused when |
|
|
1165
|
+
| --- | --- |
|
|
1166
|
+
| v1, `PROXY TCP4 …`, `PROXY TCP6 …`, `PROXY UNKNOWN …` | the line, CRLF included, is longer than 107 bytes; it does not end in CRLF; it holds a byte that is not printable ASCII; the protocol is none of the three; an address or a port is malformed, or of the other family than `TCP4` or `TCP6` says |
|
|
1167
|
+
| v2, the 12-byte signature, then version 2 | the TLVs (the extensions after the addresses) announce more than 2048 bytes — refused from the first 16 bytes, before any of it is waited for; a TLV runs past the end of the block; the command, family or transport is unknown; the address block is shorter than its family needs |
|
|
1168
|
+
|
|
1169
|
+
TLVs are checked for their shape, then skipped: the server reads none,
|
|
1170
|
+
SSL and authority included.
|
|
1171
|
+
|
|
1172
|
+
What the client sent in the same segment as the header, behind it, is the
|
|
1173
|
+
client's own. On a clear port that is bytes sent before the greeting, so
|
|
1174
|
+
the client is refused with `554 <hostname> Talked before the greeting`, as
|
|
1175
|
+
it would be without a proxy.
|
|
1176
|
+
|
|
1177
|
+
### Which address the client gets
|
|
1178
|
+
|
|
1179
|
+
| The proxy sends | `session.remoteAddress` is |
|
|
1180
|
+
| --- | --- |
|
|
1181
|
+
| v1 `PROXY TCP4` or `PROXY TCP6` | the source address |
|
|
1182
|
+
| v2 `PROXY`, over TCP (STREAM), IPv4 or IPv6 | the source address |
|
|
1183
|
+
| v2 `LOCAL`: the proxy's own connection, a health check | the peer's, the proxy's |
|
|
1184
|
+
| v2 `PROXY` with an UNSPEC or UNIX address, or over UDP (DGRAM) | the peer's |
|
|
1185
|
+
| v1 `PROXY UNKNOWN` | the peer's |
|
|
1186
|
+
|
|
1187
|
+
An IPv6 source is written in RFC 5952's form — lower case, no leading
|
|
1188
|
+
zeros, the longest run of zeros as `::` — and an IPv4-mapped source as its
|
|
1189
|
+
plain IPv4 address: `::FFFF:C633:6407` reaches your code as
|
|
1190
|
+
`198.51.100.7`.
|
|
1191
|
+
|
|
1192
|
+
That address is the client's everywhere the server uses one:
|
|
1193
|
+
`session.remoteAddress` in `onConnect`, in every hook, in `authenticate`
|
|
1194
|
+
and in `onError`; the Received field (`from client.example
|
|
1195
|
+
([198.51.100.7])`); and `maxConnectionsPerClient`, which counts it by
|
|
1196
|
+
`clientKey` as any other. So a policy written for a server without a proxy
|
|
1197
|
+
works unchanged behind one:
|
|
1198
|
+
|
|
1199
|
+
```ts
|
|
1200
|
+
import { clientKey, createSmtpServer, reply } from '@bumail/smtp';
|
|
1201
|
+
|
|
1202
|
+
const blocked = new Set(['203.0.113.7', '2001:db8:0:0::/64']);
|
|
1203
|
+
|
|
1204
|
+
const server = createSmtpServer({
|
|
1205
|
+
hostname: 'mx.example.com',
|
|
1206
|
+
localDomains: ['example.com'],
|
|
1207
|
+
proxyProtocol: { trusted: ['10.0.0.5', 'fd00::5'] },
|
|
1208
|
+
maxConnectionsPerClient: 5, // per client behind the proxy, not for the proxy
|
|
1209
|
+
onConnect: (session) =>
|
|
1210
|
+
blocked.has(clientKey(session.remoteAddress) ?? '') ? reply(554, '5.7.1', 'Go away') : undefined,
|
|
1211
|
+
onData: async (message) => {
|
|
1212
|
+
await new Response(message.content).bytes();
|
|
1213
|
+
},
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
await server.listen({ port: 25 });
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
### STARTTLS on 25 and 587
|
|
1220
|
+
|
|
1221
|
+
Nothing changes for STARTTLS. The header is read before the greeting; the
|
|
1222
|
+
session then runs in clear, `STARTTLS` upgrades it as usual, and the
|
|
1223
|
+
client's address carries over to the encrypted session. The proxy passes
|
|
1224
|
+
the TLS records through untouched: it must not terminate TLS itself.
|
|
1225
|
+
|
|
1226
|
+
### Implicit TLS on 465, behind a proxy
|
|
1227
|
+
|
|
1228
|
+
On an `implicitTls` port the proxy's header comes first, then the client's
|
|
1229
|
+
ClientHello. A Bun TLS listener would take the header for a broken
|
|
1230
|
+
ClientHello, and Bun's `socket.upgradeTLS` reads only bytes that arrive
|
|
1231
|
+
after it is called, while a proxy often sends the ClientHello in the same
|
|
1232
|
+
TCP segment as the header (with Bun 1.4.2, the handshake then never
|
|
1233
|
+
completes).
|
|
1234
|
+
|
|
1235
|
+
So with `proxyProtocol` and `implicitTls` together, the port listens in
|
|
1236
|
+
clear, and every connection's TLS runs through `node:tls` over the raw
|
|
1237
|
+
socket: after the header from a trusted proxy, from the first byte from
|
|
1238
|
+
anyone else. For you, it changes little:
|
|
1239
|
+
|
|
1240
|
+
- `tls.key` and `tls.cert` still take a string, bytes or a `BunFile`; they
|
|
1241
|
+
are read when `listen()` is called.
|
|
1242
|
+
- Behind the header, the TLS handshake has `handshakeTimeout` again, on
|
|
1243
|
+
Bun's socket timer, as on implicit TLS without a proxy; meanwhile the
|
|
1244
|
+
socket holds its slot, counted by the client's address.
|
|
1245
|
+
- A refusal by a limit, `onConnect`, the greeting and `greetingDelay` wait
|
|
1246
|
+
for the handshake, so the client reads them over TLS and
|
|
1247
|
+
`session.secure` is `true`.
|
|
1248
|
+
|
|
1249
|
+
```ts
|
|
1250
|
+
import { createSmtpServer } from '@bumail/smtp';
|
|
1251
|
+
|
|
1252
|
+
const server = createSmtpServer({
|
|
1253
|
+
hostname: 'smtp.example.com',
|
|
1254
|
+
mode: 'submission',
|
|
1255
|
+
implicitTls: true,
|
|
1256
|
+
localDomains: ['example.com'],
|
|
1257
|
+
tls: {
|
|
1258
|
+
key: Bun.file('/etc/ssl/smtp.example.com.key'),
|
|
1259
|
+
cert: Bun.file('/etc/ssl/smtp.example.com.crt'),
|
|
1260
|
+
},
|
|
1261
|
+
proxyProtocol: { trusted: ['10.0.0.5'] },
|
|
1262
|
+
authenticate: ({ username, password }) => username === 'alice' && password === Bun.env['ALICE_PASSWORD'],
|
|
1263
|
+
async onData(message) {
|
|
1264
|
+
await Bun.write(`queue/${message.id}.eml`, await new Response(message.content).bytes());
|
|
1265
|
+
},
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
await server.listen({ port: 465 });
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
### With Traefik
|
|
1272
|
+
|
|
1273
|
+
Traefik's TCP routers pass a connection through and, with a
|
|
1274
|
+
`serversTransport` whose `proxyProtocol.version` is 2, send a v2 header
|
|
1275
|
+
first. One entry
|
|
1276
|
+
point per port, in the static configuration:
|
|
1277
|
+
|
|
1278
|
+
```yaml
|
|
1279
|
+
# traefik.yml
|
|
1280
|
+
entryPoints:
|
|
1281
|
+
smtp:
|
|
1282
|
+
address: ':25'
|
|
1283
|
+
submission:
|
|
1284
|
+
address: ':587'
|
|
1285
|
+
submissions:
|
|
1286
|
+
address: ':465'
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
Then, in the dynamic configuration, one router and one service each.
|
|
1290
|
+
``HostSNI(`*`)`` with no `tls` section matches every connection without
|
|
1291
|
+
terminating TLS, so STARTTLS on 25 and 587, and the TLS of 465, pass
|
|
1292
|
+
through to the server, which holds the certificate:
|
|
1293
|
+
|
|
1294
|
+
```yaml
|
|
1295
|
+
# dynamic.yml
|
|
1296
|
+
tcp:
|
|
1297
|
+
routers:
|
|
1298
|
+
smtp:
|
|
1299
|
+
entryPoints: [smtp]
|
|
1300
|
+
rule: 'HostSNI(`*`)'
|
|
1301
|
+
service: smtp
|
|
1302
|
+
submission:
|
|
1303
|
+
entryPoints: [submission]
|
|
1304
|
+
rule: 'HostSNI(`*`)'
|
|
1305
|
+
service: submission
|
|
1306
|
+
submissions:
|
|
1307
|
+
entryPoints: [submissions]
|
|
1308
|
+
rule: 'HostSNI(`*`)'
|
|
1309
|
+
service: submissions
|
|
1310
|
+
serversTransports:
|
|
1311
|
+
proxy-v2:
|
|
1312
|
+
proxyProtocol:
|
|
1313
|
+
version: 2
|
|
1314
|
+
services:
|
|
1315
|
+
smtp:
|
|
1316
|
+
loadBalancer:
|
|
1317
|
+
serversTransport: proxy-v2
|
|
1318
|
+
servers:
|
|
1319
|
+
- address: 'mail:25'
|
|
1320
|
+
submission:
|
|
1321
|
+
loadBalancer:
|
|
1322
|
+
serversTransport: proxy-v2
|
|
1323
|
+
servers:
|
|
1324
|
+
- address: 'mail:587'
|
|
1325
|
+
submissions:
|
|
1326
|
+
loadBalancer:
|
|
1327
|
+
serversTransport: proxy-v2
|
|
1328
|
+
servers:
|
|
1329
|
+
- address: 'mail:465'
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
This was run against Traefik v3.7 in Docker, a client reaching it with
|
|
1333
|
+
`openssl s_client` on 465 and with `-starttls smtp` on 25 and 587: the
|
|
1334
|
+
server saw the client's address in `onConnect` and every hook, Traefik's
|
|
1335
|
+
nowhere. Older v3 releases take the version on the service instead —
|
|
1336
|
+
`loadBalancer.proxyProtocol: { version: 2 }` beside `servers` — which
|
|
1337
|
+
v3.7 still honours, with a warning that it is deprecated in favour of the
|
|
1338
|
+
`serversTransport` above; the header sent is the same.
|
|
1339
|
+
|
|
1340
|
+
The matching servers, trusting Traefik's address on the network it shares
|
|
1341
|
+
with the mail server (`172.20.0.2` here; give the proxy a fixed address
|
|
1342
|
+
there):
|
|
1343
|
+
|
|
1344
|
+
```ts
|
|
1345
|
+
import { createSmtpServer, type ReceivedMessage, type SmtpServerOptions } from '@bumail/smtp';
|
|
1346
|
+
|
|
1347
|
+
const tls = {
|
|
1348
|
+
key: await Bun.file('/etc/ssl/mail.example.com.key').text(),
|
|
1349
|
+
cert: await Bun.file('/etc/ssl/mail.example.com.crt').text(),
|
|
1350
|
+
};
|
|
1351
|
+
const users = new Map([['alice', await Bun.password.hash('correct horse')]]);
|
|
1352
|
+
|
|
1353
|
+
async function deliver(message: ReceivedMessage): Promise<void> {
|
|
1354
|
+
await Bun.write(`spool/${message.id}.eml`, await new Response(message.content).bytes());
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
const shared = {
|
|
1358
|
+
hostname: 'mail.example.com',
|
|
1359
|
+
localDomains: ['example.com'],
|
|
1360
|
+
tls,
|
|
1361
|
+
// Traefik alone: never the network's whole range if anything else on it is not yours.
|
|
1362
|
+
proxyProtocol: { trusted: ['172.20.0.2'] },
|
|
1363
|
+
onData: deliver,
|
|
1364
|
+
} satisfies SmtpServerOptions;
|
|
1365
|
+
|
|
1366
|
+
const submission = {
|
|
1367
|
+
...shared,
|
|
1368
|
+
mode: 'submission',
|
|
1369
|
+
async authenticate({ username, password }) {
|
|
1370
|
+
const hash = users.get(username);
|
|
1371
|
+
return hash !== undefined && (await Bun.password.verify(password, hash));
|
|
1372
|
+
},
|
|
1373
|
+
} satisfies SmtpServerOptions;
|
|
1374
|
+
|
|
1375
|
+
await createSmtpServer(shared).listen({ port: 25 });
|
|
1376
|
+
await createSmtpServer(submission).listen({ port: 587 });
|
|
1377
|
+
await createSmtpServer({ ...submission, implicitTls: true }).listen({ port: 465 });
|
|
1378
|
+
```
|
|
1379
|
+
|
|
1380
|
+
Traefik's health checks, and any connection it opens on its own, come as
|
|
1381
|
+
v2 `LOCAL`: they keep the proxy's address, by design. A client that
|
|
1382
|
+
reaches the server without Traefik, on another network, is a peer not
|
|
1383
|
+
listed and is served as such.
|
|
1384
|
+
|
|
934
1385
|
## Sending mail: the client
|
|
935
1386
|
|
|
936
1387
|
`@bumail/smtp/client` is the other side of the conversation: `sendMail`
|
|
@@ -1291,6 +1742,7 @@ a server of your own.
|
|
|
1291
1742
|
|
|
1292
1743
|
```ts
|
|
1293
1744
|
import {
|
|
1745
|
+
clientKey,
|
|
1294
1746
|
DataReader,
|
|
1295
1747
|
decodePlain,
|
|
1296
1748
|
formatReply,
|
|
@@ -1310,6 +1762,12 @@ parsePath('<Joe.Bloggs@EXAMPLE.Org>', false)?.address; // 'Joe.Bloggs@example.or
|
|
|
1310
1762
|
parsePath('<>', true); // { address: '', local: '', domain: '' }: the null path, where allowed
|
|
1311
1763
|
parsePath('<>', false); // undefined
|
|
1312
1764
|
|
|
1765
|
+
parsePathCommand('TO:<Postmaster>', 'TO')?.path;
|
|
1766
|
+
// { address: 'postmaster', local: 'postmaster', domain: '', postmaster: true }
|
|
1767
|
+
|
|
1768
|
+
clientKey('::ffff:192.0.2.1'); // '192.0.2.1'
|
|
1769
|
+
clientKey('2001:db8:1:2::9'); // '2001:db8:1:2::/64'
|
|
1770
|
+
|
|
1313
1771
|
formatReply(reply(550, '5.7.1', ['a', 'b'])); // '550-5.7.1 a\r\n550 5.7.1 b\r\n'
|
|
1314
1772
|
formatReply(reply(250, '2.0.0', 'OK'), false); // '250 OK\r\n': no enhanced status
|
|
1315
1773
|
|
|
@@ -1343,6 +1801,7 @@ console.log(chunk.done, reader.bareLineBreaks);
|
|
|
1343
1801
|
| Behaviour | RFC |
|
|
1344
1802
|
| --- | --- |
|
|
1345
1803
|
| the session, commands, replies, paths, the Received field | RFC 5321 |
|
|
1804
|
+
| `RCPT TO:<postmaster>` with no domain | RFC 5321 §4.1.1.3, §4.5.1 |
|
|
1346
1805
|
| SIZE | RFC 1870 |
|
|
1347
1806
|
| PIPELINING | RFC 2920 |
|
|
1348
1807
|
| 8BITMIME | RFC 6152 |
|
|
@@ -1350,6 +1809,7 @@ console.log(chunk.done, reader.bareLineBreaks);
|
|
|
1350
1809
|
| ENHANCEDSTATUSCODES and the codes themselves | RFC 2034, RFC 3463 |
|
|
1351
1810
|
| STARTTLS | RFC 3207 |
|
|
1352
1811
|
| implicit TLS | RFC 8314 |
|
|
1812
|
+
| the PROXY protocol, v1 and v2 | not an RFC: HAProxy's `proxy-protocol.txt` |
|
|
1353
1813
|
| AUTH | RFC 4954 |
|
|
1354
1814
|
| SASL PLAIN | RFC 4616 |
|
|
1355
1815
|
| submission mode | RFC 6409 |
|