@bumail/smtp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -0
  3. package/dist/errors.d.ts +13 -0
  4. package/dist/errors.d.ts.map +1 -0
  5. package/dist/index.d.ts +9 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +1319 -0
  8. package/dist/index.js.map +29 -0
  9. package/dist/protocol/command.d.ts +20 -0
  10. package/dist/protocol/command.d.ts.map +1 -0
  11. package/dist/protocol/data.d.ts +24 -0
  12. package/dist/protocol/data.d.ts.map +1 -0
  13. package/dist/protocol/path.d.ts +20 -0
  14. package/dist/protocol/path.d.ts.map +1 -0
  15. package/dist/protocol/reply.d.ts +13 -0
  16. package/dist/protocol/reply.d.ts.map +1 -0
  17. package/dist/protocol/sasl.d.ts +16 -0
  18. package/dist/protocol/sasl.d.ts.map +1 -0
  19. package/dist/server/auth.d.ts +6 -0
  20. package/dist/server/auth.d.ts.map +1 -0
  21. package/dist/server/commands.d.ts +4 -0
  22. package/dist/server/commands.d.ts.map +1 -0
  23. package/dist/server/connection.d.ts +52 -0
  24. package/dist/server/connection.d.ts.map +1 -0
  25. package/dist/server/guard.d.ts +18 -0
  26. package/dist/server/guard.d.ts.map +1 -0
  27. package/dist/server/input.d.ts +31 -0
  28. package/dist/server/input.d.ts.map +1 -0
  29. package/dist/server/intake.d.ts +31 -0
  30. package/dist/server/intake.d.ts.map +1 -0
  31. package/dist/server/lines.d.ts +25 -0
  32. package/dist/server/lines.d.ts.map +1 -0
  33. package/dist/server/options.d.ts +151 -0
  34. package/dist/server/options.d.ts.map +1 -0
  35. package/dist/server/received.d.ts +11 -0
  36. package/dist/server/received.d.ts.map +1 -0
  37. package/dist/server/refusals.d.ts +16 -0
  38. package/dist/server/refusals.d.ts.map +1 -0
  39. package/dist/server/server.d.ts +25 -0
  40. package/dist/server/server.d.ts.map +1 -0
  41. package/dist/server/settings.d.ts +16 -0
  42. package/dist/server/settings.d.ts.map +1 -0
  43. package/dist/server/state.d.ts +24 -0
  44. package/dist/server/state.d.ts.map +1 -0
  45. package/dist/server/transaction.d.ts +6 -0
  46. package/dist/server/transaction.d.ts.map +1 -0
  47. package/dist/server/transport.d.ts +42 -0
  48. package/dist/server/transport.d.ts.map +1 -0
  49. package/docs/README.md +10 -0
  50. package/docs/guide.md +976 -0
  51. package/docs/roadmap.md +45 -0
  52. package/docs/troubleshooting.md +1308 -0
  53. package/package.json +55 -0
