@bumail/smtp 0.11.1 → 0.12.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 (42) hide show
  1. package/README.md +30 -2
  2. package/dist/client/connect.d.ts +18 -0
  3. package/dist/client/connect.d.ts.map +1 -0
  4. package/dist/client/inbox.d.ts +2 -0
  5. package/dist/client/inbox.d.ts.map +1 -1
  6. package/dist/client/index.d.ts +2 -0
  7. package/dist/client/index.d.ts.map +1 -1
  8. package/dist/client/index.js +679 -344
  9. package/dist/client/index.js.map +20 -11
  10. package/dist/client/options.d.ts +2 -0
  11. package/dist/client/options.d.ts.map +1 -1
  12. package/dist/client/pool/idle.d.ts +10 -0
  13. package/dist/client/pool/idle.d.ts.map +1 -0
  14. package/dist/client/pool/index.d.ts +18 -0
  15. package/dist/client/pool/index.d.ts.map +1 -0
  16. package/dist/client/pool/options.d.ts +26 -0
  17. package/dist/client/pool/options.d.ts.map +1 -0
  18. package/dist/client/pool/reset.d.ts +9 -0
  19. package/dist/client/pool/reset.d.ts.map +1 -0
  20. package/dist/client/pool/send.d.ts +8 -0
  21. package/dist/client/pool/send.d.ts.map +1 -0
  22. package/dist/client/pool/state.d.ts +28 -0
  23. package/dist/client/pool/state.d.ts.map +1 -0
  24. package/dist/client/send.d.ts +10 -1
  25. package/dist/client/send.d.ts.map +1 -1
  26. package/dist/client/session.d.ts +2 -2
  27. package/dist/client/session.d.ts.map +1 -1
  28. package/dist/client/settings.d.ts +11 -5
  29. package/dist/client/settings.d.ts.map +1 -1
  30. package/dist/client/socket-events.d.ts +15 -0
  31. package/dist/client/socket-events.d.ts.map +1 -0
  32. package/dist/client/socket-waits.d.ts +36 -0
  33. package/dist/client/socket-waits.d.ts.map +1 -0
  34. package/dist/client/socket.d.ts +14 -4
  35. package/dist/client/socket.d.ts.map +1 -1
  36. package/dist/client/transaction.d.ts +9 -3
  37. package/dist/client/transaction.d.ts.map +1 -1
  38. package/docs/README.md +3 -3
  39. package/docs/guide.md +25 -3
  40. package/docs/roadmap.md +10 -4
  41. package/docs/troubleshooting.md +84 -0
  42. package/package.json +2 -2
@@ -225,6 +225,20 @@ function contentOf(message, normalize, size) {
225
225
  };
226
226
  }
227
227
 
228
+ // src/protocol/ehlo.ts
229
+ function parseEhlo(lines) {
230
+ const extensions = new Map;
231
+ for (const line of lines.slice(1)) {
232
+ const text = line.trim();
233
+ const space = text.indexOf(" ");
234
+ const keyword = (space < 0 ? text : text.slice(0, space)).toUpperCase();
235
+ if (keyword === "")
236
+ continue;
237
+ extensions.set(keyword, space < 0 ? "" : text.slice(space + 1).trim());
238
+ }
239
+ return extensions;
240
+ }
241
+
228
242
  // src/protocol/reply-reader.ts
229
243
  var MAX_REPLY_LINES = 100;
230
244
  var isDigit = (c) => c >= 48 && c <= 57;
@@ -316,20 +330,6 @@ function afterDot(error, taken) {
316
330
  });
317
331
  }
318
332
 
319
- // src/protocol/ehlo.ts
320
- function parseEhlo(lines) {
321
- const extensions = new Map;
322
- for (const line of lines.slice(1)) {
323
- const text = line.trim();
324
- const space = text.indexOf(" ");
325
- const keyword = (space < 0 ? text : text.slice(0, space)).toUpperCase();
326
- if (keyword === "")
327
- continue;
328
- extensions.set(keyword, space < 0 ? "" : text.slice(space + 1).trim());
329
- }
330
- return extensions;
331
- }
332
-
333
333
  // src/client/session.ts
