@bumail/smtp 0.1.0 → 0.2.1

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 (65) hide show
  1. package/README.md +149 -7
  2. package/dist/chunks/index-3423jxf9.js +249 -0
  3. package/dist/chunks/index-3423jxf9.js.map +14 -0
  4. package/dist/client/content.d.ts +25 -0
  5. package/dist/client/content.d.ts.map +1 -0
  6. package/dist/client/inbox.d.ts +18 -0
  7. package/dist/client/inbox.d.ts.map +1 -0
  8. package/dist/client/index.d.ts +6 -0
  9. package/dist/client/index.d.ts.map +1 -0
  10. package/dist/client/index.js +948 -0
  11. package/dist/client/index.js.map +22 -0
  12. package/dist/client/mx.d.ts +29 -0
  13. package/dist/client/mx.d.ts.map +1 -0
  14. package/dist/client/options.d.ts +136 -0
  15. package/dist/client/options.d.ts.map +1 -0
  16. package/dist/client/refusal.d.ts +15 -0
  17. package/dist/client/refusal.d.ts.map +1 -0
  18. package/dist/client/send.d.ts +13 -0
  19. package/dist/client/send.d.ts.map +1 -0
  20. package/dist/client/session.d.ts +11 -0
  21. package/dist/client/session.d.ts.map +1 -0
  22. package/dist/client/settings.d.ts +30 -0
  23. package/dist/client/settings.d.ts.map +1 -0
  24. package/dist/client/socket.d.ts +31 -0
  25. package/dist/client/socket.d.ts.map +1 -0
  26. package/dist/client/target.d.ts +29 -0
  27. package/dist/client/target.d.ts.map +1 -0
  28. package/dist/client/transaction.d.ts +15 -0
  29. package/dist/client/transaction.d.ts.map +1 -0
  30. package/dist/errors.d.ts +28 -4
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +164 -197
  35. package/dist/index.js.map +9 -13
  36. package/dist/io/outgoing.d.ts +21 -0
  37. package/dist/io/outgoing.d.ts.map +1 -0
  38. package/dist/protocol/data-writer.d.ts +22 -0
  39. package/dist/protocol/data-writer.d.ts.map +1 -0
  40. package/dist/protocol/ehlo.d.ts +7 -0
  41. package/dist/protocol/ehlo.d.ts.map +1 -0
  42. package/dist/{server → protocol}/lines.d.ts +9 -4
  43. package/dist/protocol/lines.d.ts.map +1 -0
  44. package/dist/protocol/path.d.ts +24 -3
  45. package/dist/protocol/path.d.ts.map +1 -1
  46. package/dist/protocol/reply-reader.d.ts +19 -0
  47. package/dist/protocol/reply-reader.d.ts.map +1 -0
  48. package/dist/protocol/sasl.d.ts +4 -0
  49. package/dist/protocol/sasl.d.ts.map +1 -1
  50. package/dist/server/commands.d.ts.map +1 -1
  51. package/dist/server/connection.d.ts +8 -0
  52. package/dist/server/connection.d.ts.map +1 -1
  53. package/dist/server/input.d.ts.map +1 -1
  54. package/dist/server/options.d.ts +2 -1
  55. package/dist/server/options.d.ts.map +1 -1
  56. package/dist/server/server.d.ts.map +1 -1
  57. package/dist/server/settings.d.ts.map +1 -1
  58. package/dist/server/transport.d.ts +40 -4
  59. package/dist/server/transport.d.ts.map +1 -1
  60. package/docs/README.md +3 -3
  61. package/docs/guide.md +407 -13
  62. package/docs/roadmap.md +43 -8
  63. package/docs/troubleshooting.md +706 -9
  64. package/package.json +12 -4
  65. package/dist/server/lines.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,MAAM,aAAa,GACtB,gBAAgB,GAChB,mBAAmB,GACnB,iBAAiB,GACjB,iBAAiB,GACjB,iBAAiB,GACjB,cAAc,GACd,oBAAoB,GACpB,kBAAkB,CAAC;AAEtB;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,KAAK;IACnC,SAAkB,IAAI,eAAe;IACrC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;gBAEjB,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM;CAIhD"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAE9C,uGAAuG;AACvG,MAAM,MAAM,aAAa,GACtB,gBAAgB,GAChB,mBAAmB,GACnB,iBAAiB,GACjB,iBAAiB,GACjB,iBAAiB,GACjB,cAAc,GACd,oBAAoB,GACpB,kBAAkB,GAClB,mBAAmB,GACnB,SAAS,GACT,WAAW,GACX,SAAS,GACT,oBAAoB,GACpB,iBAAiB,GACjB,YAAY,GACZ,kBAAkB,GAClB,mBAAmB,GACnB,SAAS,GACT,YAAY,CAAC;AAEhB,6DAA6D;AAC7D,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACtB;AAED,uFAAuF;AACvF,MAAM,WAAW,gBAAgB;IAChC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CAC9C;AAYD;;;;;;;GAOG;AACH,qBAAa,SAAU,SAAQ,KAAK;IACnC,SAAkB,IAAI,eAAe;IACrC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,mFAAmF;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;gBAG7C,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,gBAAqB;CAU/B"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { SmtpError, type SmtpErrorCode } from './errors';
