@bumail/smtp 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -4
- package/dist/chunks/{index-3423jxf9.js → index-753q702a.js} +9 -3
- package/dist/chunks/index-753q702a.js.map +14 -0
- package/dist/client/index.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +226 -108
- package/dist/index.js.map +10 -7
- 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/client-key.d.ts +15 -0
- package/dist/server/client-key.d.ts.map +1 -0
- package/dist/server/listener.d.ts +37 -0
- package/dist/server/listener.d.ts.map +1 -0
- package/dist/server/options.d.ts +25 -1
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/settings.d.ts +2 -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/transaction.d.ts.map +1 -1
- package/docs/guide.md +130 -8
- package/docs/roadmap.md +44 -0
- package/docs/troubleshooting.md +84 -3
- package/package.json +2 -2
- package/dist/chunks/index-3423jxf9.js.map +0 -14
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,sGAAsG;AACtG,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,gDAAgD;IAChD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,sCAAsC;IACtC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED,gDAAgD;AAChD,MAAM,WAAW,QAAQ;IACxB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,gDAAgD;IAChD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,oFAAoF;IACpF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;CACnC;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC/B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IAC7C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC7B;AAED;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,SAAS,GAAG,IAAI,CAAC;AAElD,MAAM,WAAW,SAAS;IACzB
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAInD,MAAM,WAAW,UAAU;IAC1B,0FAA0F;IAC1F,MAAM,CAAC,OAAO,EAAE;QACf,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,wEAAwE;IACxE,IAAI,CAAC,gBAAgB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACvC,wBAAwB;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAkDvE"}
|
|
@@ -6,9 +6,11 @@ export interface Settings {
|
|
|
6
6
|
readonly maxMessageSize: number;
|
|
7
7
|
readonly maxRecipients: number;
|
|
8
8
|
readonly maxConnections: number;
|
|
9
|
+
readonly maxConnectionsPerClient: number;
|
|
9
10
|
readonly maxErrors: number;
|
|
10
11
|
readonly timeout: number;
|
|
11
12
|
readonly hookTimeout: number;
|
|
13
|
+
readonly handshakeTimeout: number;
|
|
12
14
|
readonly greetingDelay: number;
|
|
13
15
|
isLocal(domain: string): boolean | Promise<boolean>;
|
|
14
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/server/settings.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,qDAAqD;AACrD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,YAAY,CAAC;IACnC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpD;AAkED,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,QAAQ,
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/server/settings.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,qDAAqD;AACrD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,YAAY,CAAC;IACnC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpD;AAkED,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,QAAQ,CAyD/D"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connections a server holds, and how many each client holds, by
|
|
3
|
+
* `clientKey`. `release` is idempotent: every close handler calls it.
|
|
4
|
+
*/
|
|
5
|
+
export declare class Slots<T> {
|
|
6
|
+
#private;
|
|
7
|
+
get size(): number;
|
|
8
|
+
/** How many connections the client `key` holds. */
|
|
9
|
+
of(key: string | undefined): number;
|
|
10
|
+
take(connection: T, key: string | undefined): void;
|
|
11
|
+
release(connection: T): void;
|
|
12
|
+
connections(): T[];
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=slots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/server/slots.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,KAAK,CAAC,CAAC;;IAInB,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,mDAAmD;IACnD,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM;IAInC,IAAI,CAAC,UAAU,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAKlD,OAAO,CAAC,UAAU,EAAE,CAAC,GAAG,IAAI;IAU5B,WAAW,IAAI,CAAC,EAAE;CAGlB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../../src/server/transaction.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAS/C,iFAAiF;AACjF,wBAAsB,IAAI,CACzB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAuDf;AAED,oFAAoF;AACpF,wBAAsB,IAAI,CACzB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../../src/server/transaction.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAS/C,iFAAiF;AACjF,wBAAsB,IAAI,CACzB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAuDf;AAED,oFAAoF;AACpF,wBAAsB,IAAI,CACzB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAyCf"}
|
package/docs/guide.md
CHANGED
|
@@ -8,6 +8,7 @@ with the client, and the RFCs behind each behaviour.
|
|
|
8
8
|
- [Options](#options)
|
|
9
9
|
- [The session, and what each reply means](#the-session-and-what-each-reply-means)
|
|
10
10
|
- [Hooks, and their order](#hooks-and-their-order)
|
|
11
|
+
- [`RCPT TO:<postmaster>`](#rcpt-topostmaster)
|
|
11
12
|
- [Session.data](#sessiondata)
|
|
12
13
|
- [What onData receives](#what-ondata-receives)
|
|
13
14
|
- [Delivering into @bumail/store](#delivering-into-bumailstore)
|
|
@@ -67,6 +68,8 @@ export interface SmtpServer {
|
|
|
67
68
|
| `maxMessageSize` | `number` | 25 MiB | bytes; announced with SIZE |
|
|
68
69
|
| `maxRecipients` | `number` | `100` | recipients per message |
|
|
69
70
|
| `maxConnections` | `number` | `1000` | open connections at once |
|
|
71
|
+
| `maxConnectionsPerClient` | `number` | `10` | open connections at once from one client: an IPv4 address, or an IPv6 /64; see [Limits](#limits) |
|
|
72
|
+
| `handshakeTimeout` | `number` | `10` | seconds a client on implicit TLS has to complete its handshake, from the TCP connection on; past it, the socket is closed. See [TLS](#tls) |
|
|
70
73
|
| `maxErrors` | `number` | `10` | failed commands before the server hangs up |
|
|
71
74
|
| `timeout` | `number` | `300` | seconds since the client's last byte, or since the 220, before the server hangs up |
|
|
72
75
|
| `hookTimeout` | `number` | `60` | seconds a hook, `authenticate` or `localDomains` has to settle, and `onData` to read on; past it, `451 4.3.0`. At most 2 147 483, what a timer can wait |
|
|
@@ -166,7 +169,7 @@ Every reply the server sends of its own:
|
|
|
166
169
|
| `250 <hostname> greets <name>` | EHLO or HELO accepted; either one also ends a transaction in progress |
|
|
167
170
|
| `501 Syntax: EHLO hostname` / `501 Syntax: HELO hostname` | the EHLO or HELO argument is missing, or is not a domain or an address literal; no enhanced code before an EHLO was accepted, `501 5.5.4 Syntax: EHLO hostname` after one |
|
|
168
171
|
| `250 2.1.0 OK` | MAIL FROM accepted |
|
|
169
|
-
| `250 2.1.5 OK` | RCPT TO accepted |
|
|
172
|
+
| `250 2.1.5 OK` | RCPT TO accepted; `RCPT TO:<postmaster>` with no domain is accepted without AUTH (see [below](#rcpt-topostmaster)) |
|
|
170
173
|
| `354 End data with <CR><LF>.<CR><LF>` | DATA: send the message |
|
|
171
174
|
| `250 2.0.0 OK queued as <id>` | the message was taken: `onData` read its content stream to the clean end and resolved without refusing |
|
|
172
175
|
| `250 2.0.0 OK` | RSET (the transaction is forgotten) or NOOP |
|
|
@@ -193,6 +196,7 @@ Every reply the server sends of its own:
|
|
|
193
196
|
| `500 5.5.2 Command unrecognized` | an unknown verb — EXPN, BDAT and the rest |
|
|
194
197
|
| `500 5.5.6 Line too long` | a command line over 2048 bytes; the rest of it is skipped |
|
|
195
198
|
| `421 4.3.2 <hostname> Too many connections, try later` | a connection past `maxConnections`, then the server hangs up |
|
|
199
|
+
| `421 4.7.0 <hostname> Too many connections from your address, try later` | a connection past `maxConnectionsPerClient` from one client, then the server hangs up |
|
|
196
200
|
| `421 4.4.2 <hostname> Idle too long, closing` | nothing for `timeout` seconds |
|
|
197
201
|
| `421 4.7.0 <hostname> Too many errors, closing` | `maxErrors` failed commands |
|
|
198
202
|
| `421 4.3.0 Local error, closing` | the server failed itself |
|
|
@@ -234,7 +238,7 @@ In the order a session meets them:
|
|
|
234
238
|
|
|
235
239
|
| Hook | Runs | After the server checked | A refusal |
|
|
236
240
|
| --- | --- | --- | --- |
|
|
237
|
-
| `onConnect` | when a client connects, before the greeting | the connection
|
|
241
|
+
| `onConnect` | when a client connects, before the greeting | the connection counts: `maxConnections`, then `maxConnectionsPerClient` | replaces the greeting, at once even with `greetingDelay`; then the server hangs up. When `onConnect` accepts, a client that talked before the greeting is refused with `554` |
|
|
238
242
|
| `authenticate` | at AUTH | TLS, EHLO, no transaction in progress | see [Authentication](#authentication) |
|
|
239
243
|
| `onMailFrom` | at each MAIL FROM | EHLO, AUTH in `submission`, syntax, parameters, SIZE, SMTPUTF8 | refuses the sender; the client may try another MAIL |
|
|
240
244
|
| `onRcptTo` | at each RCPT TO | MAIL, syntax, SMTPUTF8, `maxRecipients`, **relaying** | refuses that recipient; the others stand |
|
|
@@ -285,12 +289,14 @@ await server.listen({ port: 25 });
|
|
|
285
289
|
|
|
286
290
|
```ts
|
|
287
291
|
export interface Path {
|
|
288
|
-
/** `local@domain`; `''` for the null reverse-path `<>`: a bounce
|
|
292
|
+
/** `local@domain`; `''` for the null reverse-path `<>`: a bounce; `'postmaster'` for `RCPT TO:<postmaster>`. */
|
|
289
293
|
readonly address: string;
|
|
290
294
|
/** As sent: case is kept. */
|
|
291
295
|
readonly local: string;
|
|
292
|
-
/** Lower case
|
|
296
|
+
/** Lower case; `''` for `<>` and `<postmaster>`. */
|
|
293
297
|
readonly domain: string;
|
|
298
|
+
/** `true` for `RCPT TO:<postmaster>` with no domain; absent otherwise. */
|
|
299
|
+
readonly postmaster?: true;
|
|
294
300
|
}
|
|
295
301
|
```
|
|
296
302
|
|
|
@@ -309,6 +315,49 @@ surrogates are refused anywhere in a path — the route, the local part
|
|
|
309
315
|
quoted or not, the domain — even under SMTPUTF8. An IPv4 address literal
|
|
310
316
|
takes octets up to 255: `[999.1.1.1]` is not one.
|
|
311
317
|
|
|
318
|
+
## `RCPT TO:<postmaster>`
|
|
319
|
+
|
|
320
|
+
RFC 5321 §4.5.1 says every server that delivers mail must take mail for
|
|
321
|
+
`postmaster`, and §4.1.1.3 lets RCPT TO name it with no domain at all:
|
|
322
|
+
`RCPT TO:<Postmaster>`. The server takes it, in any case, as the
|
|
323
|
+
postmaster of this server, whatever domains it hosts:
|
|
324
|
+
|
|
325
|
+
- it is not relaying, so it needs no AUTH, and `localDomains` is not asked;
|
|
326
|
+
- `onRcptTo` is asked, as for any recipient, with the `Path`
|
|
327
|
+
`{ address: 'postmaster', local: 'postmaster', domain: '', postmaster: true }`,
|
|
328
|
+
so it can still refuse;
|
|
329
|
+
- `envelope.to` lists it as `'postmaster'`, with no `@`, and the Received
|
|
330
|
+
field says `for <postmaster>` when it is the only recipient.
|
|
331
|
+
|
|
332
|
+
`<postmaster@example.com>` is an ordinary path: it is checked against
|
|
333
|
+
`localDomains` like any other, and has no `postmaster` field. A path the
|
|
334
|
+
grammar would otherwise refuse stays refused: `<postmaster@>`,
|
|
335
|
+
`<"postmaster">` and `MAIL FROM:<postmaster>` get `501 5.5.4`.
|
|
336
|
+
|
|
337
|
+
Route it to a mailbox you keep: by `path.postmaster` in `onRcptTo`, by the
|
|
338
|
+
bare `'postmaster'` in `onData`.
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
import { createSmtpServer, reply } from '@bumail/smtp';
|
|
342
|
+
|
|
343
|
+
const mailboxes = new Set(['alice@example.com', 'postmaster@example.com']);
|
|
344
|
+
const postmaster = 'postmaster@example.com';
|
|
345
|
+
|
|
346
|
+
const server = createSmtpServer({
|
|
347
|
+
hostname: 'mx.example.com',
|
|
348
|
+
localDomains: ['example.com'],
|
|
349
|
+
onRcptTo: (path) =>
|
|
350
|
+
path.postmaster || mailboxes.has(path.address.toLowerCase())
|
|
351
|
+
? undefined
|
|
352
|
+
: reply(550, '5.1.1', 'No such user here'),
|
|
353
|
+
async onData(message) {
|
|
354
|
+
const recipients = message.envelope.to.map((to) => (to === 'postmaster' ? postmaster : to));
|
|
355
|
+
const bytes = await new Response(message.content).bytes();
|
|
356
|
+
for (const to of recipients) await Bun.write(`spool/${to}/${message.id}.eml`, bytes);
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
312
361
|
## Session.data
|
|
313
362
|
|
|
314
363
|
Every hook, and `authenticate`, receives the `Session`:
|
|
@@ -396,6 +445,7 @@ export interface ReceivedMessage {
|
|
|
396
445
|
export interface Envelope {
|
|
397
446
|
/** `''` for the null reverse-path `<>`: a bounce. */
|
|
398
447
|
readonly from: string;
|
|
448
|
+
/** `local@domain` each; `'postmaster'`, with no domain, for `RCPT TO:<postmaster>`. */
|
|
399
449
|
readonly to: readonly string[];
|
|
400
450
|
/** The client asked for SMTPUTF8 (RFC 6531). */
|
|
401
451
|
readonly smtputf8: boolean;
|
|
@@ -406,8 +456,9 @@ export interface Envelope {
|
|
|
406
456
|
|
|
407
457
|
The **envelope** is what MAIL FROM and RCPT TO said — not the `From:` and
|
|
408
458
|
`To:` of the message, which may differ (Bcc, mailing lists, forwarding).
|
|
409
|
-
`to` holds only the recipients the server and `onRcptTo` accepted
|
|
410
|
-
|
|
459
|
+
`to` holds only the recipients the server and `onRcptTo` accepted, each
|
|
460
|
+
`local@domain`, or `'postmaster'` for [`RCPT TO:<postmaster>`](#rcpt-topostmaster).
|
|
461
|
+
Deliver to the envelope.
|
|
411
462
|
|
|
412
463
|
The **content** is the message as the client sent it, with the leading dot
|
|
413
464
|
of each stuffed line removed (RFC 5321 §4.5.2), the terminating `.` line
|
|
@@ -767,6 +818,22 @@ STARTTLS, in clear, are dropped and never answered. The STARTTLS replies:
|
|
|
767
818
|
with `tls`, usually on 465. STARTTLS is not offered there — the connection
|
|
768
819
|
is already secure — and AUTH is offered at once.
|
|
769
820
|
|
|
821
|
+
On implicit TLS the server counts a socket from the TCP connection on, not
|
|
822
|
+
from the end of its handshake: a socket that has not finished its handshake
|
|
823
|
+
holds a slot of `maxConnections` and of `maxConnectionsPerClient`, and is
|
|
824
|
+
closed, without a reply, `handshakeTimeout` seconds (default 10) after it
|
|
825
|
+
connected, so clients that open TCP connections and never send a
|
|
826
|
+
ClientHello cannot fill the server. A refusal by a limit, `onConnect`, the
|
|
827
|
+
greeting and `greetingDelay` all wait for the handshake, so the client
|
|
828
|
+
reads them over TLS and `session.secure` is already `true`. A handshake
|
|
829
|
+
that fails is closed by Bun at once and counted out. A STARTTLS handshake
|
|
830
|
+
is bounded by the idle `timeout`, the connection already counted.
|
|
831
|
+
|
|
832
|
+
Bun 1.4.2 calls a TLS listener's `open` only once the handshake completed
|
|
833
|
+
unless the listener has a `handshake` handler; with one, `open` comes at
|
|
834
|
+
the TCP connection and the socket's timer runs during the handshake. The
|
|
835
|
+
server sets one for that reason.
|
|
836
|
+
|
|
770
837
|
An MX takes mail in clear as well as encrypted: senders on the Internet that
|
|
771
838
|
cannot do TLS still deliver. Whether to refuse them is policy — check
|
|
772
839
|
`session.secure` in a hook:
|
|
@@ -837,6 +904,8 @@ await createSmtpServer({ ...submission, implicitTls: true }).listen({ port: 465
|
|
|
837
904
|
| `maxMessageSize` | 25 MiB | `552 5.3.4` at MAIL when `SIZE=` says more; during DATA once the message is more — the content stream errors with `MESSAGE_TOO_BIG`, the rest is read and dropped, and the session goes on |
|
|
838
905
|
| `maxRecipients` | 100, the least RFC 5321 §4.5.3.1.8 asks a server to take | `452 4.5.3` for each extra recipient (RFC 5321 §4.5.3.1.10); the client sends the rest in another transaction |
|
|
839
906
|
| `maxConnections` | 1000 | `421 4.3.2` and the server hangs up, before the greeting and before `onConnect` |
|
|
907
|
+
| `maxConnectionsPerClient` | 10 | `421 4.7.0 <hostname> Too many connections from your address, try later` and the server hangs up, before the greeting and before `onConnect` |
|
|
908
|
+
| `handshakeTimeout` | 10 seconds, on implicit TLS; ticks of about 4 s, as `timeout` | the socket is closed without a reply: there is no TLS to write one on |
|
|
840
909
|
| `maxErrors` | 10 | `421 4.7.0` and the server hangs up |
|
|
841
910
|
| `timeout` | 300 seconds, RFC 5321 §4.5.3.2.7's, counted from the client's last byte, or from the 220; Bun's socket timer ticks in steps of about 4 s, so the hang-up comes up to that much later | `421 4.4.2` and the server hangs up |
|
|
842
911
|
| `hookTimeout` | 60 seconds | `451 4.3.0` for that command; `onError` gets an `SmtpError` `HOOK_TIMEOUT` |
|
|
@@ -869,6 +938,50 @@ const server = createSmtpServer({
|
|
|
869
938
|
await server.listen({ port: 25 });
|
|
870
939
|
```
|
|
871
940
|
|
|
941
|
+
`maxConnectionsPerClient` keeps one host from taking every slot of
|
|
942
|
+
`maxConnections`. A client is counted by `clientKey(session.remoteAddress)`:
|
|
943
|
+
|
|
944
|
+
| Remote address | Counted as |
|
|
945
|
+
| --- | --- |
|
|
946
|
+
| `192.0.2.1` | `192.0.2.1` |
|
|
947
|
+
| `::ffff:192.0.2.1`, `::ffff:c000:201` (IPv4-mapped, as a listener on `::` sees an IPv4 client) | `192.0.2.1` |
|
|
948
|
+
| `64:ff9b::c000:201` (NAT64) | `192.0.2.1` |
|
|
949
|
+
| `2001:db8:1:2::9`, `2001:db8:1:2:ffff::1` | `2001:db8:1:2::/64` |
|
|
950
|
+
| `2001:db8::1`, `2001:0db8:0000::ffff` | `2001:db8:0:0::/64` |
|
|
951
|
+
| anything else (a Unix socket) | not limited |
|
|
952
|
+
|
|
953
|
+
An IPv6 key is always the first four groups in lower-case hex, without
|
|
954
|
+
leading zeros, every group written out — zeros too — then `::/64`:
|
|
955
|
+
`2001:db8::1` is `2001:db8:0:0::/64`, never `2001:db8::/64`. Write the
|
|
956
|
+
keys you compare with it that way.
|
|
957
|
+
|
|
958
|
+
An IPv6 host usually holds a whole /64, so counting each address apart
|
|
959
|
+
would let it open as many connections as it likes. Every close frees the
|
|
960
|
+
slot as it frees one of `maxConnections`: QUIT, the client hanging up, a
|
|
961
|
+
socket error, the idle `timeout`, `maxErrors`, a refusal from `onConnect`,
|
|
962
|
+
and `stop(true)`. The count lives in the server's memory, for that one
|
|
963
|
+
server. For a policy of your own — a list, a per-network budget — use
|
|
964
|
+
`onConnect` with `clientKey`:
|
|
965
|
+
|
|
966
|
+
```ts
|
|
967
|
+
import { clientKey, createSmtpServer, reply } from '@bumail/smtp';
|
|
968
|
+
|
|
969
|
+
// As clientKey writes them: '2001:db8:0:0::/64', not '2001:db8::/64'.
|
|
970
|
+
const blocked = new Set(['203.0.113.7', '2001:db8:0:0::/64', '2001:db8:dead:beef::/64']);
|
|
971
|
+
|
|
972
|
+
const server = createSmtpServer({
|
|
973
|
+
hostname: 'mx.example.com',
|
|
974
|
+
localDomains: ['example.com'],
|
|
975
|
+
maxConnections: 500,
|
|
976
|
+
maxConnectionsPerClient: 5,
|
|
977
|
+
onConnect: (session) =>
|
|
978
|
+
blocked.has(clientKey(session.remoteAddress) ?? '') ? reply(554, '5.7.1', 'Go away') : undefined,
|
|
979
|
+
onData: async (message) => {
|
|
980
|
+
await new Response(message.content).bytes();
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
```
|
|
984
|
+
|
|
872
985
|
Some limits are fixed: a command line is at most 2048 bytes (RFC 5321
|
|
873
986
|
§4.5.3.1.4 asks for 512 at least); a session gets three AUTH attempts; and
|
|
874
987
|
the server holds at most 64 KiB of a client's unread input, and of a
|
|
@@ -910,8 +1023,9 @@ TLS buffer. So a `221` behind replies that are never read holds the slot
|
|
|
910
1023
|
5 seconds, not until the `timeout`.
|
|
911
1024
|
|
|
912
1025
|
Stop a server with `stop()`; `stop(true)` also hangs up on every client,
|
|
913
|
-
those moved to TLS by STARTTLS included, as the idle timeout does
|
|
914
|
-
|
|
1026
|
+
those moved to TLS by STARTTLS included, as the idle timeout does; a
|
|
1027
|
+
socket still in its implicit TLS handshake is reset, with no 421, having
|
|
1028
|
+
no TLS to write one on. `connections` counts the clients currently connected:
|
|
915
1029
|
|
|
916
1030
|
```ts
|
|
917
1031
|
import { createSmtpServer } from '@bumail/smtp';
|
|
@@ -1291,6 +1405,7 @@ a server of your own.
|
|
|
1291
1405
|
|
|
1292
1406
|
```ts
|
|
1293
1407
|
import {
|
|
1408
|
+
clientKey,
|
|
1294
1409
|
DataReader,
|
|
1295
1410
|
decodePlain,
|
|
1296
1411
|
formatReply,
|
|
@@ -1310,6 +1425,12 @@ parsePath('<Joe.Bloggs@EXAMPLE.Org>', false)?.address; // 'Joe.Bloggs@example.or
|
|
|
1310
1425
|
parsePath('<>', true); // { address: '', local: '', domain: '' }: the null path, where allowed
|
|
1311
1426
|
parsePath('<>', false); // undefined
|
|
1312
1427
|
|
|
1428
|
+
parsePathCommand('TO:<Postmaster>', 'TO')?.path;
|
|
1429
|
+
// { address: 'postmaster', local: 'postmaster', domain: '', postmaster: true }
|
|
1430
|
+
|
|
1431
|
+
clientKey('::ffff:192.0.2.1'); // '192.0.2.1'
|
|
1432
|
+
clientKey('2001:db8:1:2::9'); // '2001:db8:1:2::/64'
|
|
1433
|
+
|
|
1313
1434
|
formatReply(reply(550, '5.7.1', ['a', 'b'])); // '550-5.7.1 a\r\n550 5.7.1 b\r\n'
|
|
1314
1435
|
formatReply(reply(250, '2.0.0', 'OK'), false); // '250 OK\r\n': no enhanced status
|
|
1315
1436
|
|
|
@@ -1343,6 +1464,7 @@ console.log(chunk.done, reader.bareLineBreaks);
|
|
|
1343
1464
|
| Behaviour | RFC |
|
|
1344
1465
|
| --- | --- |
|
|
1345
1466
|
| the session, commands, replies, paths, the Received field | RFC 5321 |
|
|
1467
|
+
| `RCPT TO:<postmaster>` with no domain | RFC 5321 §4.1.1.3, §4.5.1 |
|
|
1346
1468
|
| SIZE | RFC 1870 |
|
|
1347
1469
|
| PIPELINING | RFC 2920 |
|
|
1348
1470
|
| 8BITMIME | RFC 6152 |
|
package/docs/roadmap.md
CHANGED
|
@@ -34,6 +34,50 @@ Nothing in progress.
|
|
|
34
34
|
|
|
35
35
|
### Unreleased — merged, not yet published
|
|
36
36
|
|
|
37
|
+
- **A connection limit per client**, `maxConnectionsPerClient` (default
|
|
38
|
+
10): one host can no longer take every slot of `maxConnections` on an
|
|
39
|
+
MX. A client is an IPv4 address, or an IPv6 address by its /64, and an
|
|
40
|
+
IPv4-mapped or NAT64 address counts as its IPv4 address; one more is
|
|
41
|
+
answered `421 4.7.0` and closed, and every close frees its slot.
|
|
42
|
+
`clientKey(address)` is exported, to group clients the same way in
|
|
43
|
+
`onConnect`.
|
|
44
|
+
- **The implicit TLS handshake is bounded.** A socket on an
|
|
45
|
+
`implicitTls` port is counted by `maxConnections` and
|
|
46
|
+
`maxConnectionsPerClient` from the TCP connection on, and closed past
|
|
47
|
+
`handshakeTimeout` (default 10 seconds) if its handshake has not
|
|
48
|
+
completed. Before, a socket that never sent its ClientHello was counted
|
|
49
|
+
by no limit and closed by no timer.
|
|
50
|
+
- **`RCPT TO:<postmaster>` with no domain** (RFC 5321 §4.5.1) is taken
|
|
51
|
+
as this server's postmaster, without AUTH: `onRcptTo` gets a `Path`
|
|
52
|
+
with `postmaster: true`, and the envelope lists it as `'postmaster'`.
|
|
53
|
+
|
|
54
|
+
### 0.3.0
|
|
55
|
+
|
|
56
|
+
- **`isMailbox(address)`**, from `@bumail/smtp/client` — whether an
|
|
57
|
+
address is exactly one `sendMail` takes, by the same check, so code that
|
|
58
|
+
keeps addresses for later (a queue) refuses a bad one when it takes it,
|
|
59
|
+
rather than at delivery.
|
|
60
|
+
|
|
61
|
+
### 0.2.1
|
|
62
|
+
|
|
63
|
+
- **Security: no command injection through an address.** `sendMail`
|
|
64
|
+
refuses a source route (`@host:`) in `from` and `to`, and any control
|
|
65
|
+
character, lone surrogate or out-of-range IPv4 literal in an address,
|
|
66
|
+
before it connects; it used to drop the route unchecked and write the
|
|
67
|
+
address as given, so a crafted recipient put extra commands on the
|
|
68
|
+
wire. The server answers a path holding a control character, or a
|
|
69
|
+
malformed route, with `501 5.5.4`. Upgrade if an address can come from
|
|
70
|
+
user input.
|
|
71
|
+
- **A hang-up frees its slot within 500 ms**, not 5 seconds, for a client
|
|
72
|
+
the server had stopped reading because it pipelined more than it could
|
|
73
|
+
take; a client that went quiet still reads the last reply and a clean
|
|
74
|
+
end, on Linux too.
|
|
75
|
+
- **IPv6 address literals** written with their `IPv6:` tag, in `helo`,
|
|
76
|
+
`EHLO` and an address's domain, are taken in any case; one without the
|
|
77
|
+
tag is refused, as RFC 5321 §4.1.3 asks.
|
|
78
|
+
|
|
79
|
+
### 0.2.0
|
|
80
|
+
|
|
37
81
|
- **A client that stops reading no longer holds a connection slot.** When
|
|
38
82
|
the server hangs up on its own — the idle `timeout`, `maxErrors`, failed
|
|
39
83
|
AUTH, a refusal — it no longer waits for the client to read what is
|
package/docs/troubleshooting.md
CHANGED
|
@@ -83,6 +83,8 @@ A reply carries its enhanced status code (`5.7.1`, RFC 3463) only after
|
|
|
83
83
|
|
|
84
84
|
- [`421 4.7.0 … Too many errors, closing`](#421-470--too-many-errors-closing)
|
|
85
85
|
- [`421 4.3.2 … Too many connections, try later`](#421-432--too-many-connections-try-later)
|
|
86
|
+
- [`421 4.7.0 … Too many connections from your address, try later`](#421-470--too-many-connections-from-your-address-try-later)
|
|
87
|
+
- [On implicit TLS, the connection closes before any greeting](#on-implicit-tls-the-connection-closes-before-any-greeting)
|
|
86
88
|
- [`421 4.4.2 … Idle too long, closing`](#421-442--idle-too-long-closing)
|
|
87
89
|
- [`554 … Talked before the greeting`](#554--talked-before-the-greeting)
|
|
88
90
|
|
|
@@ -302,8 +304,9 @@ createSmtpServer({
|
|
|
302
304
|
|
|
303
305
|
### `SmtpError: createSmtpServer(): … must be a positive integer, not …`
|
|
304
306
|
|
|
305
|
-
**When**: `maxMessageSize`, `maxRecipients`, `maxConnections`,
|
|
306
|
-
`
|
|
307
|
+
**When**: `maxMessageSize`, `maxRecipients`, `maxConnections`,
|
|
308
|
+
`maxConnectionsPerClient`, `maxErrors`,
|
|
309
|
+
`timeout`, `hookTimeout` or `handshakeTimeout` is `0`, negative, a fraction, `NaN` or
|
|
307
310
|
`Infinity`.
|
|
308
311
|
|
|
309
312
|
**Why**: each is a bound on what a client can make the server do, and none
|
|
@@ -1352,7 +1355,11 @@ such as an MTA sending to many recipients of which most are refused.
|
|
|
1352
1355
|
to retry later.
|
|
1353
1356
|
|
|
1354
1357
|
**Fix**, as the operator: raise `maxConnections`, or look for clients that
|
|
1355
|
-
hold connections open without `QUIT`.
|
|
1358
|
+
hold connections open without `QUIT`. When one host takes many of them,
|
|
1359
|
+
lower [`maxConnectionsPerClient`](#421-470--too-many-connections-from-your-address-try-later)
|
|
1360
|
+
(default 10), so it is turned away before it fills the server. On
|
|
1361
|
+
implicit TLS, sockets still in their handshake count too, for
|
|
1362
|
+
`handshakeTimeout` seconds at most. `server.connections` gives the
|
|
1356
1363
|
number open. A client that stopped reading holds its slot until its
|
|
1357
1364
|
`timeout` at most: the server's own hang-ups — idle, too many errors, a
|
|
1358
1365
|
refusal — never wait for it to read, and drop what it did not. In 0.1.0
|
|
@@ -1362,6 +1369,80 @@ active.
|
|
|
1362
1369
|
|
|
1363
1370
|
**Fix**, as a client: retry later, and send `QUIT` when done.
|
|
1364
1371
|
|
|
1372
|
+
### `421 4.7.0 … Too many connections from your address, try later`
|
|
1373
|
+
|
|
1374
|
+
**When**: on connecting, in place of the greeting, when the same client
|
|
1375
|
+
already holds `maxConnectionsPerClient` connections (10 by default). The
|
|
1376
|
+
server hangs up, before `onConnect`. A client is an IPv4 address, or an
|
|
1377
|
+
IPv6 address by its /64; an IPv4-mapped (`::ffff:192.0.2.1`) or NAT64
|
|
1378
|
+
(`64:ff9b::…`) address counts as the IPv4 address inside it.
|
|
1379
|
+
|
|
1380
|
+
**Why**: without it one host could take every slot of `maxConnections`
|
|
1381
|
+
and keep every other sender out. An MTA opens a few connections to one
|
|
1382
|
+
MX at most, and retries a 421 later.
|
|
1383
|
+
|
|
1384
|
+
**Fix**, as the operator: raise `maxConnectionsPerClient` for a known
|
|
1385
|
+
peer that needs more, such as your own relay sending in parallel, or for
|
|
1386
|
+
many users behind one address (a NAT, a /64 shared by a provider):
|
|
1387
|
+
|
|
1388
|
+
```ts
|
|
1389
|
+
import { createSmtpServer } from '@bumail/smtp';
|
|
1390
|
+
|
|
1391
|
+
const server = createSmtpServer({
|
|
1392
|
+
hostname: 'mx.example.com',
|
|
1393
|
+
localDomains: ['example.com'],
|
|
1394
|
+
maxConnectionsPerClient: 50, // default 10
|
|
1395
|
+
onData: async (message) => {
|
|
1396
|
+
await new Response(message.content).bytes();
|
|
1397
|
+
},
|
|
1398
|
+
});
|
|
1399
|
+
```
|
|
1400
|
+
|
|
1401
|
+
Check which client is counted with `clientKey(session.remoteAddress)` in
|
|
1402
|
+
`onConnect`. A slot is freed on every close — QUIT, the client hanging up,
|
|
1403
|
+
an error, the idle `timeout`, a refusal — so a count that stays at the
|
|
1404
|
+
limit means the client holds its connections open.
|
|
1405
|
+
|
|
1406
|
+
**Fix**, as a client: send several messages over one connection, close it
|
|
1407
|
+
with `QUIT`, and retry later.
|
|
1408
|
+
|
|
1409
|
+
### On implicit TLS, the connection closes before any greeting
|
|
1410
|
+
|
|
1411
|
+
**When**: a client on an `implicitTls` port gets no `220` and the
|
|
1412
|
+
connection closes, with no reply, about `handshakeTimeout` seconds (10 by
|
|
1413
|
+
default, up to 4 s later as Bun's timer ticks) after it connected; or at
|
|
1414
|
+
once, when what it sent is not TLS.
|
|
1415
|
+
|
|
1416
|
+
**Why**: it did not complete its TLS handshake in time — it speaks clear
|
|
1417
|
+
SMTP to a TLS port, waits for a greeting before its ClientHello, or never
|
|
1418
|
+
sends one. A socket in its handshake holds a slot of `maxConnections`
|
|
1419
|
+
and `maxConnectionsPerClient`, so the server bounds it. There is no TLS
|
|
1420
|
+
yet to write a reply on.
|
|
1421
|
+
|
|
1422
|
+
**Fix**, as a client: start TLS at once on 465 (`secure: true` with
|
|
1423
|
+
`sendMail`), or use STARTTLS on 587 or 25. As the operator, for clients
|
|
1424
|
+
on slow links, raise the bound:
|
|
1425
|
+
|
|
1426
|
+
```ts
|
|
1427
|
+
import { createSmtpServer } from '@bumail/smtp';
|
|
1428
|
+
|
|
1429
|
+
const server = createSmtpServer({
|
|
1430
|
+
hostname: 'smtp.example.com',
|
|
1431
|
+
mode: 'submission',
|
|
1432
|
+
implicitTls: true,
|
|
1433
|
+
localDomains: ['example.com'],
|
|
1434
|
+
tls: {
|
|
1435
|
+
key: await Bun.file('/etc/ssl/smtp.example.com.key').text(),
|
|
1436
|
+
cert: await Bun.file('/etc/ssl/smtp.example.com.crt').text(),
|
|
1437
|
+
},
|
|
1438
|
+
authenticate: ({ username, password }) => username === 'alice' && password === Bun.env['ALICE_PASSWORD'],
|
|
1439
|
+
handshakeTimeout: 30, // default 10
|
|
1440
|
+
onData: async (message) => {
|
|
1441
|
+
await new Response(message.content).bytes();
|
|
1442
|
+
},
|
|
1443
|
+
});
|
|
1444
|
+
```
|
|
1445
|
+
|
|
1365
1446
|
### `421 4.4.2 … Idle too long, closing`
|
|
1366
1447
|
|
|
1367
1448
|
**When**: nothing came from the client for `timeout` seconds (300 by
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bumail/smtp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "An SMTP server on Bun.listen — RFC 5321 with PIPELINING, SIZE, 8BITMIME, SMTPUTF8, STARTTLS and AUTH after TLS, policy hooks, and no open relay — and a client that delivers a message to a smarthost or by MX",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@bumail/auth": "^0.3.0",
|
|
51
51
|
"@bumail/dns": "^0.1.1",
|
|
52
|
-
"@bumail/store": "^0.
|
|
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
|
-
}
|