334
334
  async function hello(socket, settings, allowHelo) {
335
335
  const seconds = settings.timeouts.command;
@@ -408,206 +408,6 @@ async function open(socket, settings, tls) {
408
408
  return { extensions, authenticated: settings.auth !== undefined };
409
409
  }
410
410
 
411
- // src/client/settings.ts
412
- import { hostname as machineName } from "node:os";
413
-
414
- // src/client/dsn.ts
415
- var EVENTS = ["SUCCESS", "FAILURE", "DELAY"];
416
- function dsnOf(given, to, fail) {
417
- const { ret, envid, recipients } = given;
418
- if (ret !== undefined && ret !== "FULL" && ret !== "HDRS")
419
- throw fail(`ret must be 'FULL' or 'HDRS', not ${String(ret)}`);
420
- if (envid !== undefined && (typeof envid !== "string" || !/^[ -~]{1,100}$/.test(envid) || encodeXtext(envid).length > 100))
421
- throw fail("envid must be 1 to 100 printable ASCII characters, at most 100 once encoded as xtext");
422
- const byAddress = new Map;
423
- if (recipients !== undefined) {
424
- if (!Array.isArray(recipients))
425
- throw fail("recipients must be an array of { address, notify, orcpt }");
426
- for (const entry of recipients) {
427
- const address = entry?.address;
428
- if (typeof address !== "string" || !to.includes(address))
429
- throw fail(`recipients names ${JSON.stringify(address)}, which is not in to`);
430
- if (byAddress.has(address))
431
- throw fail(`recipients names ${address} twice`);
432
- checkRecipient(entry, fail);
433
- byAddress.set(address, entry);
434
- }
435
- }
436
- return {
437
- ...ret ? { ret } : {},
438
- ...envid === undefined ? {} : { envid },
439
- recipients: byAddress
440
- };
441
- }
442
- function checkRecipient({ address, notify, orcpt }, fail) {
443
- if (notify !== undefined && notify !== "NEVER") {
444
- const events = Array.isArray(notify) ? notify : [];
445
- if (events.length === 0 || events.some((e, i) => !EVENTS.includes(e) || events.indexOf(e) !== i))
446
- throw fail(`notify of ${address} must be 'NEVER' or a list of 'SUCCESS', 'FAILURE', 'DELAY', each once`);
447
- }
448
- if (orcpt !== undefined) {
449
- if (typeof orcpt !== "object" || orcpt === null || typeof orcpt.type !== "string" || !/^[A-Za-z0-9-]+$/.test(orcpt.type))
450
- throw fail(`orcpt of ${address} must be { type, address }, the type a token`);
451
- if (typeof orcpt.address !== "string" || orcpt.address === "")
452
- throw fail(`orcpt.address of ${address} must be a non-empty string`);
453
- const encoded = orcpt.type.toLowerCase() === "utf-8" ? utf8OrcptLength(orcpt.address) : encodeXtext(orcpt.address).length;
454
- if (orcpt.type.length + 1 + encoded > 500)
455
- throw fail(`orcpt of ${address} is over 500 characters once encoded`);
456
- }
457
- }
458
- function mailDsn(dsn, offered) {
459
- if (!offered)
460
- return "";
461
- let out = "";
462
- if (dsn.ret)
463
- out += ` RET=${dsn.ret}`;
464
- if (dsn.envid !== undefined)
465
- out += ` ENVID=${encodeXtext(dsn.envid)}`;
466
- return out;
467
- }
468
- function orcptParameter({ type, address }, smtputf8) {
469
- const kind = type.toLowerCase();
470
- if (kind === "rfc822")
471
- return /^[\x20-\x7e]+$/.test(address) ? ` ORCPT=${type};${encodeXtext(address)}` : "";
472
- if (kind !== "utf-8") {
473
- const value = `${type};${encodeXtext(address)}`;
474
- return address.isWellFormed() ? ` ORCPT=${value}` : "";
475
- }
476
- const encoded = encodeUtf8Orcpt(address, smtputf8);
477
- if (encoded === undefined)
478
- return "";
479
- const value = `${type};${encoded}`;
480
- return new TextEncoder().encode(value).length > 500 ? "" : ` ORCPT=${value}`;
481
- }
482
- function rcptDsn(dsn, address, offered, smtputf8 = false) {
483
- const entry = dsn.recipients.get(address);
484
- if (!offered || !entry)
485
- return "";
486
- let out = "";
487
- const { notify, orcpt } = entry;
488
- if (notify !== undefined)
489
- out += ` NOTIFY=${notify === "NEVER" ? "NEVER" : notify.join(",")}`;
490
- if (orcpt)
491
- out += orcptParameter(orcpt, smtputf8);
492
- return out;
493
- }
494
-
495
- // src/client/settings.ts
496
- var MAX_SECONDS = Math.floor(2147483647 / 1000);
497
- var TIMEOUTS = {
498
- connect: 30,
499
- greeting: 300,
500
- command: 300,
501
- mail: 300,
502
- rcpt: 300,
503
- dataStart: 120,
504
- dataBlock: 180,
505
- dataEnd: 600
506
- };
507
- var invalid = (message) => new SmtpError2("INVALID_OPTION", `sendMail(): ${message}`);
508
- function seconds(name, value, fallback) {
509
- if (value === undefined)
510
- return fallback;
511
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > MAX_SECONDS) {
512
- throw invalid(`${name} must be a number of seconds above 0 and at most ${MAX_SECONDS}, not ${String(value)}`);
513
- }
514
- return value;
515
- }
516
- function timeoutsOf(given = {}) {
517
- const out = {};
518
- for (const [name, fallback] of Object.entries(TIMEOUTS)) {
519
- out[name] = seconds(`timeouts.${name}`, given[name], fallback);
520
- }
521
- return out;
522
- }
523
- function address(value, allowNull) {
524
- if (allowNull && value === "")
525
- return "";
526
- if (isMailbox(value))
527
- return value;
528
- const text = typeof value === "string" ? value : "";
529
- const routed = typeof value === "string" && parsePath2(`<${text}>`, false);
530
- if (routed) {
531
- throw invalid(`${JSON.stringify(value)} holds a source route (@host:), which RFC 5321 says a client should not send: pass ${JSON.stringify(`${routed.local}@${text.slice(text.lastIndexOf("@") + 1)}`)} alone`);
532
- }
533
- throw invalid(`${JSON.stringify(value)} is not an address (local@domain${allowNull ? ", or '' for a bounce" : ""})`);
534
- }
535
- function heloOf(value, byMx) {
536
- if (value !== undefined) {
537
- if (!isHelloName(value))
538
- throw invalid(`helo "${value}" is not a host name`);
539
- return value;
540
- }
541
- if (byMx)
542
- throw invalid("helo is required for delivery by MX: pass your server's public name, such as helo: 'mail.example.com'");
543
- const name = machineName();
544
- return isHelloName(name) ? name : "localhost";
545
- }
546
- function authOf(auth) {
547
- if (auth === undefined)
548
- return;
549
- const { username, password, mechanism } = auth;
550
- if (typeof username !== "string" || username === "")
551
- throw invalid("auth.username must be a non-empty string");
552
- if (typeof password !== "string" || password === "")
553
- throw invalid("auth.password must be a non-empty string");
554
- if (mechanism !== undefined && mechanism !== "PLAIN" && mechanism !== "LOGIN")
555
- throw invalid(`auth.mechanism must be 'PLAIN' or 'LOGIN', not ${mechanism}`);
556
- return auth;
557
- }
558
- function tlsOf(options, plaintextAuth) {
559
- const secure = options.secure === true;
560
- const tls = options.tls ?? (secure || options.auth && !plaintextAuth ? "required" : "opportunistic");
561
- if (tls !== "opportunistic" && tls !== "required" && tls !== "none")
562
- throw invalid(`tls must be 'opportunistic', 'required' or 'none', not ${tls}`);
563
- if (tls === "none" && secure)
564
- throw invalid("secure is TLS from the first byte: it cannot go with tls: 'none'");
565
- if (tls === "none" && options.auth && !plaintextAuth)
566
- throw invalid("auth with tls: 'none' would send the password in clear; pass allowPlaintextAuth: true for a local test server");
567
- if (tls === "opportunistic" && options.auth && !plaintextAuth)
568
- throw invalid("auth with tls: 'opportunistic' would send the password to a server whose certificate is not checked; leave tls out to check it, or pass allowPlaintextAuth: true for a local test server");
569
- return tls;
570
- }
571
- function settingsOf(options) {
572
- if (typeof options !== "object" || options === null)
573
- throw invalid("options must be an object");
574
- const from = address(options.from, true);
575
- const list = typeof options.to === "string" ? [options.to] : options.to;
576
- if (!Array.isArray(list) || list.length === 0)
577
- throw invalid("to must name one recipient or more");
578
- const to = list.map((recipient) => address(recipient, false));
579
- const allowPlaintextAuth = options.allowPlaintextAuth === true;
580
- const { size } = options;
581
- if (size !== undefined && (!Number.isSafeInteger(size) || size < 0))
582
- throw invalid(`size must be a number of bytes, not ${size}`);
583
- const ca = options.ca === undefined ? undefined : [options.ca].flat();
584
- const auth = authOf(options.auth);
585
- return {
586
- from,
587
- to,
588
- helo: heloOf(options.helo, byMx(options)),
589
- tls: tlsOf(options, allowPlaintextAuth),
590
- secure: options.secure === true,
591
- ...ca ? { ca } : {},
592
- ...auth ? { auth } : {},
593
- allowPlaintextAuth,
594
- ...size === undefined ? {} : { size },
595
- dsn: dsnOf(options, to, invalid),
596
- smtputf8: options.smtputf8 === true || [from, ...to].some((text) => /[^ -~]/.test(text)),
597
- normalizeLineEnds: options.normalizeLineEnds === true,
598
- timeouts: timeoutsOf(options.timeouts),
599
- deadline: seconds("deadline", options.deadline, 1800)
600
- };
601
- }
602
- var byMx = (options) => !(("host" in options) && options.host !== undefined);
603
- function portOf(port, fallback) {
604
- if (port === undefined)
605
- return fallback;
606
- if (!Number.isInteger(port) || port < 1 || port > 65535)
607
- throw invalid(`port must be an integer from 1 to 65535, not ${port}`);
608
- return port;
609
- }
610
-
611
411
  // src/client/inbox.ts