2
2
  export { type Command, type PathCommand, parseCommand, parsePathCommand, } from './protocol/command';
3
3
  export { type DataChunk, DataReader } from './protocol/data';
4
- export { type Path, parsePath } from './protocol/path';
4
+ export { type Path, parsePath, type SourceRoute } from './protocol/path';
5
5
  export { formatReply, type Reply, reply } from './protocol/reply';
6
6
  export { type Credentials, decodeLoginStep, decodePlain, } from './protocol/sasl';
7
7
  export type { Envelope, HookResult, ReceivedMessage, Session, SmtpHooks, SmtpServerOptions, TlsOptions, } from './server/options';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,YAAY,EACZ,gBAAgB,GAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,KAAK,IAAI,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EACN,KAAK,WAAW,EAChB,eAAe,EACf,WAAW,GACX,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACX,QAAQ,EACR,UAAU,EACV,eAAe,EACf,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,UAAU,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,YAAY,EACZ,gBAAgB,GAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,KAAK,IAAI,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EACN,KAAK,WAAW,EAChB,eAAe,EACf,WAAW,GACX,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACX,QAAQ,EACR,UAAU,EACV,eAAe,EACf,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,UAAU,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -1,54 +1,12 @@
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
-
1
+ import {
2
+ SmtpError2,
3
+ isHelloName,
4
+ parsePath2,
5
+ decodePlain2,
6
+ decodeLoginStep2,
7
+ LineSplitter,
8
+ Outgoing
9
+ } from "./chunks/index-3423jxf9.js";
52
10
  // src/protocol/command.ts
53
11
  function parseCommand(line) {
54
12
  const space = line.indexOf(" ");
@@ -63,7 +21,7 @@ function parsePathCommand(argument, keyword) {
63
21
  const match = new RegExp(`^${keyword}:\\s*(<[^>]*>)(.*)$`, "i").exec(argument);
64
22
  if (!match)
65
23
  return;
66
- const path = parsePath(match[1], keyword === "FROM");
24
+ const path = parsePath2(match[1], keyword === "FROM");
67
25
  if (!path)
68
26
  return;
69
27
  const parameters = {};
@@ -165,36 +123,6 @@ function formatReply(reply, enhanced = true) {
165
123
  }).join("");
166
124
  }
167
125
  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
126
  // src/server/guard.ts
199
127
  var LOCAL_ERROR = reply(451, "4.3.0", "Local error in processing");
200
128
  var TIMED_OUT = Symbol("timed out");
@@ -213,7 +141,7 @@ function timedOut(value) {
213
141
  return value === TIMED_OUT;
214
142
  }
215
143
  function hookTimeout(name, seconds) {
216
- return new SmtpError("HOOK_TIMEOUT", `${name} did not settle within hookTimeout (${seconds} s)`);
144
+ return new SmtpError2("HOOK_TIMEOUT", `${name} did not settle within hookTimeout (${seconds} s)`);
217
145
  }