@@ -0,0 +1,151 @@
1
+ import type { Path } from '../protocol/path';
2
+ import type { Reply } from '../protocol/reply';
3
+ import type { Credentials } from '../protocol/sasl';
4
+ /** What the server knows of one connection; hooks read it, and may keep their own state in `data`. */
5
+ export interface Session {
6
+ readonly id: string;
7
+ readonly remoteAddress: string;
8
+ /** TLS is on: implicit, or after STARTTLS. */
9
+ readonly secure: boolean;
10
+ /** The name the client gave in EHLO or HELO. */
11
+ readonly helo?: string;
12
+ /** The client said EHLO, not HELO. */
13
+ readonly esmtp: boolean;
14
+ /** The username the client authenticated as. */
15
+ readonly user?: string;
16
+ /**
17
+ * For the app's own state across hooks. It lasts the whole connection,
18
+ * STARTTLS included: what the server learnt before TLS is forgotten, but
19
+ * what the app keeps here is the app's to judge.
20
+ */
21
+ readonly data: Record<string, unknown>;
22
+ }
23
+ /** The sender and recipients of one message. */
24
+ export interface Envelope {
25
+ /** `''` for the null reverse-path `<>`: a bounce. */
26
+ readonly from: string;
27
+ readonly to: readonly string[];
28
+ /** The client asked for SMTPUTF8 (RFC 6531). */
29
+ readonly smtputf8: boolean;
30
+ /** `7BIT` or `8BITMIME`, as MAIL FROM's BODY= said; `7BIT` when it said nothing. */
31
+ readonly body: '7BIT' | '8BITMIME';
32
+ }
33
+ /** A message on its way in, handed to `onData` as the client sends it. */
34
+ export interface ReceivedMessage {
35
+ /** The id the server gave it, also in its Received field and the 250 reply. */
36
+ readonly id: string;
37
+ readonly envelope: Envelope;
38
+ /**
39
+ * The message as the client sends it, dot-unstuffed, with the server's
40
+ * Received field first. It holds 64 KiB at most: the server reads the
41
+ * client only as fast as this stream is read.
42
+ *
43
+ * It ends in an `SmtpError` when the message must not be delivered:
44
+ * `MESSAGE_TOO_BIG`, `BARE_LINE_BREAK` (SMTP smuggling),
45
+ * `CONNECTION_LOST`, `HOOK_TIMEOUT` or `MESSAGE_NOT_READ`. Read it to
46
+ * its end before keeping anything: a message is taken only when the
47
+ * stream ended cleanly and `onData` resolved, within `hookTimeout`,
48
+ * without a refusal. `signal` says when it was not.
49
+ */
50
+ readonly content: ReadableStream<Uint8Array>;
51
+ /**
52
+ * Aborts when the server refuses the message on `onData`'s behalf: the
53
+ * stream errored, `onData` accepted before reading to the end, did not
54
+ * answer within `hookTimeout`, threw, or answered what is not a
55
+ * refusal, or the connection closed before the reply. Its `reason` is
56
+ * the `SmtpError`, or what `onData` threw. A refusal `onData` returns
57
+ * leaves it alone, unless the client never hears it: a later stream
58
+ * failure (whose 552 or 550 replaces `onData`'s reply) or a closed
59
+ * connection still aborts it, with that error. The stream may already
60
+ * have ended cleanly, so check `signal.aborted` (or listen for `abort`)
61
+ * before keeping a message for good: the client will send it again.
62
+ */
63
+ readonly signal: AbortSignal;
64
+ }
65
+ /**
66
+ * What a hook answers: nothing to accept, or a reply — a 4xx or 5xx code —
67
+ * to refuse with. A hook that throws, answers anything else, or does not
68
+ * settle within `hookTimeout` refuses with `451 4.3.0`, and the error goes
69
+ * to `onError`.
70
+ */
71
+ export type HookResult = Reply | undefined | void;
72
+ export interface SmtpHooks {
73
+ /** A client connected. */
74
+ onConnect?(session: Session): HookResult | Promise<HookResult>;
75
+ /** After the server's own checks of MAIL FROM. */
76
+ onMailFrom?(path: Path, session: Session): HookResult | Promise<HookResult>;
77
+ /** After the server's own checks of RCPT TO, relaying included. */
78
+ onRcptTo?(path: Path, session: Session): HookResult | Promise<HookResult>;
79
+ /**
80
+ * A message is coming: read `message.content` to its end, then deliver
81
+ * or queue it before resolving. The reply to DATA waits for this hook
82
+ * and for the end of the content, so the client knows whether the
83
+ * message was taken. If the stream errors, do not deliver.
84
+ */
85
+ onData(message: ReceivedMessage, session: Session): HookResult | Promise<HookResult>;
86
+ }
87
+ /** Bun's TLS options: `key` and `cert`, as `Bun.listen` takes them. */
88
+ export interface TlsOptions {
89
+ readonly key: string | Uint8Array | Bun.BunFile;
90
+ readonly cert: string | Uint8Array | Bun.BunFile;
91
+ }
92
+ export interface SmtpServerOptions extends SmtpHooks {
93
+ /** The server's own name, in its greeting, its EHLO reply and its Received fields. */
94
+ readonly hostname: string;
95
+ /**
96
+ * `mx` receives mail for `localDomains` from anyone, and relays nothing
97
+ * without AUTH (port 25). `submission` takes mail only from
98
+ * authenticated users (ports 587 and 465). Default `mx`.
99
+ */
100
+ readonly mode?: 'mx' | 'submission';
101
+ /**
102
+ * The domains this server receives mail for, matched whole and without
103
+ * case. A recipient anywhere else is relaying, and is refused unless the
104
+ * session authenticated: there is no option to relay without AUTH. A
105
+ * function is given the domain in lower case; if it throws or times out,
106
+ * that recipient gets `451 4.3.0`.
107
+ */
108
+ readonly localDomains: readonly string[] | ((domain: string) => boolean | Promise<boolean>);
109
+ /** Turns on STARTTLS, or TLS from the first byte with `implicitTls`. */
110
+ readonly tls?: TlsOptions;
111
+ /** TLS from the first byte (port 465, RFC 8314). Needs `tls`. */
112
+ readonly implicitTls?: boolean;
113
+ /**
114
+ * Checks credentials; turns on AUTH PLAIN and LOGIN (RFC 4954), offered
115
+ * and accepted only over TLS, so it needs `tls`. Only `true` accepts. A
116
+ * PLAIN authorization identity other than the username is refused before
117
+ * this is called: a session acts as the user it authenticated as.
118
+ */
119
+ authenticate?(credentials: Credentials, session: Session): boolean | Promise<boolean>;
120
+ /** The largest message, in bytes; announced with SIZE (RFC 1870). Default 25 MiB. */
121
+ readonly maxMessageSize?: number;
122
+ /** Recipients per message. Default 100, the minimum RFC 5321 §4.5.3.1.8 asks a server to take. */
123
+ readonly maxRecipients?: number;
124
+ /** Open connections at once. Default 1000. */
125
+ readonly maxConnections?: number;
126
+ /** Commands that fail before the server hangs up. Default 10. */
127
+ readonly maxErrors?: number;
128
+ /** Idle time before the server hangs up, in seconds; any byte from the client starts it again. Default 300, RFC 5321 §4.5.3.2.7's. */
129
+ readonly timeout?: number;
130
+ /**
131
+ * Seconds the server waits, once `onConnect` accepted, before its 220
132
+ * greeting. A client that talks in that time is refused with `554`
133
+ * (RFC 5321 §4.3.1): a legitimate client waits for the greeting, many
134
+ * spam senders do not. Default 0: the greeting goes out at once, and
135
+ * only a client that talks before it is refused.
136
+ */
137
+ readonly greetingDelay?: number;
138
+ /**
139
+ * Seconds a hook, `authenticate` or `localDomains` has to settle — and
140
+ * `onData` to read the next part of a message, then to answer once it
141
+ * ended. Past it, the command is refused with `451 4.3.0`. Default 60.
142
+ */
143
+ readonly hookTimeout?: number;
144
+ /**
145
+ * Told of what went wrong in the app's code: a hook, `authenticate` or
146
+ * `localDomains` that threw or timed out, or a hook reply that is not a
147
+ * refusal. The client only sees a 451 or 454.
148
+ */
149
+ onError?(error: unknown, session: Session): void;
150
+ }
151
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { Connection } from './connection';
2
+ /** An RFC 5322 §3.3 date-time, in UTC. */
3
+ export declare function dateTime(date: Date): string;
4
+ /** RFC 3848: how the message came — SMTP, ESMTP, then S for TLS and A for AUTH. */
5
+ export declare function protocolOf(esmtp: boolean, secure: boolean, authenticated: boolean): string;
6
+ /**
7
+ * The Received field the server puts on top (RFC 5321 §4.4): who sent the
8
+ * message, from where, how, to whom when there is one recipient, and when.
9
+ */
10
+ export declare function receivedField(connection: Connection, recipients: readonly string[], id: string, now?: Date): string;
11
+ //# sourceMappingURL=received.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"received.d.ts","sourceRoot":"","sources":["../../src/server/received.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAoB/C,0CAA0C;AAC1C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAE3C;AAED,mFAAmF;AACnF,wBAAgB,UAAU,CACzB,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACpB,MAAM,CAGR;AAiBD;;;GAGG;AACH,wBAAgB,aAAa,CAC5B,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,EAAE,EAAE,MAAM,EACV,GAAG,OAAa,GACd,MAAM,CAYR"}
@@ -0,0 +1,16 @@
1
+ import { SmtpError } from '../errors';
2
+ /** Why a message on its way in is refused, as the reply and the error `onData` and `onError` see. */
3
+ export declare const TOO_BIG: import("..").Reply;
4
+ export declare const BARE_LINE_BREAK: import("..").Reply;
5
+ export declare function tooBig(maxMessageSize: number): SmtpError;
6
+ /** A bare CR or LF is how SMTP smuggling hides a second message. */
7
+ export declare function bareLineBreak(): SmtpError;
8
+ /**
9
+ * The connection closed mid-message, or after its end but before the reply:
10
+ * the client hung up, or the server closed it (idle timeout, socket error).
11
+ */
12
+ export declare function connectionLost(ended: boolean): SmtpError;
13
+ export declare function notAnswered(seconds: number): SmtpError;
14
+ export declare function notReadInTime(seconds: number): SmtpError;
15
+ export declare function notRead(): SmtpError;
16
+ //# sourceMappingURL=refusals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refusals.d.ts","sourceRoot":"","sources":["../../src/server/refusals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,qGAAqG;AAErG,eAAO,MAAM,OAAO,oBAAoD,CAAC;AAEzE,eAAO,MAAM,eAAe,oBAI3B,CAAC;AAEF,wBAAgB,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAKxD;AAED,oEAAoE;AACpE,wBAAgB,aAAa,IAAI,SAAS,CAKzC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAOxD;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAKtD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAKxD;AAED,wBAAgB,OAAO,IAAI,SAAS,CAKnC"}
@@ -0,0 +1,25 @@
1
+ import type { SmtpServerOptions } from './options';
2
+ export interface SmtpServer {
3
+ /** Starts listening; resolves once the port is bound. Once only: a second call throws. */
4
+ listen(options: {
5
+ port: number;
6
+ hostname?: string;
7
+ }): Promise<{
8
+ port: number;
9
+ hostname: string;
10
+ }>;
11
+ /** Stops listening; `closeConnections` hangs up on every client too. */
12
+ stop(closeConnections?: boolean): void;
13
+ /** Open connections. */
14
+ readonly connections: number;
15
+ }
16
+ /**
17
+ * An SMTP server (RFC 5321) on `Bun.listen`: EHLO with PIPELINING, SIZE,
18
+ * 8BITMIME, SMTPUTF8 and ENHANCEDSTATUSCODES; STARTTLS (RFC 3207) or
19
+ * implicit TLS (RFC 8314); AUTH PLAIN and LOGIN over TLS only (RFC 4954);
20
+ * policy through hooks. It never relays without AUTH: a recipient outside
21
+ * `localDomains` is refused to an unauthenticated session, whatever the
22
+ * hooks say.
23
+ */
24
+ export declare function createSmtpServer(options: SmtpServerOptions): SmtpServer;
25
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +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;AAWnD,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;AAqED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAuDvE"}
@@ -0,0 +1,16 @@
1
+ import type { SmtpServerOptions } from './options';
2
+ /** The options with their defaults, checked once. */
3
+ export interface Settings {
4
+ readonly options: SmtpServerOptions;
5
+ readonly mode: 'mx' | 'submission';
6
+ readonly maxMessageSize: number;
7
+ readonly maxRecipients: number;
8
+ readonly maxConnections: number;
9
+ readonly maxErrors: number;
10
+ readonly timeout: number;
11
+ readonly hookTimeout: number;
12
+ readonly greetingDelay: number;
13
+ isLocal(domain: string): boolean | Promise<boolean>;
14
+ }
15
+ export declare function settingsOf(options: SmtpServerOptions): Settings;
16
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +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;AAgDD,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,QAAQ,CA+C/D"}
@@ -0,0 +1,24 @@
1
+ import type { Envelope } from './options';
2
+ /** A mail transaction in progress (RFC 5321 §3.3). */
3
+ export interface Transaction {
4
+ from?: string;
5
+ to: string[];
6
+ smtputf8: boolean;
7
+ body: Envelope['body'];
8
+ }
9
+ /** What the next line from the client is. */
10
+ export type Waiting = 'command' | 'data' | 'auth-plain' | 'auth-login-user' | 'auth-login-password';
11
+ /** The state of a session the commands change. */
12
+ export interface State {
13
+ helo?: string | undefined;
14
+ esmtp: boolean;
15
+ user?: string | undefined;
16
+ secure: boolean;
17
+ transaction: Transaction;
18
+ waiting: Waiting;
19
+ loginUser?: string | undefined;
20
+ errors: number;
21
+ authFailures: number;
22
+ }
23
+ export declare const emptyTransaction: () => Transaction;
24
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/server/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C,sDAAsD;AACtD,MAAM,WAAW,WAAW;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CACvB;AAED,6CAA6C;AAC7C,MAAM,MAAM,OAAO,GAChB,SAAS,GACT,MAAM,GACN,YAAY,GACZ,iBAAiB,GACjB,qBAAqB,CAAC;AAEzB,kDAAkD;AAClD,MAAM,WAAW,KAAK;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,WAAW,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,gBAAgB,QAAO,WAIlC,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { Connection } from './connection';
2
+ /** `MAIL FROM:<path> [SIZE=] [BODY=] [SMTPUTF8] [AUTH=]` (RFC 5321 §4.1.1.2). */
3
+ export declare function mail(connection: Connection, argument: string): Promise<void>;
4
+ /** `RCPT TO:<path>` (RFC 5321 §4.1.1.3), where relaying is refused without AUTH. */
5
+ export declare function rcpt(connection: Connection, argument: string): Promise<void>;
6
+ //# sourceMappingURL=transaction.d.ts.map
@@ -0,0 +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,CAwCf"}
@@ -0,0 +1,42 @@
1
+ import type { Socket } from 'bun';
2
+ /** What a connection needs from its socket: Bun's, or a fake one in specs. */
3
+ export interface Transport {
4
+ readonly remoteAddress: string;
5
+ readonly secure: boolean;
6
+ /** Queues text for the client; what the socket cannot take now is kept for `drain`. */
7
+ write(text: string): void;
8
+ /** Resolves once everything written has left. */
9
+ drained(): Promise<void>;
10
+ /** Hangs up once everything written has left. */
11
+ end(): void;
12
+ /** Stops reading from the client, while the server catches up. */
13
+ pause(): void;
14
+ resume(): void;
15
+ /** Starts TLS on the connection, once the 220 to STARTTLS has left. */
16
+ startTls(): void;
17
+ /** Starts the idle time again, as a byte from the client does. */
18
+ restartIdle(seconds: number): void;
19
+ }
20
+ /**
21
+ * A Bun socket as a Transport. Bun's sockets do not buffer: `write` takes
22
+ * what the kernel takes and says how much. The rest waits here for the
23
+ * `drain` event, so no reply is lost to a client that reads slowly.
24
+ */
25
+ export declare class SocketTransport implements Transport {
26
+ #private;
27
+ readonly remoteAddress: string;
28
+ readonly secure: boolean;
29
+ constructor(socket: Socket<unknown>, secure: boolean, startTls: () => void, remoteAddress?: string);
30
+ write(text: string): void;
31
+ /** Bun's `drain`: the socket can take more. */
32
+ drain(): void;
33
+ drained(): Promise<void>;
34
+ /** The socket closed: nothing more will leave. */
35
+ closed(): void;
36
+ end(): void;
37
+ pause(): void;
38
+ resume(): void;
39
+ startTls(): void;
40
+ restartIdle(seconds: number): void;
41
+ }
42
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/server/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAElC,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,uFAAuF;IACvF,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iDAAiD;IACjD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,iDAAiD;IACjD,GAAG,IAAI,IAAI,CAAC;IACZ,kEAAkE;IAClE,KAAK,IAAI,IAAI,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;IACf,uEAAuE;IACvE,QAAQ,IAAI,IAAI,CAAC;IACjB,kEAAkE;IAClE,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED;;;;GAIG;AACH,qBAAa,eAAgB,YAAW,SAAS;;IAEhD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;gBAOxB,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,EACvB,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,MAAM,IAAI,EACpB,aAAa,SAAuB;IAQrC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAUzB,+CAA+C;IAC/C,KAAK,IAAI,IAAI;IAcb,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAKxB,kDAAkD;IAClD,MAAM,IAAI,IAAI;IAKd,GAAG,IAAI,IAAI;IAKX,KAAK,IAAI,IAAI;IAIb,MAAM,IAAI,IAAI;IAId,QAAQ,IAAI,IAAI;IAIhB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;CAGlC"}
package/docs/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # @bumail/smtp documentation
2
+
3
+ The [package README](../README.md) is the short version. This folder is the
4
+ long one.
5
+
6
+ | Page | Read it when |
7
+ | --- | --- |
8
+ | [Guide](guide.md) | running an MX, a submission server or implicit TLS; reading what a reply means; writing hooks and keeping state in `Session.data`; handling what `onData` receives; delivering into `@bumail/store`; setting TLS, limits and `greetingDelay`; or checking which RFC a behaviour follows |
9
+ | [Troubleshooting](troubleshooting.md) | `createSmtpServer` threw, the server will not listen, or a client reports a reply you did not expect |
10
+ | [Roadmap](roadmap.md) | wondering what is coming — the client, DSN, a ready-made delivery into the store — and what is not planned |