612
412
  var MAX_REPLY_BYTES = 1024 * 1024;
613
413
 
@@ -634,6 +434,9 @@ class Inbox {
634
434
  }
635
435
  return;
636
436
  }
437
+ reset() {
438
+ this.#received = 0;
439
+ }
637
440
  shift() {
638
441
  return this.#replies.shift();
639
442
  }
@@ -675,47 +478,151 @@ class Clock {
675
478
  }
676
479
  }
677
480
 
678
- // src/client/socket.ts
679
- class ClientSocket {
680
- host;
681
- #clock;
682
- #socket;
683
- #outgoing = new Outgoing(() => 0);
684
- #tls = false;
685
- #inbox = new Inbox;
686
- #failure;
687
- #closed = false;
481
+ // src/client/socket-events.ts
482
+ function socketHandlers(live, hooks) {
483
+ const when = (run) => (...args) => {
484
+ if (live())
485
+ run(...args);
486
+ };
487
+ return {
488
+ data: when((_, chunk) => hooks.receive(chunk)),
489
+ drain: when(() => hooks.drain()),
490
+ close: when(() => hooks.lost()),
491
+ error: when(() => hooks.lost()),
492
+ handshake: when((_, ok, error) => hooks.handshake(ok, error))
493
+ };
494
+ }
495
+ var connectFailure = (endpoint, error) => error instanceof SmtpError2 ? error : new SmtpError2("CONNECTION_FAILED", `Could not connect to ${endpoint.host} (${endpoint.address}:${endpoint.port}): ${reason(error)}`);
496
+
497
+ // src/client/socket-waits.ts
498
+ class Waits {
499
+ hooks;
688
500
  #wake;
689
- #handshaking;
690
- #gone = Promise.withResolvers();
691
- secure = false;
692
- verified = false;
693
- constructor(host, clock) {
694
- this.host = host;
695
- this.#clock = clock;
501
+ #watchers = new Set;
502
+ #replies = 0;
503
+ get replies() {
504
+ return this.#replies;
696
505
  }
697
- static async open(endpoint, tls, seconds, clock) {
698
- const client = new ClientSocket(endpoint.host, clock);
699
- client.#tls = tls !== undefined;
700
- const handshake = tls ? client.#expectHandshake(tls) : undefined;
701
- handshake?.catch(() => {});
702
- const { address, port } = endpoint;
703
- const connecting = Bun.connect({
704
- hostname: address,
506
+ constructor(hooks) {
507
+ this.hooks = hooks;
508
+ }
509
+ wakeUp() {
510
+ const wake = this.#wake;
511
+ this.#wake = undefined;
512
+ wake?.();
513
+ }
514
+ release() {
515
+ for (const watcher of this.#watchers)
516
+ watcher();
517
+ }
518
+ race(promise, seconds, what, start = performance.now()) {
519
+ const ms = start + seconds * 1000 - performance.now();
520
+ const { host, fail } = this.hooks;
521
+ const clock = this.hooks.clock();
522
+ return clock.race(promise, ms, (passed) => fail(timedOut(host, what, seconds, passed ? clock.seconds : undefined)));
523
+ }
524
+ async reply(what, seconds) {
525
+ const { inbox, failure, closed, fail, host } = this.hooks;
526
+ const start = performance.now();
527
+ for (;; ) {
528
+ const next = inbox.shift();
529
+ if (next) {
530
+ this.#replies++;
531
+ return next;
532
+ }
533
+ const failed = failure();
534
+ if (failed)
535
+ throw failed;
536
+ if (closed())
537
+ throw fail(lost(host, what));
538
+ await this.race(new Promise((wake) => {
539
+ this.#wake = wake;
540
+ }), seconds, what, start);
541
+ }
542
+ }
543
+ within(promise, seconds, what) {
544
+ const { host, closed, hungUp } = this.hooks;
545
+ const watcher = Promise.withResolvers();
546
+ if (closed())
547
+ watcher.resolve();
548
+ else
549
+ this.#watchers.add(watcher.resolve);
550
+ const gone = watcher.promise;
551
+ let over = false;
552
+ const hung = gone.then(() => {
553
+ if (!over)
554
+ throw hungUp(lost(host, what));
555
+ });
556
+ const race = Promise.race([promise, hung]);
557
+ race.catch(() => {
558
+ return;
559
+ });
560
+ hung.catch(() => {
561
+ return;
562
+ });
563
+ return this.race(race, seconds, what).finally(() => {
564
+ over = true;
565
+ this.#watchers.delete(watcher.resolve);
566
+ });
567
+ }
568
+ }
569
+
570
+ // src/client/socket.ts
571
+ class ClientSocket {
572
+ host;
573
+ clock;
574
+ #socket;
575
+ #outgoing = new Outgoing(() => 0);
576
+ #tls = false;
577
+ #inbox = new Inbox;
578
+ #failure;
579
+ #closed = false;
580
+ #handshaking;
581
+ #waits;
582
+ secure = false;
583
+ get replies() {
584
+ return this.#waits.replies;
585
+ }
586
+ verified = false;
587
+ constructor(host, clock) {
588
+ this.host = host;
589
+ this.clock = clock;
590
+ this.#waits = new Waits({
591
+ host,
592
+ clock: () => this.clock,
593
+ inbox: this.#inbox,
594
+ failure: () => this.#failure,
595
+ closed: () => this.#closed,
596
+ fail: (error) => this.#fail(error),
597
+ hungUp: (otherwise) => this.#hungUp(otherwise)
598
+ });
599
+ }
600
+ reply(what, seconds) {
601
+ return this.#waits.reply(what, seconds);
602
+ }
603
+ within(promise, seconds, what) {
604
+ return this.#waits.within(promise, seconds, what);
605
+ }
606
+ static async open(endpoint, tls, seconds, clock) {
607
+ const client = new ClientSocket(endpoint.host, clock);
608
+ client.#tls = tls !== undefined;
609
+ const handshake = tls ? client.#expectHandshake(tls) : undefined;
610
+ handshake?.catch(() => {});
611
+ const { address, port } = endpoint;
612
+ const connecting = Bun.connect({
613
+ hostname: address,
705
614
  port,
706
615
  ...tls ? { tls: tlsOptions(tls) } : {},
707
616
  socket: client.#handlers(tls !== undefined)
708
617
  });
709
618
  try {
710
- client.#use(await client.#race(connecting, seconds, "the connection"));
619
+ client.#use(await client.#waits.race(connecting, seconds, "the connection"));
711
620
  } catch (error) {
712
621
  connecting.then((socket) => socket.terminate(), () => {});
713
- if (error instanceof SmtpError2)
714
- throw error;
715
- throw new SmtpError2("CONNECTION_FAILED", `Could not connect to ${endpoint.host} (${address}:${port}): ${reason(error)}`);
622
+ throw connectFailure(endpoint, error);
716
623
  }
717
624
  if (handshake) {
718
- await client.#race(handshake, seconds, "the TLS handshake").catch((e) => {
625
+ await client.#waits.race(handshake, seconds, "the TLS handshake").catch((e) => {
719
626
  client.close(true);
720
627
  throw e;
721
628
  });
@@ -727,17 +634,12 @@ class ClientSocket {
727
634
  this.#outgoing = new Outgoing((bytes) => socket.write(bytes));
728
635
  }
729
636
  #handlers(tls) {
730
- const live = (run) => (...args) => {
731
- if (this.#tls === tls)
732
- run(...args);
733
- };
734
- return {
735
- data: live((_, chunk) => this.#receive(chunk)),
736
- drain: live(() => this.#outgoing.drain()),
737
- close: live(() => this.#lost()),
738
- error: live(() => this.#lost()),
739
- handshake: live((_, ok, error) => this.#handshaking?.(ok, error))
740
- };
637
+ return socketHandlers(() => this.#tls === tls, {
638
+ receive: (chunk) => this.#receive(chunk),
639
+ drain: () => this.#outgoing.drain(),
640
+ lost: () => this.#lost(),
641
+ handshake: (ok, error) => this.#handshaking?.(ok, error)
642
+ });
741
643
  }
742
644
  #expectHandshake(tls) {
743
645
  return new Promise((resolve, reject) => {
@@ -763,13 +665,11 @@ class ClientSocket {
763
665
  this.#wakeUp();
764
666
  }
765
667
  #wakeUp() {
766
- const wake = this.#wake;
767
- this.#wake = undefined;
768
- wake?.();
668
+ this.#waits.wakeUp();
769
669
  }
770
670
  #lost() {
771
671
  this.#closed = true;
772
- this.#gone.resolve();
672
+ this.#waits.release();
773
673
  this.#outgoing.clear();
774
674
  this.#handshaking?.(false, null);
775
675
  this.#wakeUp();
@@ -780,39 +680,6 @@ class ClientSocket {
780
680
  this.#lost();
781
681
  return this.#failure;
782
682
  }
783
- #race(promise, seconds, what, start = performance.now()) {
784
- const ms = start + seconds * 1000 - performance.now();
785
- const { host } = this;
786
- const deadline = this.#clock.seconds;
787
- return this.#clock.race(promise, ms, (passed) => this.#fail(timedOut(host, what, seconds, passed ? deadline : undefined)));
788
- }
789
- async reply(what, seconds) {
790
- const start = performance.now();
791
- for (;; ) {
792
- const next = this.#inbox.shift();
793
- if (next)
794
- return next;
795
- if (this.#failure)
796
- throw this.#failure;
797
- if (this.#closed)
798
- throw this.#fail(lost(this.host, what));
799
- await this.#race(new Promise((wake) => {
800
- this.#wake = wake;
801
- }), seconds, what, start);
802
- }
803
- }
804
- within(promise, seconds, what) {
805
- const over = new AbortController;
806
- const gone = this.#gone.promise.then(() => {
807
- if (!over.signal.aborted)
808
- throw this.#hungUp(lost(this.host, what));
809
- });
810
- const race = Promise.race([promise, gone]);
811
- race.catch(() => {
812
- return;
813
- });
814
- return this.#race(race, seconds, what).finally(() => over.abort());
815
- }
816
683
  write(data) {
817
684
  if (this.#failure || this.#closed)
818
685
  return false;
@@ -820,7 +687,7 @@ class ClientSocket {
820
687
  return true;
821
688
  }
822
689
  async drained(seconds) {
823
- await this.#race(this.#outgoing.drained(), seconds, "the server to take the message");
690
+ await this.#waits.race(this.#outgoing.drained(), seconds, "the server to take the message");
824
691
  if (this.#failure || this.#closed)
825
692
  throw this.#hungUp(lost(this.host));
826
693
  }
@@ -843,7 +710,19 @@ class ClientSocket {
843
710
  socket: this.#handlers(true)
844
711
  });
845
712
  this.#use(encrypted);
846
- await this.#race(handshake, seconds, "the TLS handshake");
713
+ await this.#waits.race(handshake, seconds, "the TLS handshake");
714
+ }
715
+ get quiet() {
716
+ return !this.#failure && !this.#closed && !this.#inbox.holding;
717
+ }
718
+ ref(keep) {
719
+ if (keep)
720
+ this.#socket?.ref();
721
+ else
722
+ this.#socket?.unref();
723
+ }
724
+ newMessage() {
725
+ this.#inbox.reset();
847
726
  }
848
727
  close(now = false) {
849
728
  this.#closed = true;
@@ -854,7 +733,246 @@ class ClientSocket {
854
733
  }
855
734
  }
856
735
 
736
+ // src/client/connect.ts
737
+ var early = new WeakSet;
738
+ async function connect(endpoint, settings, clock) {
739
+ const tls = {
740
+ servername: endpoint.host,
741
+ verify: settings.tls === "required",
742
+ ...settings.ca ? { ca: settings.ca } : {}
743
+ };
744
+ let socket;
745
+ try {
746
+ socket = await ClientSocket.open(endpoint, settings.secure ? tls : undefined, settings.timeouts.connect, clock);
747
+ } catch (error) {
748
+ early.add(error);
749
+ throw error;
750
+ }
751
+ try {
752
+ const ready = await open(socket, settings, tls);
753
+ return { socket, ready };
754
+ } catch (error) {
755
+ early.add(error);
756
+ socket.write(`QUIT\r
757
+ `);
758
+ socket.close();
759
+ throw error;
760
+ }
761
+ }
762
+ function resultOf(delivered, { socket, ready }, endpoint) {
763
+ return {
764
+ ...delivered,
765
+ host: endpoint.host,
766
+ port: endpoint.port,
767
+ tls: socket.secure ? { verified: socket.verified } : false,
768
+ authenticated: ready.authenticated
769
+ };
770
+ }
771
+
772
+ // src/client/settings.ts
773
+ import { hostname as machineName } from "node:os";
774
+
775
+ // src/client/dsn.ts
776
+ var EVENTS = ["SUCCESS", "FAILURE", "DELAY"];
777
+ function dsnOf(given, to, fail) {
778
+ const { ret, envid, recipients } = given;
779
+ if (ret !== undefined && ret !== "FULL" && ret !== "HDRS")
780
+ throw fail(`ret must be 'FULL' or 'HDRS', not ${String(ret)}`);
781
+ if (envid !== undefined && (typeof envid !== "string" || !/^[ -~]{1,100}$/.test(envid) || encodeXtext(envid).length > 100))
782
+ throw fail("envid must be 1 to 100 printable ASCII characters, at most 100 once encoded as xtext");
783
+ const byAddress = new Map;
784
+ if (recipients !== undefined) {
785
+ if (!Array.isArray(recipients))
786
+ throw fail("recipients must be an array of { address, notify, orcpt }");
787
+ for (const entry of recipients) {
788
+ const address = entry?.address;
789
+ if (typeof address !== "string" || !to.includes(address))
790
+ throw fail(`recipients names ${JSON.stringify(address)}, which is not in to`);
791
+ if (byAddress.has(address))
792
+ throw fail(`recipients names ${address} twice`);
793
+ checkRecipient(entry, fail);
794
+ byAddress.set(address, entry);
795
+ }
796
+ }
797
+ return {
798
+ ...ret ? { ret } : {},
799
+ ...envid === undefined ? {} : { envid },
800
+ recipients: byAddress
801
+ };
802
+ }
803
+ function checkRecipient({ address, notify, orcpt }, fail) {
804
+ if (notify !== undefined && notify !== "NEVER") {
805
+ const events = Array.isArray(notify) ? notify : [];
806
+ if (events.length === 0 || events.some((e, i) => !EVENTS.includes(e) || events.indexOf(e) !== i))
807
+ throw fail(`notify of ${address} must be 'NEVER' or a list of 'SUCCESS', 'FAILURE', 'DELAY', each once`);
808
+ }
809
+ if (orcpt !== undefined) {
810
+ if (typeof orcpt !== "object" || orcpt === null || typeof orcpt.type !== "string" || !/^[A-Za-z0-9-]+$/.test(orcpt.type))
811
+ throw fail(`orcpt of ${address} must be { type, address }, the type a token`);
812
+ if (typeof orcpt.address !== "string" || orcpt.address === "")
813
+ throw fail(`orcpt.address of ${address} must be a non-empty string`);
814
+ const encoded = orcpt.type.toLowerCase() === "utf-8" ? utf8OrcptLength(orcpt.address) : encodeXtext(orcpt.address).length;
815
+ if (orcpt.type.length + 1 + encoded > 500)
816
+ throw fail(`orcpt of ${address} is over 500 characters once encoded`);
817
+ }
818
+ }
819
+ function mailDsn(dsn, offered) {
820
+ if (!offered)
821
+ return "";
822
+ let out = "";
823
+ if (dsn.ret)
824
+ out += ` RET=${dsn.ret}`;
825
+ if (dsn.envid !== undefined)
826
+ out += ` ENVID=${encodeXtext(dsn.envid)}`;
827
+ return out;
828
+ }
829
+ function orcptParameter({ type, address }, smtputf8) {
830
+ const kind = type.toLowerCase();
831
+ if (kind === "rfc822")
832
+ return /^[\x20-\x7e]+$/.test(address) ? ` ORCPT=${type};${encodeXtext(address)}` : "";
833
+ if (kind !== "utf-8") {
834
+ const value = `${type};${encodeXtext(address)}`;
835
+ return address.isWellFormed() ? ` ORCPT=${value}` : "";
836
+ }
837
+ const encoded = encodeUtf8Orcpt(address, smtputf8);
838
+ if (encoded === undefined)
839
+ return "";
840
+ const value = `${type};${encoded}`;
841
+ return new TextEncoder().encode(value).length > 500 ? "" : ` ORCPT=${value}`;
842
+ }
843
+ function rcptDsn(dsn, address, offered, smtputf8 = false) {
844
+ const entry = dsn.recipients.get(address);
845
+ if (!offered || !entry)
846
+ return "";
847
+ let out = "";
848
+ const { notify, orcpt } = entry;
849
+ if (notify !== undefined)
850
+ out += ` NOTIFY=${notify === "NEVER" ? "NEVER" : notify.join(",")}`;
851
+ if (orcpt)
852
+ out += orcptParameter(orcpt, smtputf8);
853
+ return out;
854
+ }
855
+
856
+ // src/client/settings.ts
857
+ var MAX_SECONDS = Math.floor(2147483647 / 1000);
858
+ var TIMEOUTS = {
859
+ connect: 30,
860
+ greeting: 300,
861
+ command: 300,
862
+ mail: 300,
863
+ rcpt: 300,
864
+ dataStart: 120,
865
+ dataBlock: 180,
866
+ dataEnd: 600
867
+ };
868
+ var invalid = (message) => new SmtpError2("INVALID_OPTION", `sendMail(): ${message}`);
869
+ function seconds(name, value, fallback) {
870
+ if (value === undefined)
871
+ return fallback;
872
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > MAX_SECONDS) {
873
+ throw invalid(`${name} must be a number of seconds above 0 and at most ${MAX_SECONDS}, not ${String(value)}`);
874
+ }
875
+ return value;
876
+ }
877
+ function timeoutsOf(given = {}) {
878
+ const out = {};
879
+ for (const [name, fallback] of Object.entries(TIMEOUTS)) {
880
+ out[name] = seconds(`timeouts.${name}`, given[name], fallback);
881
+ }
882
+ return out;
883
+ }
884
+ function address(value, allowNull) {
885
+ if (allowNull && value === "")
886
+ return "";
887
+ if (isMailbox(value))
888
+ return value;
889
+ const text = typeof value === "string" ? value : "";
890
+ const routed = typeof value === "string" && parsePath2(`<${text}>`, false);
891
+ if (routed) {
892
+ throw invalid(`${JSON.stringify(value)} holds a source route (@host:), which RFC 5321 says a client should not send: pass ${JSON.stringify(`${routed.local}@${text.slice(text.lastIndexOf("@") + 1)}`)} alone`);
893
+ }
894
+ throw invalid(`${JSON.stringify(value)} is not an address (local@domain${allowNull ? ", or '' for a bounce" : ""})`);
895
+ }
896
+ function heloOf(value, byMx) {
897
+ if (value !== undefined) {
898
+ if (!isHelloName(value))
899
+ throw invalid(`helo "${value}" is not a host name`);
900
+ return value;
901
+ }
902
+ if (byMx)
903
+ throw invalid("helo is required for delivery by MX: pass your server's public name, such as helo: 'mail.example.com'");
904
+ const name = machineName();
905
+ return isHelloName(name) ? name : "localhost";
906
+ }
907
+ function authOf(auth) {
908
+ if (auth === undefined)
909
+ return;
910
+ const { username, password, mechanism } = auth;
911
+ if (typeof username !== "string" || username === "")
912
+ throw invalid("auth.username must be a non-empty string");
913
+ if (typeof password !== "string" || password === "")
914
+ throw invalid("auth.password must be a non-empty string");
915
+ if (mechanism !== undefined && mechanism !== "PLAIN" && mechanism !== "LOGIN")
916
+ throw invalid(`auth.mechanism must be 'PLAIN' or 'LOGIN', not ${mechanism}`);
917
+ return auth;
918
+ }
919
+ function tlsOf(options, plaintextAuth) {
920
+ const secure = options.secure === true;
921
+ const tls = options.tls ?? (secure || options.auth && !plaintextAuth ? "required" : "opportunistic");
922
+ if (tls !== "opportunistic" && tls !== "required" && tls !== "none")
923
+ throw invalid(`tls must be 'opportunistic', 'required' or 'none', not ${tls}`);
924
+ if (tls === "none" && secure)
925
+ throw invalid("secure is TLS from the first byte: it cannot go with tls: 'none'");
926
+ if (tls === "none" && options.auth && !plaintextAuth)
927
+ throw invalid("auth with tls: 'none' would send the password in clear; pass allowPlaintextAuth: true for a local test server");
928
+ if (tls === "opportunistic" && options.auth && !plaintextAuth)
929
+ throw invalid("auth with tls: 'opportunistic' would send the password to a server whose certificate is not checked; leave tls out to check it, or pass allowPlaintextAuth: true for a local test server");
930
+ return tls;
931
+ }
932
+ function settingsOf(options) {
933
+ if (typeof options !== "object" || options === null)
934
+ throw invalid("options must be an object");
935
+ const from = address(options.from, true);
936
+ const list = typeof options.to === "string" ? [options.to] : options.to;
937
+ if (!Array.isArray(list) || list.length === 0)
938
+ throw invalid("to must name one recipient or more");
939
+ const to = list.map((recipient) => address(recipient, false));
940
+ const allowPlaintextAuth = options.allowPlaintextAuth === true;
941
+ const { size } = options;
942
+ if (size !== undefined && (!Number.isSafeInteger(size) || size < 0))
943
+ throw invalid(`size must be a number of bytes, not ${size}`);
944
+ const ca = options.ca === undefined ? undefined : [options.ca].flat();
945
+ const auth = authOf(options.auth);
946
+ return {
947
+ from,
948
+ to,
949
+ helo: heloOf(options.helo, byMx(options)),
950
+ tls: tlsOf(options, allowPlaintextAuth),
951
+ secure: options.secure === true,
952
+ ...ca ? { ca } : {},
953
+ ...auth ? { auth } : {},
954
+ allowPlaintextAuth,
955
+ ...size === undefined ? {} : { size },
956
+ dsn: dsnOf(options, to, invalid),
957
+ smtputf8: options.smtputf8 === true || [from, ...to].some((text) => /[^ -~]/.test(text)),
958
+ normalizeLineEnds: options.normalizeLineEnds === true,
959
+ timeouts: timeoutsOf(options.timeouts),
960
+ deadline: seconds("deadline", options.deadline, 1800)
961
+ };
962
+ }
963
+ var byMx = (options) => !(("host" in options) && options.host !== undefined);
964
+ function portOf(port, fallback) {
965
+ if (port === undefined)
966
+ return fallback;
967
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
968
+ throw invalid(`port must be an integer from 1 to 65535, not ${port}`);
969
+ return port;
970
+ }
971
+
857
972
  // src/client/transaction.ts
973
+ var stepped = new WeakSet;
974
+ var broken = new WeakSet;
975
+ var inStep = (error) => typeof error === "object" && error !== null && stepped.has(error);
858
976
  var missing = (host, extension, why) => new SmtpError2("EXTENSION_MISSING", `${host} does not offer ${extension}, which ${why}`);
859
977
  function mailParameters(host, settings, content, extensions) {
860
978
  let parameters = "";
@@ -883,15 +1001,34 @@ async function envelope(socket, settings, content, ready) {
883
1001
  const { host } = socket;
884
1002
  const { timeouts } = settings;
885
1003
  const dsn = ready.extensions.has("DSN");
886
- const mail = `MAIL FROM:<${settings.from}>${mailParameters(host, settings, content, ready.extensions)}${mailDsn(settings.dsn, dsn)}\r
1004
+ let parameters;
1005
+ try {
1006
+ parameters = mailParameters(host, settings, content, ready.extensions);
1007
+ } catch (error) {
1008
+ stepped.add(error);
1009
+ throw error;
1010
+ }
1011
+ const mail = `MAIL FROM:<${settings.from}>${parameters}${mailDsn(settings.dsn, dsn)}\r
887
1012
  `;
888
1013
  const rcpts = settings.to.map((to) => `RCPT TO:<${to}>${rcptDsn(settings.dsn, to, dsn, settings.smtputf8)}\r
889
1014
  `);
890
1015
  const pipelining = ready.extensions.has("PIPELINING");
891
1016
  socket.write(pipelining ? mail + rcpts.join("") : mail);
892
1017
  const from = await socket.reply("the reply to MAIL FROM", timeouts.mail);
893
- if (from.code !== 250)
894
- throw refused(host, "the sender", from);
1018
+ if (from.code !== 250) {
1019
+ const refusal = refused(host, "the sender", from);
1020
+ if (pipelining) {
1021
+ for (const _ of rcpts) {
1022
+ try {
1023
+ await socket.reply("the reply to RCPT TO", timeouts.rcpt);
1024
+ } catch {
1025
+ broken.add(refusal);
1026
+ break;
1027
+ }
1028
+ }
1029
+ }
1030
+ throw refusal;
1031
+ }
895
1032
  const accepted = [];
896
1033
  const rejected = [];
897
1034
  for (const [i, recipient] of settings.to.entries()) {
@@ -954,63 +1091,49 @@ async function data(socket, settings, content, ready, taken) {
954
1091
  return end;
955
1092
  }
956
1093
  async function transact(socket, settings, content, ready) {
957
- const { accepted, rejected } = await envelope(socket, settings, content, ready);
958
- const reply = await data(socket, settings, content, ready, {
959
- accepted,
960
- rejected
961
- });
962
- return {
963
- accepted,
964
- rejected,
965
- reply,
966
- dsnOffered: ready.extensions.has("DSN")
967
- };
1094
+ try {
1095
+ const { accepted, rejected } = await envelope(socket, settings, content, ready);
1096
+ const reply = await data(socket, settings, content, ready, {
1097
+ accepted,
1098
+ rejected
1099
+ });
1100
+ const delivered = {
1101
+ accepted,
1102
+ rejected,
1103
+ reply,
1104
+ dsnOffered: ready.extensions.has("DSN")
1105
+ };
1106
+ return { delivered };
1107
+ } catch (error) {
1108
+ const code = error instanceof SmtpError2 ? error.reply?.code : undefined;
1109
+ if (code !== undefined && code >= 400 && code < 600 && code !== 421 && !broken.has(error))
1110
+ stepped.add(error);
1111
+ throw error;
1112
+ }
968
1113
  }
969
1114
 
970
1115
  // src/client/send.ts
971
1116
  var MAX_ADDRESSES = 10;
972
1117
  var QUIT_WAIT = 5;
973
- var early = new WeakSet;
974
- async function attempt(endpoint, settings, content, clock) {
975
- const tls = {
976
- servername: endpoint.host,
977
- verify: settings.tls === "required",
978
- ...settings.ca ? { ca: settings.ca } : {}
979
- };
980
- let socket;
981
- try {
982
- socket = await ClientSocket.open(endpoint, settings.secure ? tls : undefined, settings.timeouts.connect, clock);
983
- } catch (error) {
984
- early.add(error);
985
- throw error;
986
- }
1118
+ var attempt = async (endpoint, settings, content, clock) => {
1119
+ const { socket, ready } = await connect(endpoint, settings, clock);
987
1120
  let quit = false;
988
1121
  try {
989
- const ready = await open(socket, settings, tls).catch((error) => {
990
- early.add(error);
991
- throw error;
992
- });
993
- const delivered = await transact(socket, settings, content, ready);
1122
+ const { delivered } = await transact(socket, settings, content, ready);
994
1123
  socket.write(`QUIT\r
995
1124
  `);
996
1125
  quit = true;
997
1126
  await socket.reply("the reply to QUIT", Math.min(QUIT_WAIT, settings.timeouts.command)).catch(() => {});
998
- return {
999
- ...delivered,
1000
- host: endpoint.host,
1001
- port: endpoint.port,
1002
- tls: socket.secure ? { verified: socket.verified } : false,
1003
- authenticated: ready.authenticated
1004
- };
1127
+ return resultOf(delivered, { socket, ready }, endpoint);
1005
1128
  } finally {
1006
1129
  if (!quit)
1007
1130
  socket.write(`QUIT\r
1008
1131
  `);
1009
1132
  socket.close();
1010
1133
  }
1011
- }
1134
+ };
1012
1135
  var withinDeadline = (lookup, clock, name, what) => clock.race(lookup, Number.POSITIVE_INFINITY, () => timedOut(name, what, clock.seconds, clock.seconds));
1013
- async function viaMx(options, settings, content, clock) {
1136
+ async function viaMx(try_, options, settings, content, clock) {
1014
1137
  const { domain, resolver } = options;
1015
1138
  if (typeof domain !== "string" || domain === "")
1016
1139
  throw invalid("domain must be a domain name");
@@ -1035,7 +1158,7 @@ async function viaMx(options, settings, content, clock) {
1035
1158
  if (tried++ >= MAX_ADDRESSES)
1036
1159
  break hosts;
1037
1160
  try {
1038
- return await attempt({ host: mx.host, address, port }, settings, content, clock);
1161
+ return await try_({ host: mx.host, address, port }, settings, content, clock);
1039
1162
  } catch (error) {
1040
1163
  const next = error instanceof SmtpError2 && error.temporary && early.has(error);
1041
1164
  if (!next)
@@ -1046,25 +1169,237 @@ async function viaMx(options, settings, content, clock) {
1046
1169
  }
1047
1170
  throw last ?? noAddress(domain, dnsError);
1048
1171
  }
1049
- async function sendMail(message, options) {
1050
- const settings = settingsOf(options);
1051
- const content = contentOf(message, settings.normalizeLineEnds, settings.size);
1052
- const clock = new Clock(settings.deadline);
1172
+ function deliver(try_, options, settings, content, clock) {
1053
1173
  if (!byMx(options)) {
1054
1174
  const { host } = options;
1055
1175
  if (typeof host !== "string" || !/^[^\s/]+$/.test(host))
1056
1176
  throw invalid(`host must be a host name or an address, not ${host}`);
1057
1177
  const port = portOf(options.port, settings.secure ? 465 : 25);
1058
- return attempt({ host, address: host, port }, settings, content, clock);
1178
+ return try_({ host, address: host, port }, settings, content, clock);
1059
1179
  }
1060
- return viaMx(options, settings, content, clock);
1180
+ return viaMx(try_, options, settings, content, clock);
1181
+ }
1182
+ async function sendMail(message, options) {
1183
+ const settings = settingsOf(options);
1184
+ const content = contentOf(message, settings.normalizeLineEnds, settings.size);
1185
+ return deliver(attempt, options, settings, content, new Clock(settings.deadline));
1186
+ }
1187
+
1188
+ // src/client/pool/options.ts
1189
+ var invalid2 = (message) => new SmtpError2("INVALID_OPTION", `createSmtpPool(): ${message}`);
1190
+ function count(name, value, fallback) {
1191
+ if (value === undefined)
1192
+ return fallback;
1193
+ if (!Number.isSafeInteger(value) || value < 1)
1194
+ throw invalid2(`${name} must be an integer of at least 1, not ${String(value)}`);
1195
+ return value;
1196
+ }
1197
+ function seconds2(name, value, fallback) {
1198
+ if (value === undefined)
1199
+ return fallback;
1200
+ if (!Number.isFinite(value) || value <= 0 || value > MAX_SECONDS)
1201
+ throw invalid2(`${name} must be a number of seconds above 0 and at most ${MAX_SECONDS}, not ${String(value)}`);
1202
+ return value;
1203
+ }
1204
+ function limitsOf(options = {}) {
1205
+ if (typeof options !== "object" || options === null)
1206
+ throw invalid2("options must be an object");
1207
+ return {
1208
+ maxMessages: count("maxMessages", options.maxMessages, 100),
1209
+ idleSeconds: seconds2("idleSeconds", options.idleSeconds, 5),
1210
+ maxAgeSeconds: seconds2("maxAgeSeconds", options.maxAgeSeconds, 300),
1211
+ maxIdlePerHost: count("maxIdlePerHost", options.maxIdlePerHost, 4)
1212
+ };
1213
+ }
1214
+ var sameList = (a, b) => a === b || a !== undefined && b !== undefined && a.length === b.length && a.every((item, i) => item === b[i]);
1215
+ var sameAuth = (a, b) => a === b || a !== undefined && b !== undefined && a.username === b.username && a.password === b.password && a.mechanism === b.mechanism;
1216
+ function sameIdentity(a, b) {
1217
+ const x = a.connection;
1218
+ const y = b.connection;
1219
+ return a.host === b.host && a.address === b.address && a.port === b.port && x.helo === y.helo && x.tls === y.tls && x.secure === y.secure && x.allowPlaintextAuth === y.allowPlaintextAuth && sameList(x.ca, y.ca) && sameAuth(x.auth, y.auth);
1220
+ }
1221
+ var identityOf = (endpoint, settings) => ({
1222
+ host: endpoint.host,
1223
+ address: endpoint.address,
1224
+ port: endpoint.port,
1225
+ connection: {
1226
+ helo: settings.helo,
1227
+ tls: settings.tls,
1228
+ secure: settings.secure,
1229
+ allowPlaintextAuth: settings.allowPlaintextAuth,
1230
+ ...settings.ca ? { ca: [...settings.ca] } : {},
1231
+ ...settings.auth ? { auth: { ...settings.auth } } : {}
1232
+ }
1233
+ });
1234
+
1235
+ // src/client/pool/reset.ts
1236
+ var RESET_WAIT = 5;
1237
+ async function reset(entry, clock, seconds) {
1238
+ const { socket } = entry;
1239
+ if (!socket.quiet)
1240
+ return false;
1241
+ socket.clock = clock;
1242
+ socket.write(`RSET\r
1243
+ `);
1244
+ try {
1245
+ return (await socket.reply("the reply to RSET", seconds)).code === 250;
1246
+ } catch {
1247
+ return false;
1248
+ }
1249
+ }
1250
+ var stale = (entry, before, error) => error instanceof SmtpError2 && error.code === "CONNECTION_LOST" && error.reply === undefined && !error.afterData && entry.socket.replies === before;
1251
+
1252
+ // src/client/pool/state.ts
1253
+ var newState = (limits) => ({
1254
+ limits,
1255
+ idle: [],
1256
+ busy: new Set,
1257
+ resetting: new Set,
1258
+ closed: false
1259
+ });
1260
+ function hangUp(entry, polite = true) {
1261
+ clearTimeout(entry.timer);
1262
+ if (polite)
1263
+ entry.socket.write(`QUIT\r
1264
+ `);
1265
+ entry.socket.close();
1266
+ }
1267
+ var aged = (state, entry) => performance.now() - entry.born >= state.limits.maxAgeSeconds * 1000;
1268
+
1269
+ // src/client/pool/idle.ts
1270
+ function forget(state, entry) {
1271
+ const at = state.idle.indexOf(entry);
1272
+ if (at >= 0)
1273
+ state.idle.splice(at, 1);
1274
+ clearTimeout(entry.timer);
1275
+ }
1276
+ function release(state, entry) {
1277
+ const { limits, idle } = state;
1278
+ const spent = state.closed || entry.messages >= limits.maxMessages || aged(state, entry) || !entry.socket.quiet;
1279
+ for (const other of idle.filter((e) => !e.socket.quiet)) {
1280
+ forget(state, other);
1281
+ hangUp(other, false);
1282
+ }
1283
+ const same = idle.filter((e) => e.identity.host === entry.identity.host);
1284
+ if (spent || same.length >= limits.maxIdlePerHost) {
1285
+ hangUp(entry);
1286
+ return;
1287
+ }
1288
+ entry.timer = setTimeout(() => {
1289
+ forget(state, entry);
1290
+ hangUp(entry);
1291
+ }, limits.idleSeconds * 1000);
1292
+ entry.timer.unref();
1293
+ entry.socket.ref(false);
1294
+ idle.push(entry);
1295
+ }
1296
+ async function borrow(state, identity, seconds, clock) {
1297
+ for (;; ) {
1298
+ const entry = state.idle.find((e) => sameIdentity(e.identity, identity));
1299
+ if (!entry)
1300
+ return;
1301
+ forget(state, entry);
1302
+ entry.socket.ref(true);
1303
+ if (aged(state, entry)) {
1304
+ hangUp(entry, true);
1305
+ continue;
1306
+ }
1307
+ if (await reset(entry, clock, seconds))
1308
+ return entry;
1309
+ hangUp(entry, false);
1310
+ }
1311
+ }
1312
+
1313
+ // src/client/pool/send.ts
1314
+ class Stale extends Error {
1315
+ }
1316
+ function recover(state, entry) {
1317
+ const job = reset(entry, new Clock(RESET_WAIT), RESET_WAIT).then((ok) => ok ? release(state, entry) : hangUp(entry, false)).catch(() => hangUp(entry, false));
1318
+ state.resetting.add(job);
1319
+ job.finally(() => state.resetting.delete(job));
1320
+ }
1321
+ async function send(state, entry, settings, content, clock, reused) {
1322
+ const { socket, ready } = entry;
1323
+ socket.clock = clock;
1324
+ socket.newMessage();
1325
+ const before = socket.replies;
1326
+ let result;
1327
+ try {
1328
+ const { delivered } = await transact(socket, settings, content, ready);
1329
+ result = {
1330
+ ...resultOf(delivered, entry, entry.identity),
1331
+ reused
1332
+ };
1333
+ } catch (error) {
1334
+ if (inStep(error)) {
1335
+ entry.messages++;
1336
+ recover(state, entry);
1337
+ } else
1338
+ hangUp(entry);
1339
+ if (reused && stale(entry, before, error))
1340
+ throw new Stale;
1341
+ throw error;
1342
+ }
1343
+ entry.messages++;
1344
+ release(state, entry);
1345
+ return result;
1346
+ }
1347
+ async function attempt2(state, endpoint, settings, content, clock) {
1348
+ const identity = identityOf(endpoint, settings);
1349
+ const wait = Math.min(RESET_WAIT, settings.timeouts.command);
1350
+ const entry = await borrow(state, identity, wait, clock);
1351
+ if (entry) {
1352
+ try {
1353
+ return await send(state, entry, settings, content, clock, true);
1354
+ } catch (error) {
1355
+ if (!(error instanceof Stale))
1356
+ throw error;
1357
+ }
1358
+ }
1359
+ const { socket, ready } = await connect(endpoint, settings, clock);
1360
+ const fresh = {
1361
+ socket,
1362
+ ready,
1363
+ identity,
1364
+ born: performance.now(),
1365
+ messages: 0
1366
+ };
1367
+ return send(state, fresh, settings, content, clock, false);
1368
+ }
1369
+
1370
+ // src/client/pool/index.ts
1371
+ function createSmtpPool(options) {
1372
+ const state = newState(limitsOf(options));
1373
+ return {
1374
+ sendMail(message, sendOptions) {
1375
+ const job = (async () => {
1376
+ if (state.closed)
1377
+ throw new SmtpError2("STOPPED", "The SMTP pool is closed");
1378
+ const settings = settingsOf(sendOptions);
1379
+ const content = contentOf(message, settings.normalizeLineEnds, settings.size);
1380
+ return deliver((endpoint, s, c, clock) => attempt2(state, endpoint, s, c, clock), sendOptions, settings, content, new Clock(settings.deadline));
1381
+ })();
1382
+ state.busy.add(job);
1383
+ const done = () => state.busy.delete(job);
1384
+ job.then(done, done);
1385
+ return job;
1386
+ },
1387
+ async close() {
1388
+ state.closed = true;
1389
+ for (const entry of state.idle.splice(0))
1390
+ hangUp(entry);
1391
+ await Promise.allSettled([...state.busy]);
1392
+ await Promise.allSettled([...state.resetting]);
1393
+ }
1394
+ };
1061
1395
  }
1062
1396
  export {
1063
1397
  SmtpError2 as SmtpError,
1398
+ createSmtpPool,
1064
1399
  isMailbox,
1065
1400
  resolveMx,
1066
1401
  sendMail
1067
1402
  };
1068
1403
 
1069
- //# debugId=3C3AEF3D2B40447E64756E2164756E21
1404
+ //# debugId=7F1B42BBCDFFD91464756E2164756E21
1070
1405
  //# sourceMappingURL=index.js.map