@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
package/dist/index.js ADDED
@@ -0,0 +1,1319 @@
1
+ // src/errors.ts
2
+ class SmtpError extends Error {
3
+ name = "SmtpError";
4
+ code;
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.code = code;
8
+ }
9
+ }
10
+ // src/protocol/path.ts
11
+ var ATOM = /^[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~\u0080-\u{10ffff}]+$/u;
12
+ var LABEL = /^(?:[A-Za-z0-9\u0080-\u{10ffff}](?:[A-Za-z0-9\-\u0080-\u{10ffff}]*[A-Za-z0-9\u0080-\u{10ffff}])?)$/u;
13
+ var INVISIBLE = /[\u0080-\u009f\u2028\u2029\p{Cf}]/u;
14
+ function isDotString(text) {
15
+ return text.split(".").every((atom) => ATOM.test(atom));
16
+ }
17
+ function isQuotedString(text) {
18
+ return /^"(?:[\x20\x21\x23-\x5b\x5d-\x7e\u0080-\u{10ffff}]|\\[\x20-\x7e])*"$/u.test(text);
19
+ }
20
+ function isDomain(domain) {
21
+ if (/^\[(?:IPv6:[0-9A-Fa-f:.]+|\d{1,3}(?:\.\d{1,3}){3})\]$/.test(domain))
22
+ return true;
23
+ return domain.length <= 255 && domain.split(".").every((label) => label.length <= 63 && LABEL.test(label));
24
+ }
25
+ function parsePath(text, allowNull) {
26
+ if (!text.startsWith("<") || !text.endsWith(">"))
27
+ return;
28
+ let inner = text.slice(1, -1);
29
+ if (INVISIBLE.test(inner))
30
+ return;
31
+ if (inner === "")
32
+ return allowNull ? { address: "", local: "", domain: "" } : undefined;
33
+ const route = /^@[^:]+:/.exec(inner);
34
+ if (route)
35
+ inner = inner.slice(route[0].length);
36
+ const at = inner.lastIndexOf("@");
37
+ if (at <= 0)
38
+ return;
39
+ const local = inner.slice(0, at);
40
+ const domain = inner.slice(at + 1);
41
+ if (local.length > 64 || !(isDotString(local) || isQuotedString(local)))
42
+ return;
43
+ if (!isDomain(domain))
44
+ return;
45
+ return {
46
+ address: `${local}@${domain.toLowerCase()}`,
47
+ local,
48
+ domain: domain.toLowerCase()
49
+ };
50
+ }
51
+
52
+ // src/protocol/command.ts
53
+ function parseCommand(line) {
54
+ const space = line.indexOf(" ");
55
+ if (space < 0)
56
+ return { verb: line.toUpperCase(), argument: "" };
57
+ return {
58
+ verb: line.slice(0, space).toUpperCase(),
59
+ argument: line.slice(space + 1).trim()
60
+ };
61
+ }
62
+ function parsePathCommand(argument, keyword) {
63
+ const match = new RegExp(`^${keyword}:\\s*(<[^>]*>)(.*)$`, "i").exec(argument);
64
+ if (!match)
65
+ return;
66
+ const path = parsePath(match[1], keyword === "FROM");
67
+ if (!path)
68
+ return;
69
+ const parameters = {};
70
+ for (const item of match[2].trim().split(/\s+/)) {
71
+ if (item === "")
72
+ continue;
73
+ const equals = item.indexOf("=");
74
+ const key = (equals < 0 ? item : item.slice(0, equals)).toUpperCase();
75
+ if (!/^[A-Z0-9][A-Z0-9-]*$/.test(key))
76
+ return;
77
+ parameters[key] = equals < 0 ? "" : item.slice(equals + 1);
78
+ }
79
+ return { path, parameters };
80
+ }
81
+ // src/protocol/data.ts
82
+ var CR = 13;
83
+ var LF = 10;
84
+ var DOT = 46;
85
+
86
+ class DataReader {
87
+ #lineStart = true;
88
+ #held = [];
89
+ #size = 0;
90
+ bareLineBreaks = 0;
91
+ get size() {
92
+ return this.#size;
93
+ }
94
+ write(chunk) {
95
+ const out = new Uint8Array(chunk.length + 2);
96
+ let n = 0;
97
+ for (let i = 0;i < chunk.length; i++) {
98
+ const byte = chunk[i];
99
+ const held = this.#held;
100
+ if (held.length > 0) {
101
+ if (held[0] === DOT && held.length === 1) {
102
+ if (byte === CR) {
103
+ held.push(CR);
104
+ continue;
105
+ }
106
+ this.#held = [];
107
+ this.#lineStart = false;
108
+ i--;
109
+ continue;
110
+ }
111
+ if (held[0] === DOT && held.length === 2) {
112
+ this.#held = [];
113
+ if (byte === LF) {
114
+ this.#size += n;
115
+ return {
116
+ data: out.subarray(0, n),
117
+ done: true,
118
+ rest: chunk.slice(i + 1)
119
+ };
120
+ }
121
+ this.bareLineBreaks++;
122
+ out[n++] = CR;
123
+ this.#lineStart = false;
124
+ i--;
125
+ continue;
126
+ }
127
+ this.#held = [];
128
+ if (byte === LF) {
129
+ out[n++] = CR;
130
+ out[n++] = LF;
131
+ this.#lineStart = true;
132
+ continue;
133
+ }
134
+ this.bareLineBreaks++;
135
+ out[n++] = CR;
136
+ this.#lineStart = false;
137
+ i--;
138
+ continue;
139
+ }
140
+ if (byte === CR) {
141
+ this.#held = [CR];
142
+ continue;
143
+ }
144
+ if (byte === DOT && this.#lineStart) {
145
+ this.#held = [DOT];
146
+ continue;
147
+ }
148
+ if (byte === LF)
149
+ this.bareLineBreaks++;
150
+ out[n++] = byte;
151
+ this.#lineStart = false;
152
+ }
153
+ this.#size += n;
154
+ return { data: out.subarray(0, n), done: false, rest: new Uint8Array(0) };
155
+ }
156
+ }
157
+ // src/protocol/reply.ts
158
+ function formatReply(reply, enhanced = true) {
159
+ const lines = typeof reply.text === "string" ? [reply.text] : reply.text;
160
+ const status = enhanced && reply.status ? `${reply.status} ` : "";
161
+ return lines.map((line, i) => {
162
+ const clean = line.replace(/[\r\n]+/g, " ");
163
+ return `${reply.code}${i === lines.length - 1 ? " " : "-"}${status}${clean}\r
164
+ `;
165
+ }).join("");
166
+ }
167
+ var reply = (code, status, text) => status === undefined ? { code, text } : { code, status, text };
168
+ // src/protocol/sasl.ts
169
+ function base64ToText(text) {
170
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(text) || text.length % 4 !== 0)
171
+ return;
172
+ try {
173
+ return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.fromBase64(text));
174
+ } catch {
175
+ return;
176
+ }
177
+ }
178
+ function decodePlain(response) {
179
+ const text = base64ToText(response);
180
+ if (text === undefined)
181
+ return;
182
+ const parts = text.split("\x00");
183
+ if (parts.length !== 3)
184
+ return;
185
+ const [authorizationId = "", username = "", password = ""] = parts;
186
+ if (username === "" || password === "")
187
+ return;
188
+ return {
189
+ mechanism: "PLAIN",
190
+ username,
191
+ password,
192
+ ...authorizationId === "" || authorizationId === username ? {} : { authorizationId }
193
+ };
194
+ }
195
+ function decodeLoginStep(response) {
196
+ return base64ToText(response);
197
+ }
198
+ // src/server/guard.ts
199
+ var LOCAL_ERROR = reply(451, "4.3.0", "Local error in processing");
200
+ var TIMED_OUT = Symbol("timed out");
201
+ async function within(promise, seconds) {
202
+ let timer;
203
+ const deadline = new Promise((resolve) => {
204
+ timer = setTimeout(() => resolve(TIMED_OUT), seconds * 1000);
205
+ });
206
+ try {
207
+ return await Promise.race([promise, deadline]);
208
+ } finally {
209
+ clearTimeout(timer);
210
+ }
211
+ }
212
+ function timedOut(value) {
213
+ return value === TIMED_OUT;
214
+ }
215
+ function hookTimeout(name, seconds) {
216
+ return new SmtpError("HOOK_TIMEOUT", `${name} did not settle within hookTimeout (${seconds} s)`);
217
+ }
218
+ function refusalOf(name, answer, report) {
219
+ if (answer === undefined)
220
+ return;
221
+ const code = answer?.code;
222
+ if (typeof code === "number" && code >= 400 && code < 600) {
223
+ return answer;
224
+ }
225
+ report(new SmtpError("INVALID_HOOK_REPLY", `${name} answered ${JSON.stringify(answer)}, which is not a refusal: a hook refuses with a 4xx or 5xx reply, and accepts with undefined`));
226
+ return LOCAL_ERROR;
227
+ }
228
+
229
+ // src/server/auth.ts
230
+ var MAX_AUTH_FAILURES = 3;
231
+ async function check(connection, credentials) {
232
+ const { state, settings } = connection;
233
+ state.waiting = "command";
234
+ state.loginUser = undefined;
235
+ if (credentials === undefined) {
236
+ return connection.fail(reply(501, "5.5.2", "Cannot decode the response"));
237
+ }
238
+ if (credentials.authorizationId !== undefined && credentials.authorizationId !== credentials.username) {
239
+ return refuse(connection);
240
+ }
241
+ const { authenticate } = settings.options;
242
+ const answer = await connection.check("authenticate", () => authenticate?.(credentials, connection.session));
243
+ if (answer.failed) {
244
+ return connection.send(reply(454, "4.7.0", "Temporary authentication failure"));
245
+ }
246
+ const ok = answer.value === true;
247
+ if (ok) {
248
+ state.user = credentials.username;
249
+ return connection.send(reply(235, "2.7.0", "Authentication successful"));
250
+ }
251
+ refuse(connection);
252
+ }
253
+ function refuse(connection) {
254
+ const { state, settings } = connection;
255
+ state.authFailures++;
256
+ if (state.authFailures >= MAX_AUTH_FAILURES) {
257
+ const { hostname } = settings.options;
258
+ connection.close(reply(421, "4.7.0", `${hostname} Too many failed authentications, closing`));
259
+ return;
260
+ }
261
+ connection.fail(reply(535, "5.7.8", "Authentication credentials invalid"));
262
+ }
263
+ async function authenticate(connection, argument) {
264
+ const { state, settings } = connection;
265
+ if (!settings.options.authenticate)
266
+ return connection.fail(reply(502, "5.5.1", "AUTH not available"));
267
+ if (!state.secure)
268
+ return connection.fail(reply(538, "5.7.11", "Encryption required for requested authentication mechanism"));
269
+ if (state.helo === undefined || !state.esmtp)
270
+ return connection.fail(reply(503, "5.5.1", "Send EHLO first"));
271
+ if (state.user !== undefined)
272
+ return connection.fail(reply(503, "5.5.1", "Already authenticated"));
273
+ if (state.transaction.from !== undefined)
274
+ return connection.fail(reply(503, "5.5.1", "AUTH not allowed during a transaction"));
275
+ const [mechanism = "", initial, extra] = argument.split(" ");
276
+ if (extra !== undefined)
277
+ return connection.fail(reply(501, "5.5.4", "Syntax: AUTH mechanism [initial-response]"));
278
+ switch (mechanism.toUpperCase()) {
279
+ case "PLAIN":
280
+ if (initial === undefined) {
281
+ state.waiting = "auth-plain";
282
+ return connection.send(reply(334, undefined, ""));
283
+ }
284
+ return check(connection, decodePlain(initial === "=" ? "" : initial));
285
+ case "LOGIN":
286
+ if (initial === undefined) {
287
+ state.waiting = "auth-login-user";
288
+ return connection.send(reply(334, undefined, "VXNlcm5hbWU6"));
289
+ }
290
+ return loginUser(connection, initial);
291
+ default:
292
+ return connection.fail(reply(504, "5.5.4", "Unrecognized authentication type"));
293
+ }
294
+ }
295
+ function loginUser(connection, response) {
296
+ const user = decodeLoginStep(response);
297
+ if (!user)
298
+ return check(connection, undefined);
299
+ connection.state.loginUser = user;
300
+ connection.state.waiting = "auth-login-password";
301
+ connection.send(reply(334, undefined, "UGFzc3dvcmQ6"));
302
+ }
303
+ async function continueAuth(connection, line) {
304
+ const { state } = connection;
305
+ if (line === "*") {
306
+ state.waiting = "command";
307
+ state.loginUser = undefined;
308
+ return connection.fail(reply(501, "5.0.0", "Authentication cancelled"));
309
+ }
310
+ switch (state.waiting) {
311
+ case "auth-plain":
312
+ return check(connection, decodePlain(line));
313
+ case "auth-login-user":
314
+ return loginUser(connection, line);
315
+ default: {
316
+ const password = decodeLoginStep(line);
317
+ const username = state.loginUser ?? "";
318
+ return check(connection, password ? { mechanism: "LOGIN", username, password } : undefined);
319
+ }
320
+ }
321
+ }
322
+
323
+ // src/server/state.ts
324
+ var emptyTransaction = () => ({
325
+ to: [],
326
+ smtputf8: false,
327
+ body: "7BIT"
328
+ });
329
+
330
+ // src/server/transaction.ts
331
+ function nonAscii(text) {
332
+ for (let i = 0;i < text.length; i++)
333
+ if (text.charCodeAt(i) > 127)
334
+ return true;
335
+ return false;
336
+ }
337
+ async function mail(connection, argument) {
338
+ const { state, settings } = connection;
339
+ if (state.helo === undefined)
340
+ return connection.fail(reply(503, "5.5.1", "Send EHLO first"));
341
+ if (state.transaction.from !== undefined) {
342
+ return connection.fail(reply(503, "5.5.1", "Nested MAIL command"));
343
+ }
344
+ if (settings.mode === "submission" && state.user === undefined) {
345
+ return connection.fail(reply(530, "5.7.0", "Authentication required"));
346
+ }
347
+ const command = parsePathCommand(argument, "FROM");
348
+ if (!command)
349
+ return connection.fail(reply(501, "5.5.4", "Syntax: MAIL FROM:<address>"));
350
+ const { path, parameters } = command;
351
+ let body = "7BIT";
352
+ let smtputf8 = false;
353
+ for (const [key, value] of Object.entries(parameters)) {
354
+ if (!state.esmtp)
355
+ return connection.fail(reply(555, "5.5.4", `${key} needs EHLO`));
356
+ if (key === "SIZE") {
357
+ if (!/^\d+$/.test(value))
358
+ return connection.fail(reply(501, "5.5.4", "Syntax: SIZE=<bytes>"));
359
+ if (Number(value) > settings.maxMessageSize) {
360
+ return connection.fail(reply(552, "5.3.4", "Message too big for system"));
361
+ }
362
+ } else if (key === "BODY") {
363
+ const kind = value.toUpperCase();
364
+ if (kind !== "7BIT" && kind !== "8BITMIME") {
365
+ return connection.fail(reply(501, "5.5.4", "BODY is 7BIT or 8BITMIME"));
366
+ }
367
+ body = kind;
368
+ } else if (key === "SMTPUTF8" && value === "") {
369
+ smtputf8 = true;
370
+ } else if (key === "AUTH") {} else {
371
+ return connection.fail(reply(555, "5.5.4", `${key} is not supported`));
372
+ }
373
+ }
374
+ if (!smtputf8 && nonAscii(path.address)) {
375
+ return connection.fail(reply(553, "5.6.7", "A non-ASCII address needs SMTPUTF8"));
376
+ }
377
+ const options = settings.options;
378
+ const refused = options.onMailFrom ? await connection.hook("onMailFrom", () => options.onMailFrom?.(path, connection.session)) : undefined;
379
+ if (refused)
380
+ return connection.send(refused);
381
+ state.transaction = { from: path.address, to: [], smtputf8, body };
382
+ connection.send(reply(250, "2.1.0", "OK"));
383
+ }
384
+ async function rcpt(connection, argument) {
385
+ const { state, settings } = connection;
386
+ if (state.transaction.from === undefined)
387
+ return connection.fail(reply(503, "5.5.1", "Send MAIL first"));
388
+ const command = parsePathCommand(argument, "TO");
389
+ if (!command)
390
+ return connection.fail(reply(501, "5.5.4", "Syntax: RCPT TO:<address>"));
391
+ const { path, parameters } = command;
392
+ if (Object.keys(parameters).length > 0) {
393
+ return connection.fail(reply(555, "5.5.4", `${Object.keys(parameters)[0]} is not supported`));
394
+ }
395
+ if (!state.transaction.smtputf8 && nonAscii(path.address)) {
396
+ return connection.fail(reply(553, "5.6.7", "A non-ASCII address needs SMTPUTF8"));
397
+ }
398
+ if (state.transaction.to.length >= settings.maxRecipients) {
399
+ return connection.send(reply(452, "4.5.3", "Too many recipients"));
400
+ }
401
+ if (state.user === undefined) {
402
+ const local = await connection.check("localDomains", () => settings.isLocal(path.domain));
403
+ if (local.failed)
404
+ return connection.send(LOCAL_ERROR);
405
+ if (local.value !== true) {
406
+ return connection.fail(reply(554, "5.7.1", "Relay access denied"));
407
+ }
408
+ }
409
+ const options = settings.options;
410
+ const refused = options.onRcptTo ? await connection.hook("onRcptTo", () => options.onRcptTo?.(path, connection.session)) : undefined;
411
+ if (refused)
412
+ return connection.send(refused);
413
+ state.transaction.to.push(path.address);
414
+ connection.send(reply(250, "2.1.5", "OK"));
415
+ }
416
+
417
+ // src/server/commands.ts
418
+ function ehloLines(connection) {
419
+ const { settings, state } = connection;
420
+ const lines = [
421
+ `${settings.options.hostname} greets ${state.helo}`,
422
+ "PIPELINING",
423
+ `SIZE ${settings.maxMessageSize}`,
424
+ "8BITMIME",
425
+ "SMTPUTF8",
426
+ "ENHANCEDSTATUSCODES"
427
+ ];
428
+ if (settings.options.tls && !state.secure)
429
+ lines.push("STARTTLS");
430
+ if (settings.options.authenticate && state.secure && state.user === undefined) {
431
+ lines.push("AUTH PLAIN LOGIN");
432
+ }
433
+ return lines;
434
+ }
435
+ async function hello(connection, argument, esmtp) {
436
+ if (!/^(?:[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*|\[[0-9A-Fa-f:.Iv]+\])$/.test(argument)) {
437
+ connection.fail(reply(501, "5.5.4", `Syntax: ${esmtp ? "EHLO" : "HELO"} hostname`));
438
+ return;
439
+ }
440
+ const { state } = connection;
441
+ state.helo = argument;
442
+ state.esmtp = esmtp;
443
+ state.transaction = emptyTransaction();
444
+ connection.send(esmtp ? reply(250, undefined, ehloLines(connection)) : reply(250, undefined, `${connection.settings.options.hostname} greets ${argument}`));
445
+ }
446
+ function data(connection, argument) {
447
+ const { state } = connection;
448
+ const refusal = argument !== "" ? reply(501, "5.5.4", "Syntax: DATA") : state.transaction.from === undefined ? reply(503, "5.5.1", "Send MAIL first") : state.transaction.to.length === 0 ? reply(554, "5.5.1", "No valid recipients") : undefined;
449
+ if (refusal) {
450
+ connection.fail(refusal);
451
+ connection.input.drop();
452
+ return;
453
+ }
454
+ connection.send(reply(354, undefined, "End data with <CR><LF>.<CR><LF>"));
455
+ connection.input.beginData();
456
+ }
457
+ async function startTls(connection, argument) {
458
+ const { state, settings } = connection;
459
+ const refusal = !settings.options.tls ? reply(454, "4.7.0", "TLS not available") : state.secure ? reply(503, "5.5.1", "TLS already active") : argument !== "" ? reply(501, "5.5.4", "Syntax: STARTTLS") : undefined;
460
+ if (refusal) {
461
+ connection.fail(refusal);
462
+ return;
463
+ }
464
+ connection.send(reply(220, "2.0.0", "Ready to start TLS"));
465
+ connection.input.ignore();
466
+ await connection.transport.drained();
467
+ if (connection.closed)
468
+ return;
469
+ connection.transport.startTls();
470
+ connection.input.accept();
471
+ state.secure = true;
472
+ state.helo = undefined;
473
+ state.esmtp = false;
474
+ state.transaction = emptyTransaction();
475
+ }
476
+ async function runCommand(connection, line) {
477
+ const { state } = connection;
478
+ if (state.waiting !== "command")
479
+ return continueAuth(connection, line);
480
+ const { verb, argument } = parseCommand(line);
481
+ switch (verb) {
482
+ case "EHLO":
483
+ return hello(connection, argument, true);
484
+ case "HELO":
485
+ return hello(connection, argument, false);
486
+ case "MAIL":
487
+ return mail(connection, argument);
488
+ case "RCPT":
489
+ return rcpt(connection, argument);
490
+ case "DATA":
491
+ return data(connection, argument);
492
+ case "RSET":
493
+ state.transaction = emptyTransaction();
494
+ return connection.send(reply(250, "2.0.0", "OK"));
495
+ case "NOOP":
496
+ return connection.send(reply(250, "2.0.0", "OK"));
497
+ case "VRFY":
498
+ return connection.send(reply(252, "2.5.0", "Cannot VRFY user; send the message and it will be tried"));
499
+ case "HELP":
500
+ return connection.send(reply(214, "2.0.0", "See RFC 5321"));
501
+ case "QUIT":
502
+ return connection.close(reply(221, "2.0.0", `${connection.settings.options.hostname} closing connection`));
503
+ case "STARTTLS":
504
+ return startTls(connection, argument);
505
+ case "AUTH":
506
+ return authenticate(connection, argument);
507
+ default:
508
+ return connection.fail(reply(500, "5.5.2", "Command unrecognized"));
509
+ }
510
+ }
511
+
512
+ // src/server/received.ts
513
+ var DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
514
+ var MONTHS = [
515
+ "Jan",
516
+ "Feb",
517
+ "Mar",
518
+ "Apr",
519
+ "May",
520
+ "Jun",
521
+ "Jul",
522
+ "Aug",
523
+ "Sep",
524
+ "Oct",
525
+ "Nov",
526
+ "Dec"
527
+ ];
528
+ var two = (n) => String(n).padStart(2, "0");
529
+ function dateTime(date) {
530
+ return `${DAYS[date.getUTCDay()]}, ${date.getUTCDate()} ${MONTHS[date.getUTCMonth()]} ${date.getUTCFullYear()} ${two(date.getUTCHours())}:${two(date.getUTCMinutes())}:${two(date.getUTCSeconds())} +0000`;
531
+ }
532
+ function protocolOf(esmtp, secure, authenticated) {
533
+ if (!esmtp)
534
+ return "SMTP";
535
+ return `ESMTP${secure ? "S" : ""}${authenticated ? "A" : ""}`;
536
+ }
537
+ function printable(text) {
538
+ let out = "";
539
+ for (const char of text) {
540
+ const code = char.charCodeAt(0);
541
+ out += code > 32 && code < 127 ? char : "?";
542
+ }
543
+ return out;
544
+ }
545
+ function plain(address) {
546
+ return printable(address) === address && !address.includes(";");
547
+ }
548
+ function receivedField(connection, recipients, id, now = new Date) {
549
+ const { state, transport, settings } = connection;
550
+ const lines = [
551
+ `Received: from ${printable(state.helo ?? "unknown")} ([${printable(transport.remoteAddress)}])`,
552
+ ` by ${settings.options.hostname} with ${protocolOf(state.esmtp, state.secure, state.user !== undefined)} id ${id}`
553
+ ];
554
+ const [recipient] = recipients;
555
+ if (recipients.length === 1 && recipient !== undefined && plain(recipient)) {
556
+ lines.push(` for <${recipient}>`);
557
+ }
558
+ lines[lines.length - 1] += `; ${dateTime(now)}`;
559
+ return `${lines.join(`\r
560
+ `)}\r
561
+ `;
562
+ }
563
+
564
+ // src/server/refusals.ts
565
+ var TOO_BIG = reply(552, "5.3.4", "Message too big for system");
566
+ var BARE_LINE_BREAK = reply(550, "5.6.11", "Bare CR or LF is not allowed in a message");
567
+ function tooBig(maxMessageSize) {
568
+ return new SmtpError("MESSAGE_TOO_BIG", `The message is larger than maxMessageSize (${maxMessageSize} bytes); do not deliver it`);
569
+ }
570
+ function bareLineBreak() {
571
+ return new SmtpError("BARE_LINE_BREAK", "The message holds a bare CR or LF (SMTP smuggling); do not deliver it");
572
+ }
573
+ function connectionLost(ended) {
574
+ return new SmtpError("CONNECTION_LOST", ended ? "The connection closed before the reply to the message; do not deliver it" : "The connection closed before the end of the message; do not deliver it");
575
+ }
576
+ function notAnswered(seconds) {
577
+ return new SmtpError("HOOK_TIMEOUT", `onData did not answer within hookTimeout (${seconds} s); do not deliver it`);
578
+ }
579
+ function notReadInTime(seconds) {
580
+ return new SmtpError("HOOK_TIMEOUT", `onData did not read the message within hookTimeout (${seconds} s); do not deliver it`);
581
+ }
582
+ function notRead() {
583
+ return new SmtpError("MESSAGE_NOT_READ", "onData answered without reading the message to its end; it was not taken");
584
+ }
585
+
586
+ // src/server/intake.ts
587
+ var HIGH_WATER_MARK = 64 * 1024;
588
+
589
+ class Intake {
590
+ id = crypto.getRandomValues(new Uint8Array(10)).toHex();
591
+ #connection;
592
+ #reader = new DataReader;
593
+ #controller;
594
+ #queue = [];
595
+ #queued = 0;
596
+ #ended = false;
597
+ #read = false;
598
+ #state = "open";
599
+ #failure;
600
+ #wakers = [];
601
+ #delivery;
602
+ #readWhenAnswered = false;
603
+ #refusal = new AbortController;
604
+ constructor(connection, transaction) {
605
+ this.#connection = connection;
606
+ const content = new ReadableStream({
607
+ start: (controller) => {
608
+ this.#controller = controller;
609
+ },
610
+ pull: (controller) => this.#pull(controller),
611
+ cancel: () => {
612
+ if (this.#state === "open")
613
+ this.#state = "cancelled";
614
+ this.#queue = [];
615
+ this.#queued = 0;
616
+ this.#wake();
617
+ }
618
+ }, { highWaterMark: 0 });
619
+ this.#push(new TextEncoder().encode(receivedField(connection, transaction.to, this.id)));
620
+ const message = {
621
+ id: this.id,
622
+ envelope: {
623
+ from: transaction.from ?? "",
624
+ to: [...transaction.to],
625
+ smtputf8: transaction.smtputf8,
626
+ body: transaction.body
627
+ },
628
+ content,
629
+ signal: this.#refusal.signal
630
+ };
631
+ this.#delivery = this.#deliver(message);
632
+ }
633
+ async#deliver(message) {
634
+ const connection = this.#connection;
635
+ let ownRefusal = false;
636
+ const refuse = (error) => {
637
+ this.#refusal.abort(error);
638
+ connection.report(error);
639
+ };
640
+ try {
641
+ await Promise.resolve();
642
+ const answer = await connection.settings.options.onData(message, connection.session);
643
+ const refusal = refusalOf("onData", answer, refuse);
644
+ ownRefusal = refusal !== undefined && refusal === answer;
645
+ return refusal;
646
+ } catch (error) {
647
+ refuse(error);
648
+ return LOCAL_ERROR;
649
+ } finally {
650
+ this.#readWhenAnswered = this.#read;
651
+ if (!this.#read)
652
+ this.#stop(!ownRefusal);
653
+ this.#wake();
654
+ }
655
+ }
656
+ async write(chunk) {
657
+ const { data, done, rest } = this.#reader.write(chunk);
658
+ const { maxMessageSize } = this.#connection.settings;
659
+ if (this.#failure)
660
+ return { done, rest };
661
+ if (this.#reader.size > maxMessageSize) {
662
+ this.#fail(TOO_BIG, tooBig(maxMessageSize));
663
+ } else if (this.#reader.bareLineBreaks > 0) {
664
+ this.#fail(BARE_LINE_BREAK, bareLineBreak());
665
+ } else if (this.#state === "open" && data.length > 0) {
666
+ this.#push(data);
667
+ await this.#room();
668
+ }
669
+ return { done, rest };
670
+ }
671
+ async finish() {
672
+ if (this.#failure)
673
+ return this.#failure;
674
+ this.#ended = true;
675
+ this.#wake();
676
+ const { hookTimeout: seconds } = this.#connection.settings;
677
+ const answer = await within(this.#delivery, seconds);
678
+ if (timedOut(answer)) {
679
+ this.#connection.report(hookTimeout("onData", seconds));
680
+ this.#fail(LOCAL_ERROR, notAnswered(seconds));
681
+ return LOCAL_ERROR;
682
+ }
683
+ if (answer)
684
+ return answer;
685
+ if (!this.#readWhenAnswered) {
686
+ const error = notRead();
687
+ this.#refusal.abort(error);
688
+ this.#connection.report(error);
689
+ return LOCAL_ERROR;
690
+ }
691
+ return reply(250, "2.0.0", `OK queued as ${this.id}`);
692
+ }
693
+ abort() {
694
+ this.#fail(LOCAL_ERROR, connectionLost(this.#ended));
695
+ }
696
+ #push(bytes) {
697
+ this.#queue.push(bytes);
698
+ this.#queued += bytes.length;
699
+ this.#wake();
700
+ }
701
+ async#pull(controller) {
702
+ for (;; ) {
703
+ if (this.#state !== "open")
704
+ return;
705
+ const next = this.#queue.shift();
706
+ if (next) {
707
+ this.#queued -= next.length;
708
+ controller.enqueue(next);
709
+ this.#wake();
710
+ return;
711
+ }
712
+ if (this.#ended) {
713
+ this.#read = true;
714
+ this.#state = "closed";
715
+ controller.close();
716
+ return;
717
+ }
718
+ await new Promise((wake) => this.#wakers.push(wake));
719
+ }
720
+ }
721
+ #stop(abortSignal) {
722
+ if (this.#state !== "open")
723
+ return;
724
+ const error = notRead();
725
+ if (abortSignal)
726
+ this.#refusal.abort(error);
727
+ this.#controller.error(error);
728
+ this.#state = "cancelled";
729
+ this.#queue = [];
730
+ this.#queued = 0;
731
+ }
732
+ #fail(answer, error) {
733
+ this.#failure ??= answer;
734
+ this.#refusal.abort(error);
735
+ if (this.#state === "open")
736
+ this.#controller.error(error);
737
+ this.#state = "errored";
738
+ this.#queue = [];
739
+ this.#queued = 0;
740
+ this.#wake();
741
+ }
742
+ async#room() {
743
+ const { hookTimeout: seconds } = this.#connection.settings;
744
+ while (this.#state === "open" && this.#queued > HIGH_WATER_MARK) {
745
+ const woken = await within(new Promise((wake) => this.#wakers.push(wake)), seconds);
746
+ if (timedOut(woken)) {
747
+ this.#connection.report(hookTimeout("onData", seconds));
748
+ this.#fail(LOCAL_ERROR, notReadInTime(seconds));
749
+ }
750
+ }
751
+ }
752
+ #wake() {
753
+ for (const wake of this.#wakers.splice(0))
754
+ wake();
755
+ }
756
+ }
757
+
758
+ // src/server/lines.ts
759
+ var MAX_LINE = 2048;
760
+
761
+ class LineSplitter {
762
+ #held = new Uint8Array(0);
763
+ #skipping = false;
764
+ #rest = new Uint8Array(0);
765
+ push(chunk) {
766
+ this.#rest = this.#held.length > 0 ? concat(this.#held, chunk) : chunk;
767
+ this.#held = new Uint8Array(0);
768
+ }
769
+ next() {
770
+ for (;; ) {
771
+ const data = this.#rest;
772
+ const lf = data.indexOf(10);
773
+ if (lf < 0) {
774
+ this.#rest = new Uint8Array(0);
775
+ if (this.#skipping)
776
+ return;
777
+ if (data.length > MAX_LINE) {
778
+ this.#skipping = true;
779
+ return { tooLong: true };
780
+ }
781
+ this.#held = data.slice();
782
+ return;
783
+ }
784
+ const end = lf > 0 && data[lf - 1] === 13 ? lf - 1 : lf;
785
+ this.#rest = data.subarray(lf + 1);
786
+ if (this.#skipping) {
787
+ this.#skipping = false;
788
+ continue;
789
+ }
790
+ if (end > MAX_LINE)
791
+ return { tooLong: true };
792
+ return { line: new TextDecoder().decode(data.subarray(0, end)) };
793
+ }
794
+ }
795
+ takeRest() {
796
+ const rest = this.#rest;
797
+ this.#rest = new Uint8Array(0);
798
+ return rest;
799
+ }
800
+ clear() {
801
+ this.#held = new Uint8Array(0);
802
+ this.#rest = new Uint8Array(0);
803
+ this.#skipping = false;
804
+ }
805
+ }
806
+ function concat(a, b) {
807
+ const out = new Uint8Array(a.length + b.length);
808
+ out.set(a, 0);
809
+ out.set(b, a.length);
810
+ return out;
811
+ }
812
+
813
+ // src/server/input.ts
814
+ var INPUT_LIMIT = 64 * 1024;
815
+
816
+ class Input {
817
+ #connection;
818
+ #input = [];
819
+ #queued = 0;
820
+ #paused = false;
821
+ #lines = new LineSplitter;
822
+ #intake;
823
+ #answering;
824
+ #pumping;
825
+ #greeted = false;
826
+ early = false;
827
+ #ignoring = false;
828
+ #dropped = false;
829
+ constructor(connection) {
830
+ this.#connection = connection;
831
+ }
832
+ greeted() {
833
+ this.#greeted = true;
834
+ }
835
+ receive(chunk) {
836
+ if (this.#ignoring)
837
+ return;
838
+ if (!this.#greeted) {
839
+ this.early = true;
840
+ return;
841
+ }
842
+ this.#input.push(chunk.slice());
843
+ this.#queued += chunk.length;
844
+ if (this.#queued > INPUT_LIMIT && !this.#paused) {
845
+ this.#paused = true;
846
+ this.#connection.transport.pause();
847
+ }
848
+ this.#kick();
849
+ }
850
+ abort() {
851
+ this.#intake?.abort();
852
+ this.#intake = undefined;
853
+ this.#answering?.abort();
854
+ this.#answering = undefined;
855
+ }
856
+ ignore() {
857
+ this.drop();
858
+ this.#input = [];
859
+ this.#queued = 0;
860
+ this.#ignoring = true;
861
+ this.#resume();
862
+ }
863
+ accept() {
864
+ this.#ignoring = false;
865
+ }
866
+ drop() {
867
+ this.#lines.clear();
868
+ this.#dropped = true;
869
+ }
870
+ beginData() {
871
+ const connection = this.#connection;
872
+ this.#intake = new Intake(connection, connection.state.transaction);
873
+ connection.state.waiting = "data";
874
+ }
875
+ async idle() {
876
+ while (this.#pumping)
877
+ await this.#pumping;
878
+ }
879
+ #kick() {
880
+ if (this.#pumping)
881
+ return;
882
+ const connection = this.#connection;
883
+ this.#pumping = this.#pump().catch((error) => {
884
+ connection.report(error);
885
+ connection.close(reply(421, "4.3.0", "Local error, closing"));
886
+ }).finally(() => {
887
+ this.#pumping = undefined;
888
+ if (!connection.closed && this.#input.length > 0)
889
+ this.#kick();
890
+ });
891
+ }
892
+ #resume() {
893
+ if (this.#paused && this.#queued <= INPUT_LIMIT / 2) {
894
+ this.#paused = false;
895
+ this.#connection.transport.resume();
896
+ }
897
+ }
898
+ async#pump() {
899
+ while (!this.#connection.closed && this.#input.length > 0) {
900
+ const chunk = this.#input.shift();
901
+ this.#queued -= chunk.length;
902
+ this.#resume();
903
+ if (this.#connection.state.waiting === "data")
904
+ await this.#data(chunk);
905
+ else
906
+ await this.#commands(chunk);
907
+ }
908
+ }
909
+ async#commands(chunk) {
910
+ this.#lines.push(chunk);
911
+ while (!this.#connection.closed) {
912
+ await this.#connection.transport.drained();
913
+ const event = this.#lines.next();
914
+ if (!event)
915
+ return;
916
+ if (!("line" in event)) {
917
+ this.#connection.fail(reply(500, "5.5.6", "Line too long"));
918
+ continue;
919
+ }
920
+ this.#dropped = false;
921
+ await runCommand(this.#connection, event.line);
922
+ if (this.#dropped)
923
+ return;
924
+ if (this.#connection.state.waiting === "data") {
925
+ const rest = this.#lines.takeRest();
926
+ if (rest.length > 0)
927
+ this.#unshift(rest.slice());
928
+ return;
929
+ }
930
+ }
931
+ }
932
+ async#data(chunk) {
933
+ const intake = this.#intake;
934
+ const { done, rest } = await intake.write(chunk);
935
+ if (!done || this.#connection.closed)
936
+ return;
937
+ this.#intake = undefined;
938
+ this.#answering = intake;
939
+ this.#connection.state.waiting = "command";
940
+ this.#connection.state.transaction = emptyTransaction();
941
+ const answer = await intake.finish();
942
+ this.#answering = undefined;
943
+ this.#connection.send(answer);
944
+ if (rest.length > 0)
945
+ this.#unshift(rest);
946
+ }
947
+ #unshift(bytes) {
948
+ this.#input.unshift(bytes);
949
+ this.#queued += bytes.length;
950
+ if (this.#queued > INPUT_LIMIT && !this.#paused) {
951
+ this.#paused = true;
952
+ this.#connection.transport.pause();
953
+ }
954
+ }
955
+ }
956
+
957
+ // src/server/connection.ts
958
+ class Connection {
959
+ settings;
960
+ #transport;
961
+ id = crypto.getRandomValues(new Uint8Array(8)).toHex();
962
+ data = {};
963
+ state;
964
+ input = new Input(this);
965
+ #closed = false;
966
+ constructor(settings, transport) {
967
+ this.settings = settings;
968
+ this.#transport = transport;
969
+ this.state = {
970
+ esmtp: false,
971
+ secure: transport.secure,
972
+ transaction: emptyTransaction(),
973
+ waiting: "command",
974
+ errors: 0,
975
+ authFailures: 0
976
+ };
977
+ }
978
+ get transport() {
979
+ return this.#transport;
980
+ }
981
+ useTransport(transport) {
982
+ this.#transport = transport;
983
+ }
984
+ get session() {
985
+ const { helo, user } = this.state;
986
+ return {
987
+ id: this.id,
988
+ remoteAddress: this.transport.remoteAddress,
989
+ secure: this.state.secure,
990
+ esmtp: this.state.esmtp,
991
+ ...helo === undefined ? {} : { helo },
992
+ ...user === undefined ? {} : { user },
993
+ data: this.data
994
+ };
995
+ }
996
+ get closed() {
997
+ return this.#closed;
998
+ }
999
+ send(answer) {
1000
+ if (this.#closed)
1001
+ return;
1002
+ this.transport.write(formatReply(answer, this.state.esmtp));
1003
+ }
1004
+ close(answer) {
1005
+ if (this.#closed)
1006
+ return;
1007
+ if (answer)
1008
+ this.send(answer);
1009
+ this.#closed = true;
1010
+ this.input.abort();
1011
+ this.transport.end();
1012
+ }
1013
+ fail(answer) {
1014
+ this.state.errors++;
1015
+ if (this.state.errors >= this.settings.maxErrors) {
1016
+ const { hostname } = this.settings.options;
1017
+ this.close(reply(421, "4.7.0", `${hostname} Too many errors, closing`));
1018
+ } else {
1019
+ this.send(answer);
1020
+ }
1021
+ }
1022
+ report(error) {
1023
+ try {
1024
+ this.settings.options.onError?.(error, this.session);
1025
+ } catch {}
1026
+ }
1027
+ async hook(name, run, deadline = true) {
1028
+ const answer = await this.check(name, run, deadline);
1029
+ if (answer.failed)
1030
+ return LOCAL_ERROR;
1031
+ return refusalOf(name, answer.value, (error) => this.report(error));
1032
+ }
1033
+ async check(name, run, deadline = true) {
1034
+ const seconds = this.settings.hookTimeout;
1035
+ try {
1036
+ const running = Promise.resolve().then(run);
1037
+ const value = deadline ? await within(running, seconds) : await running;
1038
+ if (timedOut(value)) {
1039
+ this.report(hookTimeout(name, seconds));
1040
+ return { failed: true };
1041
+ }
1042
+ return { failed: false, value };
1043
+ } catch (error) {
1044
+ this.report(error);
1045
+ return { failed: true };
1046
+ }
1047
+ }
1048
+ async open() {
1049
+ const { options } = this.settings;
1050
+ const refused = options.onConnect ? await this.hook("onConnect", () => options.onConnect?.(this.session)) : undefined;
1051
+ if (refused)
1052
+ return this.close(refused);
1053
+ const { greetingDelay } = this.settings;
1054
+ if (greetingDelay > 0)
1055
+ await Bun.sleep(greetingDelay * 1000);
1056
+ if (this.#closed)
1057
+ return;
1058
+ if (this.input.early) {
1059
+ return this.close(reply(554, "5.5.0", `${options.hostname} Talked before the greeting`));
1060
+ }
1061
+ this.send(reply(220, undefined, `${options.hostname} ESMTP ready`));
1062
+ this.transport.restartIdle(this.settings.timeout);
1063
+ this.input.greeted();
1064
+ }
1065
+ receive(chunk) {
1066
+ if (!this.#closed)
1067
+ this.input.receive(chunk);
1068
+ }
1069
+ idle() {
1070
+ return this.input.idle();
1071
+ }
1072
+ }
1073
+
1074
+ // src/server/settings.ts
1075
+ var invalid = (message) => new SmtpError("INVALID_OPTION", `createSmtpServer(): ${message}`);
1076
+ function positive(name, value, fallback) {
1077
+ if (value === undefined)
1078
+ return fallback;
1079
+ if (!Number.isSafeInteger(value) || value < 1) {
1080
+ throw invalid(`${name} must be a positive integer, not ${value}`);
1081
+ }
1082
+ return value;
1083
+ }
1084
+ function nonNegative(name, value, fallback) {
1085
+ if (value === undefined)
1086
+ return fallback;
1087
+ if (!Number.isFinite(value) || value < 0) {
1088
+ throw invalid(`${name} must be a number of seconds, 0 or more, not ${value}`);
1089
+ }
1090
+ return value;
1091
+ }
1092
+ function localLookup(local) {
1093
+ if (typeof local === "function")
1094
+ return local;
1095
+ if (Array.isArray(local) && local.every((domain) => typeof domain === "string")) {
1096
+ const domains = new Set(local.map((domain) => domain.toLowerCase()));
1097
+ return (domain) => domains.has(domain);
1098
+ }
1099
+ throw invalid("localDomains must be an array of domains or a function");
1100
+ }
1101
+ function settingsOf(options) {
1102
+ if (typeof options.hostname !== "string" || !/^[A-Za-z0-9.-]+$/.test(options.hostname)) {
1103
+ throw invalid(`"${options.hostname}" is not a host name`);
1104
+ }
1105
+ if (typeof options.onData !== "function") {
1106
+ throw invalid("onData must be a function: it is where messages go");
1107
+ }
1108
+ if (options.implicitTls && !options.tls) {
1109
+ throw invalid("implicitTls needs tls: { key, cert }");
1110
+ }
1111
+ if (options.mode === "submission" && !options.authenticate) {
1112
+ throw invalid("submission takes mail only from authenticated users, so it needs authenticate");
1113
+ }
1114
+ if (options.authenticate && !options.tls) {
1115
+ throw invalid("authenticate needs tls: { key, cert }, since AUTH is offered only once encrypted");
1116
+ }
1117
+ const isLocal = localLookup(options.localDomains);
1118
+ const timeout = positive("timeout", options.timeout, 300);
1119
+ const greetingDelay = nonNegative("greetingDelay", options.greetingDelay, 0);
1120
+ if (greetingDelay >= timeout) {
1121
+ throw invalid(`greetingDelay (${greetingDelay} s) must be shorter than timeout (${timeout} s), or every client times out before the greeting`);
1122
+ }
1123
+ return {
1124
+ options,
1125
+ mode: options.mode ?? "mx",
1126
+ maxMessageSize: positive("maxMessageSize", options.maxMessageSize, 25 * 1024 * 1024),
1127
+ maxRecipients: positive("maxRecipients", options.maxRecipients, 100),
1128
+ maxConnections: positive("maxConnections", options.maxConnections, 1000),
1129
+ maxErrors: positive("maxErrors", options.maxErrors, 10),
1130
+ timeout,
1131
+ hookTimeout: positive("hookTimeout", options.hookTimeout, 60),
1132
+ greetingDelay,
1133
+ isLocal: (domain) => isLocal(domain.toLowerCase())
1134
+ };
1135
+ }
1136
+
1137
+ // src/server/transport.ts
1138
+ class SocketTransport {
1139
+ #socket;
1140
+ remoteAddress;
1141
+ secure;
1142
+ #startTls;
1143
+ #queue = [];
1144
+ #waiters = [];
1145
+ #ending = false;
1146
+ constructor(socket, secure, startTls, remoteAddress = socket.remoteAddress) {
1147
+ this.#socket = socket;
1148
+ this.secure = secure;
1149
+ this.#startTls = startTls;
1150
+ this.remoteAddress = remoteAddress;
1151
+ }
1152
+ write(text) {
1153
+ const bytes = new TextEncoder().encode(text);
1154
+ if (this.#queue.length > 0) {
1155
+ this.#queue.push(bytes);
1156
+ return;
1157
+ }
1158
+ const written = this.#socket.write(bytes);
1159
+ if (written < bytes.length)
1160
+ this.#queue.push(bytes.subarray(written));
1161
+ }
1162
+ drain() {
1163
+ while (this.#queue.length > 0) {
1164
+ const bytes = this.#queue[0];
1165
+ const written = this.#socket.write(bytes);
1166
+ if (written < bytes.length) {
1167
+ this.#queue[0] = bytes.subarray(written);
1168
+ return;
1169
+ }
1170
+ this.#queue.shift();
1171
+ }
1172
+ for (const wake of this.#waiters.splice(0))
1173
+ wake();
1174
+ if (this.#ending)
1175
+ this.#socket.end();
1176
+ }
1177
+ drained() {
1178
+ if (this.#queue.length === 0)
1179
+ return Promise.resolve();
1180
+ return new Promise((wake) => this.#waiters.push(wake));
1181
+ }
1182
+ closed() {
1183
+ this.#queue = [];
1184
+ for (const wake of this.#waiters.splice(0))
1185
+ wake();
1186
+ }
1187
+ end() {
1188
+ if (this.#queue.length === 0)
1189
+ this.#socket.end();
1190
+ else
1191
+ this.#ending = true;
1192
+ }
1193
+ pause() {
1194
+ this.#socket.pause();
1195
+ }
1196
+ resume() {
1197
+ this.#socket.resume();
1198
+ }
1199
+ startTls() {
1200
+ this.#startTls();
1201
+ }
1202
+ restartIdle(seconds) {
1203
+ this.#socket.timeout(seconds);
1204
+ }
1205
+ }
1206
+
1207
+ // src/server/server.ts
1208
+ function handlers(settings) {
1209
+ const idle = reply(421, "4.4.2", `${settings.options.hostname} Idle too long, closing`);
1210
+ return {
1211
+ data(socket, chunk) {
1212
+ if (socket.data.upgraded)
1213
+ return;
1214
+ socket.timeout(settings.timeout);
1215
+ socket.data.connection?.receive(chunk);
1216
+ },
1217
+ drain(socket) {
1218
+ if (!socket.data.upgraded)
1219
+ socket.data.transport?.drain();
1220
+ },
1221
+ error(socket) {
1222
+ socket.data.connection?.close();
1223
+ },
1224
+ timeout(socket) {
1225
+ if (socket.data.upgraded)
1226
+ return;
1227
+ socket.data.connection?.close(idle);
1228
+ }
1229
+ };
1230
+ }
1231
+ function transportOf(socket, settings, secure) {
1232
+ const transport = new SocketTransport(socket, secure, () => {
1233
+ const tls = settings.options.tls;
1234
+ const connection = socket.data.connection;
1235
+ if (!tls || !connection)
1236
+ return;
1237
+ socket.data.upgraded = true;
1238
+ const [, encrypted] = socket.upgradeTLS({
1239
+ tls: { key: tls.key, cert: tls.cert },
1240
+ data: { connection, upgraded: false },
1241
+ socket: {
1242
+ ...handlers(settings),
1243
+ close: (s) => {
1244
+ s.data.transport?.closed();
1245
+ connection.close();
1246
+ }
1247
+ }
1248
+ });
1249
+ encrypted.timeout(settings.timeout);
1250
+ const next = new SocketTransport(encrypted, true, () => {}, socket.remoteAddress);
1251
+ encrypted.data.transport = next;
1252
+ connection.useTransport(next);
1253
+ });
1254
+ socket.data.transport = transport;
1255
+ return transport;
1256
+ }
1257
+ function createSmtpServer(options) {
1258
+ const settings = settingsOf(options);
1259
+ let listener;
1260
+ let open = 0;
1261
+ const secure = options.implicitTls === true;
1262
+ return {
1263
+ get connections() {
1264
+ return open;
1265
+ },
1266
+ async listen({ port, hostname = "0.0.0.0" }) {
1267
+ if (listener) {
1268
+ throw new SmtpError("ALREADY_LISTENING", `listen(): the server is already listening on ${listener.hostname}:${listener.port}`);
1269
+ }
1270
+ listener = Bun.listen({
1271
+ hostname,
1272
+ port,
1273
+ ...secure && options.tls ? { tls: { key: options.tls.key, cert: options.tls.cert } } : {},
1274
+ socket: {
1275
+ ...handlers(settings),
1276
+ open(socket) {
1277
+ socket.data = { upgraded: false };
1278
+ socket.timeout(settings.timeout);
1279
+ if (open >= settings.maxConnections) {
1280
+ socket.end(`421 4.3.2 ${options.hostname} Too many connections, try later\r
1281
+ `);
1282
+ return;
1283
+ }
1284
+ open++;
1285
+ const connection = new Connection(settings, transportOf(socket, settings, secure));
1286
+ socket.data.connection = connection;
1287
+ connection.open();
1288
+ },
1289
+ close(socket) {
1290
+ if (socket.data.connection)
1291
+ open--;
1292
+ socket.data.transport?.closed();
1293
+ socket.data.connection?.close();
1294
+ }
1295
+ }
1296
+ });
1297
+ return { port: listener.port, hostname: listener.hostname };
1298
+ },
1299
+ stop(closeConnections = false) {
1300
+ listener?.stop(closeConnections);
1301
+ listener = undefined;
1302
+ }
1303
+ };
1304
+ }
1305
+ export {
1306
+ DataReader,
1307
+ SmtpError,
1308
+ createSmtpServer,
1309
+ decodeLoginStep,
1310
+ decodePlain,
1311
+ formatReply,
1312
+ parseCommand,
1313
+ parsePath,
1314
+ parsePathCommand,
1315
+ reply
1316
+ };
1317
+
1318
+ //# debugId=DE4D9903D05BDADC64756E2164756E21
1319
+ //# sourceMappingURL=index.js.map