218
146
  function refusalOf(name, answer, report) {
219
147
  if (answer === undefined)
@@ -222,7 +150,7 @@ function refusalOf(name, answer, report) {
222
150
  if (typeof code === "number" && code >= 400 && code < 600) {
223
151
  return answer;
224
152
  }
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`));
153
+ report(new SmtpError2("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
154
  return LOCAL_ERROR;
227
155
  }
228
156
 
@@ -281,7 +209,7 @@ async function authenticate(connection, argument) {
281
209
  state.waiting = "auth-plain";
282
210
  return connection.send(reply(334, undefined, ""));
283
211
  }
284
- return check(connection, decodePlain(initial === "=" ? "" : initial));
212
+ return check(connection, decodePlain2(initial === "=" ? "" : initial));
285
213
  case "LOGIN":
286
214
  if (initial === undefined) {
287
215
  state.waiting = "auth-login-user";
@@ -293,7 +221,7 @@ async function authenticate(connection, argument) {
293
221
  }
294
222
  }
295
223
  function loginUser(connection, response) {
296
- const user = decodeLoginStep(response);
224
+ const user = decodeLoginStep2(response);
297
225
  if (!user)
298
226
  return check(connection, undefined);
299
227
  connection.state.loginUser = user;
@@ -309,11 +237,11 @@ async function continueAuth(connection, line) {
309
237
  }
310
238
  switch (state.waiting) {
311
239
  case "auth-plain":
312
- return check(connection, decodePlain(line));
240
+ return check(connection, decodePlain2(line));
313
241
  case "auth-login-user":
314
242
  return loginUser(connection, line);
315
243
  default: {
316
- const password = decodeLoginStep(line);
244
+ const password = decodeLoginStep2(line);
317
245
  const username = state.loginUser ?? "";
318
246
  return check(connection, password ? { mechanism: "LOGIN", username, password } : undefined);
319
247
  }
@@ -433,7 +361,7 @@ function ehloLines(connection) {
433
361
  return lines;
434
362
  }
435
363
  async function hello(connection, argument, esmtp) {
436
- if (!/^(?:[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*|\[[0-9A-Fa-f:.Iv]+\])$/.test(argument)) {
364
+ if (!isHelloName(argument)) {
437
365
  connection.fail(reply(501, "5.5.4", `Syntax: ${esmtp ? "EHLO" : "HELO"} hostname`));
438
366
  return;
439
367
  }
@@ -499,7 +427,7 @@ async function runCommand(connection, line) {
499
427
  case "HELP":
500
428
  return connection.send(reply(214, "2.0.0", "See RFC 5321"));
501
429
  case "QUIT":
502
- return connection.close(reply(221, "2.0.0", `${connection.settings.options.hostname} closing connection`));
430
+ return connection.quit(reply(221, "2.0.0", `${connection.settings.options.hostname} closing connection`));
503
431
  case "STARTTLS":
504
432
  return startTls(connection, argument);
505
433
  case "AUTH":
@@ -565,22 +493,22 @@ function receivedField(connection, recipients, id, now = new Date) {
565
493
  var TOO_BIG = reply(552, "5.3.4", "Message too big for system");
566
494
  var BARE_LINE_BREAK = reply(550, "5.6.11", "Bare CR or LF is not allowed in a message");
567
495
  function tooBig(maxMessageSize) {
568
- return new SmtpError("MESSAGE_TOO_BIG", `The message is larger than maxMessageSize (${maxMessageSize} bytes); do not deliver it`);
496
+ return new SmtpError2("MESSAGE_TOO_BIG", `The message is larger than maxMessageSize (${maxMessageSize} bytes); do not deliver it`);
569
497
  }
570
498
  function bareLineBreak() {
571
- return new SmtpError("BARE_LINE_BREAK", "The message holds a bare CR or LF (SMTP smuggling); do not deliver it");
499
+ return new SmtpError2("BARE_LINE_BREAK", "The message holds a bare CR or LF (SMTP smuggling); do not deliver it");
572
500
  }
573
501
  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");
502
+ return new SmtpError2("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
503
  }
576
504
  function notAnswered(seconds) {
577
- return new SmtpError("HOOK_TIMEOUT", `onData did not answer within hookTimeout (${seconds} s); do not deliver it`);
505
+ return new SmtpError2("HOOK_TIMEOUT", `onData did not answer within hookTimeout (${seconds} s); do not deliver it`);
578
506
  }
579
507
  function notReadInTime(seconds) {
580
- return new SmtpError("HOOK_TIMEOUT", `onData did not read the message within hookTimeout (${seconds} s); do not deliver it`);
508
+ return new SmtpError2("HOOK_TIMEOUT", `onData did not read the message within hookTimeout (${seconds} s); do not deliver it`);
581
509
  }
582
510
  function notRead() {
583
- return new SmtpError("MESSAGE_NOT_READ", "onData answered without reading the message to its end; it was not taken");
511
+ return new SmtpError2("MESSAGE_NOT_READ", "onData answered without reading the message to its end; it was not taken");
584
512
  }
585
513
 
586
514
  // src/server/intake.ts
@@ -755,61 +683,6 @@ class Intake {
755
683
  }
756
684
  }
757
685
 
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
686
  // src/server/input.ts
814
687
  var INPUT_LIMIT = 64 * 1024;
815
688
 
@@ -1008,6 +881,14 @@ class Connection {
1008
881
  this.send(answer);
1009
882
  this.#closed = true;
1010
883
  this.input.abort();
884
+ this.transport.abort();
885
+ }
886
+ quit(answer) {
887
+ if (this.#closed)
888
+ return;
889
+ this.send(answer);
890
+ this.#closed = true;
891
+ this.input.abort();
1011
892
  this.transport.end();
1012
893
  }
1013
894
  fail(answer) {
@@ -1072,7 +953,8 @@ class Connection {
1072
953
  }
1073
954
 
1074
955
  // src/server/settings.ts
1075
- var invalid = (message) => new SmtpError("INVALID_OPTION", `createSmtpServer(): ${message}`);
956
+ var MAX_TIMER_SECONDS = Math.floor(2147483647 / 1000);
957
+ var invalid = (message) => new SmtpError2("INVALID_OPTION", `createSmtpServer(): ${message}`);
1076
958
  function positive(name, value, fallback) {
1077
959
  if (value === undefined)
1078
960
  return fallback;
@@ -1081,6 +963,13 @@ function positive(name, value, fallback) {
1081
963
  }
1082
964
  return value;
1083
965
  }
966
+ function timer(name, value, fallback) {
967
+ const seconds = positive(name, value, fallback);
968
+ if (seconds > MAX_TIMER_SECONDS) {
969
+ throw invalid(`${name} must be at most ${MAX_TIMER_SECONDS} seconds, not ${seconds}`);
970
+ }
971
+ return seconds;
972
+ }
1084
973
  function nonNegative(name, value, fallback) {
1085
974
  if (value === undefined)
1086
975
  return fallback;
@@ -1128,72 +1017,137 @@ function settingsOf(options) {
1128
1017
  maxConnections: positive("maxConnections", options.maxConnections, 1000),
1129
1018
  maxErrors: positive("maxErrors", options.maxErrors, 10),
1130
1019
  timeout,
1131
- hookTimeout: positive("hookTimeout", options.hookTimeout, 60),
1020
+ hookTimeout: timer("hookTimeout", options.hookTimeout, 60),
1132
1021
  greetingDelay,
1133
1022
  isLocal: (domain) => isLocal(domain.toLowerCase())
1134
1023
  };
1135
1024
  }
1136
1025
 
1137
1026
  // src/server/transport.ts
1027
+ var CLOSE_GRACE_MS = 5000;
1028
+ var LINGER_QUIET_MS = 20;
1029
+ var LINGER_MAX_MS = 500;
1030
+
1138
1031
  class SocketTransport {
1139
1032
  #socket;
1140
1033
  remoteAddress;
1141
1034
  secure;
1142
1035
  #startTls;
1143
- #queue = [];
1144
- #waiters = [];
1036
+ #outgoing;
1145
1037
  #ending = false;
1038
+ #closed = false;
1039
+ #paused = false;
1040
+ #grace;
1041
+ #linger;
1146
1042
  constructor(socket, secure, startTls, remoteAddress = socket.remoteAddress) {
1147
1043
  this.#socket = socket;
1148
1044
  this.secure = secure;
1149
1045
  this.#startTls = startTls;
1150
1046
  this.remoteAddress = remoteAddress;
1047
+ this.#outgoing = new Outgoing((bytes) => socket.write(bytes));
1151
1048
  }
1152
1049
  write(text) {
1153
- const bytes = new TextEncoder().encode(text);
1154
- if (this.#queue.length > 0) {
1155
- this.#queue.push(bytes);
1050
+ if (this.#closed)
1156
1051
  return;
1157
- }
1158
- const written = this.#socket.write(bytes);
1159
- if (written < bytes.length)
1160
- this.#queue.push(bytes.subarray(written));
1052
+ this.#outgoing.write(new TextEncoder().encode(text));
1161
1053
  }
1162
1054
  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();
1055
+ if (this.#outgoing.drain() && this.#ending)
1056
+ this.#hangUp(true);
1176
1057
  }
1177
1058
  drained() {
1178
- if (this.#queue.length === 0)
1179
- return Promise.resolve();
1180
- return new Promise((wake) => this.#waiters.push(wake));
1059
+ return this.#outgoing.drained();
1060
+ }
1061
+ received() {
1062
+ const linger = this.#linger;
1063
+ if (!linger)
1064
+ return;
1065
+ const left = linger.until - Date.now();
1066
+ if (left <= 0)
1067
+ this.#terminate();
1068
+ else
1069
+ this.#lingerFor(Math.min(LINGER_QUIET_MS, left));
1181
1070
  }
1182
1071
  closed() {
1183
- this.#queue = [];
1184
- for (const wake of this.#waiters.splice(0))
1185
- wake();
1072
+ this.#closed = true;
1073
+ this.#disarm();
1074
+ this.#stopLingering();
1075
+ this.#outgoing.clear();
1186
1076
  }
1187
1077
  end() {
1188
- if (this.#queue.length === 0)
1189
- this.#socket.end();
1078
+ if (this.#closed)
1079
+ return;
1080
+ this.#arm();
1081
+ if (this.#outgoing.empty)
1082
+ this.#hangUp(false);
1190
1083
  else
1191
1084
  this.#ending = true;
1192
1085
  }
1086
+ abort() {
1087
+ if (this.#closed)
1088
+ return;
1089
+ if (this.#outgoing.empty)
1090
+ this.end();
1091
+ else
1092
+ this.#terminate();
1093
+ }
1094
+ #hangUp(drained) {
1095
+ this.#ending = false;
1096
+ if (this.#linger)
1097
+ return;
1098
+ if (!this.#paused) {
1099
+ this.#shutdown(drained);
1100
+ return;
1101
+ }
1102
+ this.resume();
1103
+ this.#linger = { drained, until: Date.now() + LINGER_MAX_MS };
1104
+ this.#lingerFor(LINGER_QUIET_MS);
1105
+ }
1106
+ #lingerFor(ms) {
1107
+ const linger = this.#linger;
1108
+ if (!linger)
1109
+ return;
1110
+ clearTimeout(linger.timer);
1111
+ linger.timer = setTimeout(() => {
1112
+ if (ms < LINGER_QUIET_MS)
1113
+ return this.#terminate();
1114
+ this.#stopLingering();
1115
+ this.#shutdown(linger.drained);
1116
+ }, ms);
1117
+ linger.timer.unref?.();
1118
+ }
1119
+ #stopLingering() {
1120
+ clearTimeout(this.#linger?.timer);
1121
+ this.#linger = undefined;
1122
+ }
1123
+ #shutdown(drained) {
1124
+ if (drained && this.secure)
1125
+ this.#socket.shutdown();
1126
+ else
1127
+ this.#socket.shutdown(true);
1128
+ }
1129
+ #terminate() {
1130
+ this.#disarm();
1131
+ this.#stopLingering();
1132
+ this.#outgoing.clear();
1133
+ this.#socket.terminate();
1134
+ }
1135
+ #arm() {
1136
+ if (this.#grace)
1137
+ return;
1138
+ this.#grace = setTimeout(() => this.#terminate(), CLOSE_GRACE_MS);
1139
+ this.#grace.unref?.();
1140
+ }
1141
+ #disarm() {
1142
+ clearTimeout(this.#grace);
1143
+ this.#grace = undefined;
1144
+ }
1193
1145
  pause() {
1146
+ this.#paused = true;
1194
1147
  this.#socket.pause();
1195
1148
  }
1196
1149
  resume() {
1150
+ this.#paused = false;
1197
1151
  this.#socket.resume();
1198
1152
  }
1199
1153
  startTls() {
@@ -1211,6 +1165,7 @@ function handlers(settings) {
1211
1165
  data(socket, chunk) {
1212
1166
  if (socket.data.upgraded)
1213
1167
  return;
1168
+ socket.data.transport?.received();
1214
1169
  socket.timeout(settings.timeout);
1215
1170
  socket.data.connection?.receive(chunk);
1216
1171
  },
@@ -1224,7 +1179,11 @@ function handlers(settings) {
1224
1179
  timeout(socket) {
1225
1180
  if (socket.data.upgraded)
1226
1181
  return;
1227
- socket.data.connection?.close(idle);
1182
+ const { connection, transport } = socket.data;
1183
+ if (connection?.closed)
1184
+ transport?.abort();
1185
+ else
1186
+ connection?.close(idle);
1228
1187
  }
1229
1188
  };
1230
1189
  }
@@ -1257,15 +1216,15 @@ function transportOf(socket, settings, secure) {
1257
1216
  function createSmtpServer(options) {
1258
1217
  const settings = settingsOf(options);
1259
1218
  let listener;
1260
- let open = 0;
1219
+ const open = new Set;
1261
1220
  const secure = options.implicitTls === true;
1262
1221
  return {
1263
1222
  get connections() {
1264
- return open;
1223
+ return open.size;
1265
1224
  },
1266
1225
  async listen({ port, hostname = "0.0.0.0" }) {
1267
1226
  if (listener) {
1268
- throw new SmtpError("ALREADY_LISTENING", `listen(): the server is already listening on ${listener.hostname}:${listener.port}`);
1227
+ throw new SmtpError2("ALREADY_LISTENING", `listen(): the server is already listening on ${listener.hostname}:${listener.port}`);
1269
1228
  }
1270
1229
  listener = Bun.listen({
1271
1230
  hostname,
@@ -1276,19 +1235,23 @@ function createSmtpServer(options) {
1276
1235
  open(socket) {
1277
1236
  socket.data = { upgraded: false };
1278
1237
  socket.timeout(settings.timeout);
1279
- if (open >= settings.maxConnections) {
1280
- socket.end(`421 4.3.2 ${options.hostname} Too many connections, try later\r
1238
+ if (open.size >= settings.maxConnections) {
1239
+ const refused = new SocketTransport(socket, secure, () => {});
1240
+ socket.data.transport = refused;
1241
+ refused.write(`421 4.3.2 ${options.hostname} Too many connections, try later\r
1281
1242
  `);
1243
+ refused.end();
1282
1244
  return;
1283
1245
  }
1284
- open++;
1285
1246
  const connection = new Connection(settings, transportOf(socket, settings, secure));
1247
+ open.add(connection);
1286
1248
  socket.data.connection = connection;
1287
1249
  connection.open();
1288
1250
  },
1289
1251
  close(socket) {
1290
- if (socket.data.connection)
1291
- open--;
1252
+ const connection = socket.data.connection;
1253
+ if (connection)
1254
+ open.delete(connection);
1292
1255
  socket.data.transport?.closed();
1293
1256
  socket.data.connection?.close();
1294
1257
  }
@@ -1299,21 +1262,25 @@ function createSmtpServer(options) {
1299
1262
  stop(closeConnections = false) {
1300
1263
  listener?.stop(closeConnections);
1301
1264
  listener = undefined;
1265
+ if (!closeConnections)
1266
+ return;
1267
+ for (const connection of [...open])
1268
+ connection.close();
1302
1269
  }
1303
1270
  };
1304
1271
  }
1305
1272
  export {
1306
1273
  DataReader,
1307
- SmtpError,
1274
+ SmtpError2 as SmtpError,
1308
1275
  createSmtpServer,
1309
- decodeLoginStep,
1310
- decodePlain,
1276
+ decodeLoginStep2 as decodeLoginStep,
1277
+ decodePlain2 as decodePlain,
1311
1278
  formatReply,
1312
1279
  parseCommand,
1313
- parsePath,
1280
+ parsePath2 as parsePath,
1314
1281
  parsePathCommand,
1315
1282
  reply
1316
1283
  };
1317
1284
 
1318
- //# debugId=DE4D9903D05BDADC64756E2164756E21
1285
+ //# debugId=04EE37708209636464756E2164756E21
1319
1286
  //# sourceMappingURL=index.js.map