sently 0.4.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.2] — 2026-05-30
4
+
5
+ ### Fixed
6
+
7
+ - CI: replaced node -e dynamic import with scripts/smoke.mjs
8
+ (top-level await ESM) for reliable Node.js smoke testing
9
+ - CI: integration test now imports from dist/index.js directly
10
+ instead of relying on package self-reference resolution
11
+ - CI: updated GitHub Actions to latest patch versions
12
+
13
+ ## [0.4.1] — 2026-05-30
14
+
15
+ ### Fixed
16
+
17
+ - CI: use locally installed tsc (node_modules/.bin/tsc) in build.ts
18
+ instead of bunx tsc to avoid runtime npm downloads in CI
19
+ - CI: add bun run build step to SMTP integration job so dist/ exists
20
+ before the integration test imports from 'sently'
21
+ - CI: improved smoke test error output with explicit .catch() handler
22
+ - CI: added FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 to all workflow jobs
23
+
3
24
  ## [0.4.0] — 2026-05-30
4
25
 
5
26
  ### Added
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # sently
2
2
 
3
- **Runtime-agnostic email library for Node.js, Bun, Deno, and Cloudflare Workers.**
3
+ > Nodemailer hasn't been updated in years, doesn't run on Bun or Deno, and ships at 220KB.
4
+ > sently is the modern replacement — same familiar API, runs everywhere, tree-shakes to ~6KB.
5
+
6
+ ```bash
7
+ bun add sently
8
+ ```
4
9
 
5
10
  [![npm version](https://img.shields.io/npm/v/sently.svg)](https://www.npmjs.com/package/sently)
6
11
  [![JSR](https://jsr.io/badges/@alialnaghmoush/sently)](https://jsr.io/@alialnaghmoush/sently)
@@ -11,17 +16,62 @@
11
16
 
12
17
  ---
13
18
 
14
- ## Why sently
19
+ ## Why not Nodemailer?
20
+
21
+ | Feature | Nodemailer | sently |
22
+ |---------|-----------|--------|
23
+ | Bundle size | ~220 KB | ~6 KB core |
24
+ | Runtimes | Node.js only | Node, Bun, Deno, CF Workers |
25
+ | Module format | CommonJS | ESM only |
26
+ | Dependencies | 3 | 0 |
27
+ | DKIM signing | ✓ via `nodemailer-dkim` | ✓ built-in (Web Crypto) |
28
+ | OAuth2 / XOAUTH2 | ✓ via plugin | ✓ built-in |
29
+ | Connection pooling | ✓ | ✓ |
30
+ | HTTP transports | ✓ via plugins | ✓ built-in (6 providers) |
31
+ | Retry transport | ✗ | ✓ |
32
+ | Preview transport | ✗ | ✓ |
33
+ | Template engine | ✗ | ✓ |
34
+ | `sendBulk()` | ✗ | ✓ |
35
+ | TypeScript | via `@types/nodemailer` | ✓ built-in |
36
+ | Last release | 2021 | 2026 |
37
+
38
+ ---
39
+
40
+ ## The 30-second tour
41
+
42
+ ```typescript
43
+ import { createMailer, type MailOptions } from "sently";
44
+ import { ResendTransport } from "sently/transports/resend";
45
+ import { PreviewTransport } from "sently/transports/preview";
46
+
47
+ const addFooter = (options: MailOptions): MailOptions => ({
48
+ ...options,
49
+ html: (options.html ?? "") + '<p style="color:#999">Unsubscribe</p>',
50
+ });
51
+
52
+ // Swap providers without changing send code
53
+ const mailer = await createMailer({
54
+ transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
55
+ plugins: [addFooter],
56
+ });
15
57
 
16
- - **Works everywhere** — Node.js, Bun, Deno, Cloudflare Workers, and any environment with Web APIs
17
- - **True tree-shaking** — import only what you need; unused adapters and transports stay out of your bundle
18
- - **Zero dependencies in core** — MIME, SMTP protocol, and encoding use pure Web APIs only
19
- - **Plugin system** — transform `MailOptions` before send with composable middleware
20
- - **HTTP transports** — Resend, SendGrid, Postmark, Mailgun, AWS SES, and Brevo
21
- - **DKIM signing** — RSA-SHA256 and Ed25519-SHA256 via Web Crypto
22
- - **OAuth2 / XOAUTH2** — Gmail and Microsoft 365 SMTP auth with automatic token refresh
23
- - **Connection pooling** — reuse SMTP sessions with optional rate limiting
24
- - **TypeScript-first** — strict types, subpath exports, and full IDE support
58
+ await mailer.send({
59
+ from: "you@example.com",
60
+ to: "recipient@example.com",
61
+ subject: "Hello from sently",
62
+ html: "<p>Hello!</p>",
63
+ });
64
+
65
+ // Bulk send with concurrency control
66
+ await mailer.sendBulk(recipients, { concurrency: 5 });
67
+
68
+ // Local dev — write to disk instead of sending
69
+ const devMailer = await createMailer({
70
+ transport: process.env.CI
71
+ ? new ResendTransport({ apiKey: process.env.RESEND_API_KEY! })
72
+ : new PreviewTransport({ outDir: ".emails", open: true }),
73
+ });
74
+ ```
25
75
 
26
76
  ---
27
77
 
@@ -228,63 +278,19 @@ const pool = new SMTPPool({
228
278
 
229
279
  ### HTTP APIs
230
280
 
231
- #### Resend
232
-
233
- ```typescript
234
- import { ResendTransport } from "sently/transports/resend";
235
-
236
- const transport = new ResendTransport({ apiKey: "re_..." });
237
- ```
238
-
239
- #### SendGrid
240
-
241
- ```typescript
242
- import { SendGridTransport } from "sently/transports/sendgrid";
243
-
244
- const transport = new SendGridTransport({ apiKey: "SG...." });
245
- ```
246
-
247
- #### Postmark
248
-
249
- ```typescript
250
- import { PostmarkTransport } from "sently/transports/postmark";
251
-
252
- const transport = new PostmarkTransport({ serverToken: "..." });
253
- ```
254
-
255
- #### Mailgun
256
-
257
- ```typescript
258
- import { MailgunTransport } from "sently/transports/mailgun";
259
-
260
- const transport = new MailgunTransport({
261
- apiKey: "key-...",
262
- domain: "mg.example.com",
263
- });
264
- ```
281
+ | Transport | Import path | Required config |
282
+ |-----------|-------------|-----------------|
283
+ | Resend | `sently/transports/resend` | `apiKey` |
284
+ | SendGrid | `sently/transports/sendgrid` | `apiKey` |
285
+ | Postmark | `sently/transports/postmark` | `serverToken` |
286
+ | Mailgun | `sently/transports/mailgun` | `apiKey`, `domain` |
287
+ | AWS SES | `sently/transports/ses` | `accessKeyId`, `secretAccessKey`, `region` |
288
+ | Brevo | `sently/transports/brevo` | `apiKey` |
265
289
 
266
- #### AWS SES
267
-
268
- ```typescript
269
- import { SESTransport } from "sently/transports/ses";
270
-
271
- const transport = new SESTransport({
272
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
273
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
274
- region: "us-east-1",
275
- });
276
- ```
290
+ All transports implement the same interface — swap without changing your send code.
277
291
 
278
292
  Messages with attachments are sent as raw MIME (`Content.Raw`); simple messages use `Content.Simple`.
279
293
 
280
- #### Brevo
281
-
282
- ```typescript
283
- import { BrevoTransport } from "sently/transports/brevo";
284
-
285
- const transport = new BrevoTransport({ apiKey: "xkeysib-..." });
286
- ```
287
-
288
294
  ### PreviewTransport
289
295
 
290
296
  Write emails to disk during local development instead of sending them:
@@ -346,6 +352,38 @@ const result = await mailer.sendBulk(
346
352
  console.log(result.sent, result.failed);
347
353
  ```
348
354
 
355
+ ---
356
+
357
+ ## Plugin system
358
+
359
+ Plugins transform `MailOptions` before the transport builds and sends the message. They run sequentially — each receives the output of the previous plugin.
360
+
361
+ ```typescript
362
+ import { createMailer, type MailOptions } from "sently";
363
+
364
+ const addFooter = (options: MailOptions) => ({
365
+ ...options,
366
+ html: (options.html ?? "") + '<p style="color:#999">Unsubscribe</p>',
367
+ });
368
+
369
+ const mailer = await createMailer({
370
+ host: "smtp.resend.com",
371
+ port: 465,
372
+ secure: true,
373
+ auth: { user: "resend", pass: process.env.RESEND_API_KEY! },
374
+ plugins: [addFooter],
375
+ });
376
+ ```
377
+
378
+ Works with SMTP config or custom transports:
379
+
380
+ ```typescript
381
+ const mailer = await createMailer({
382
+ transport: new ResendTransport({ apiKey: "re_..." }),
383
+ plugins: [addFooter],
384
+ });
385
+ ```
386
+
349
387
  ### TemplatePlugin
350
388
 
351
389
  Render HTML from named templates with zero dependencies:
@@ -378,36 +416,6 @@ await mailer.send({
378
416
 
379
417
  Use a custom engine by passing any `(template, data) => string` function to `templatePlugin`.
380
418
 
381
- ### Plugin system
382
-
383
- Plugins transform `MailOptions` before the transport builds and sends the message. They run sequentially — each receives the output of the previous plugin.
384
-
385
- ```typescript
386
- import { createMailer, type MailOptions } from "sently";
387
-
388
- const addFooter = (options: MailOptions) => ({
389
- ...options,
390
- html: (options.html ?? "") + '<p style="color:#999">Unsubscribe</p>',
391
- });
392
-
393
- const mailer = await createMailer({
394
- host: "smtp.resend.com",
395
- port: 465,
396
- secure: true,
397
- auth: { user: "resend", pass: process.env.RESEND_API_KEY! },
398
- plugins: [addFooter],
399
- });
400
- ```
401
-
402
- Works with SMTP config or custom transports:
403
-
404
- ```typescript
405
- const mailer = await createMailer({
406
- transport: new ResendTransport({ apiKey: "re_..." }),
407
- plugins: [addFooter],
408
- });
409
- ```
410
-
411
419
  ---
412
420
 
413
421
  ## MailOptions Reference
@@ -469,6 +477,35 @@ On Cloudflare Workers and browsers, use `content: Uint8Array` — `attachment.pa
469
477
 
470
478
  ---
471
479
 
480
+ ## Error Handling
481
+
482
+ ```typescript
483
+ import { SMTPError } from "sently";
484
+ import { ResendError } from "sently/transports/resend";
485
+ // Each HTTP transport exports its own error class:
486
+ // SendGridError → sently/transports/sendgrid
487
+ // PostmarkError → sently/transports/postmark
488
+ // MailgunError → sently/transports/mailgun
489
+ // SESError → sently/transports/ses
490
+ // BrevoError → sently/transports/brevo
491
+
492
+ try {
493
+ await mailer.send({ ... });
494
+ } catch (err) {
495
+ if (err instanceof SMTPError) {
496
+ console.error(err.code); // SMTP response code, e.g. 550
497
+ console.error(err.command); // failed command, e.g. "RCPT TO"
498
+ }
499
+ if (err instanceof ResendError) {
500
+ console.error(err.statusCode); // HTTP status code
501
+ }
502
+ }
503
+ ```
504
+
505
+ Import error classes from their transport subpath — not from `sently` core. Each exports a `statusCode` property on HTTP failures.
506
+
507
+ ---
508
+
472
509
  ## Tree-Shaking
473
510
 
474
511
  Each import path is a separate build entry point:
@@ -518,6 +555,24 @@ Approximate gzip sizes per subpath export:
518
555
  | `sently/adapters/deno` | ~2 KB |
519
556
  | `sently/adapters/cf` | ~2 KB |
520
557
 
558
+ > **Example:** Resend only = core (~6 KB) + transport (~2 KB) = **~8 KB total**. Nodemailer ships 220 KB regardless of which transport you use.
559
+
560
+ ---
561
+
562
+ ## TypeScript
563
+
564
+ ```typescript
565
+ import type {
566
+ MailOptions,
567
+ MailPlugin,
568
+ SendResult,
569
+ Attachment,
570
+ SMTPConfig,
571
+ } from "sently";
572
+ ```
573
+
574
+ All types ship with the package — no separate `@types/` install needed.
575
+
521
576
  ---
522
577
 
523
578
  ## Links
@@ -1,3 +1,6 @@
1
+ import {
2
+ OAuth2Client
3
+ } from "./chunk-ym3zzv8b.js";
1
4
  import {
2
5
  buildMIME
3
6
  } from "./chunk-j6qw8ms6.js";
@@ -13,10 +16,7 @@ import {
13
16
  parseEHLO,
14
17
  parseResponse,
15
18
  selectAuthMethod
16
- } from "./chunk-tjsgb3qb.js";
17
- import {
18
- OAuth2Client
19
- } from "./chunk-ym3zzv8b.js";
19
+ } from "./chunk-sbydk09g.js";
20
20
  import {
21
21
  resolveAttachments
22
22
  } from "./chunk-bvxkmq94.js";
@@ -100,7 +100,8 @@ async function openSMTPSession(adapter, config) {
100
100
  const greeting = await readSMTPResponse(adapter);
101
101
  assertResponse(greeting, [220], "greeting");
102
102
  let capabilities = await ehlo(adapter, config.host);
103
- if (!config.secure && !adapter.secure) {
103
+ const supportsStartTls = capabilities.some((cap) => cap.toUpperCase() === "STARTTLS");
104
+ if (!config.secure && !adapter.secure && supportsStartTls) {
104
105
  await sendRaw(adapter, encodeCommand({ type: "STARTTLS" }));
105
106
  const starttlsResp = await readSMTPResponse(adapter);
106
107
  assertResponse(starttlsResp, [220], "STARTTLS");
@@ -241,4 +242,4 @@ async function resolveMX(domain) {
241
242
  }
242
243
  export { SMTPTransport, resolveSMTPConfig, openSMTPSession, deliverSMTPMessage, closeSMTPSession, readSMTPResponse };
243
244
 
244
- //# debugId=58BDA0CC5C272AF664756E2164756E21
245
+ //# debugId=44FB4148CD9604AA64756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/transports/smtp.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @module\n * SMTP transport — orchestrates socket adapter, MIME builder, and protocol logic.\n *\n * @example\n * ```ts\n * import { SMTPTransport } from \"sently/transports/smtp\";\n * import { NodeAdapter } from \"sently/adapters/node\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * transport: new SMTPTransport({\n * host: \"smtp.example.com\",\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * adapter: new NodeAdapter(),\n * }),\n * });\n * ```\n */\nimport { OAuth2Client } from \"../auth/oauth2.js\";\nimport { buildMIME, type MIMEBuildResult } from \"../core/mime.js\";\nimport type { SMTPResponse } from \"../core/smtp.js\";\nimport {\n accumulateResponse,\n assertResponse,\n computeCRAMMD5,\n encodeAuthLoginPass,\n encodeAuthLoginUser,\n encodeCommand,\n encodeLine,\n parseEHLO,\n parseResponse,\n SMTPError,\n selectAuthMethod,\n} from \"../core/smtp.js\";\nimport type {\n MailOptions,\n SendResult,\n SMTPConfig,\n SocketAdapter,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/**\n * SMTP transport orchestrating adapter, MIME builder, and protocol logic.\n */\nexport class SMTPTransport implements Transport {\n private readonly config: ResolvedSMTPConfig;\n private adapter: SocketAdapter | null = null;\n\n /** Creates an SMTP transport with the given configuration. */\n constructor(config: SMTPConfig) {\n this.config = resolveSMTPConfig(config);\n }\n\n /** Sends an email via SMTP using the configured adapter. */\n async send(options: MailOptions): Promise<SendResult> {\n const resolvedOptions = {\n ...options,\n attachments: await resolveAttachments(options.attachments),\n };\n const mime = await buildMIME(resolvedOptions, this.config.dkim);\n const adapter = await this.getAdapter();\n\n const host = this.config.direct\n ? await resolveMX(mime.envelope.from.split(\"@\")[1] ?? this.config.host)\n : this.config.host;\n\n await adapter.connect(host, this.config.port);\n this.adapter = adapter;\n\n try {\n await openSMTPSession(adapter, this.config);\n return await deliverSMTPMessage(adapter, mime);\n } finally {\n await closeSMTPSession(adapter);\n this.adapter = null;\n }\n }\n\n /** Verifies SMTP connectivity and authentication without sending mail. */\n async verify(): Promise<VerifyResult> {\n try {\n const adapter = await this.getAdapter();\n await adapter.connect(this.config.host, this.config.port);\n\n try {\n await openSMTPSession(adapter, this.config);\n return { ok: true, provider: \"smtp\" };\n } finally {\n await closeSMTPSession(adapter);\n }\n } catch (err) {\n return {\n ok: false,\n provider: \"smtp\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n\n /** Closes the underlying socket adapter if connected. */\n async close(): Promise<void> {\n if (this.adapter) {\n await this.adapter.close();\n this.adapter = null;\n }\n }\n\n private async getAdapter(): Promise<SocketAdapter> {\n if (!this.config.adapter) {\n throw new SMTPError(\"No socket adapter configured\", 0, \"CONNECT\", \"\");\n }\n return this.config.adapter;\n }\n}\n\n/** Resolved SMTP transport configuration with defaults applied. */\nexport interface ResolvedSMTPConfig {\n host: string;\n port: number;\n secure: boolean;\n auth?: SMTPConfig[\"auth\"];\n tls?: SMTPConfig[\"tls\"];\n dkim?: SMTPConfig[\"dkim\"];\n connectionTimeout?: number;\n greetingTimeout?: number;\n socketTimeout?: number;\n direct?: boolean;\n adapter?: SocketAdapter;\n}\n\n/** Apply defaults to SMTP configuration. */\nexport function resolveSMTPConfig(config: SMTPConfig): ResolvedSMTPConfig {\n const secure = config.secure ?? false;\n return {\n host: config.host,\n port: config.port ?? (secure ? 465 : 587),\n secure,\n ...(config.auth !== undefined ? { auth: config.auth } : {}),\n ...(config.dkim !== undefined ? { dkim: config.dkim } : {}),\n ...(config.tls !== undefined ? { tls: config.tls } : {}),\n ...(config.connectionTimeout !== undefined\n ? { connectionTimeout: config.connectionTimeout }\n : {}),\n ...(config.greetingTimeout !== undefined ? { greetingTimeout: config.greetingTimeout } : {}),\n ...(config.socketTimeout !== undefined ? { socketTimeout: config.socketTimeout } : {}),\n ...(config.direct !== undefined ? { direct: config.direct } : {}),\n ...(config.adapter !== undefined ? { adapter: config.adapter } : {}),\n };\n}\n\n/**\n * Connect greeting, EHLO, optional STARTTLS, and AUTH on an open adapter.\n */\nexport async function openSMTPSession(\n adapter: SocketAdapter,\n config: ResolvedSMTPConfig,\n): Promise<void> {\n const greeting = await readSMTPResponse(adapter);\n assertResponse(greeting, [220], \"greeting\");\n\n let capabilities = await ehlo(adapter, config.host);\n const supportsStartTls = capabilities.some((cap) => cap.toUpperCase() === \"STARTTLS\");\n if (!config.secure && !adapter.secure && supportsStartTls) {\n await sendRaw(adapter, encodeCommand({ type: \"STARTTLS\" }));\n const starttlsResp = await readSMTPResponse(adapter);\n assertResponse(starttlsResp, [220], \"STARTTLS\");\n await adapter.startTLS(config.tls);\n capabilities = await ehlo(adapter, config.host);\n }\n\n if (config.auth) {\n await authenticate(adapter, config.auth, capabilities);\n }\n}\n\n/**\n * MAIL FROM, RCPT TO, and DATA for a built MIME message on an authenticated session.\n */\nexport async function deliverSMTPMessage(\n adapter: SocketAdapter,\n mime: MIMEBuildResult,\n): Promise<SendResult> {\n await sendCommand(adapter, { type: \"MAIL_FROM\", address: mime.envelope.from });\n const mailResp = await readSMTPResponse(adapter);\n assertResponse(mailResp, [250], \"MAIL FROM\");\n\n const accepted: string[] = [];\n const rejected: string[] = [];\n\n for (const recipient of mime.envelope.to) {\n await sendRaw(adapter, encodeCommand({ type: \"RCPT_TO\", address: recipient }));\n const rcptResp = await readSMTPResponse(adapter);\n if (rcptResp.isSuccess) {\n accepted.push(recipient);\n } else {\n rejected.push(recipient);\n }\n }\n\n await sendCommand(adapter, { type: \"DATA\" });\n const dataResp = await readSMTPResponse(adapter);\n assertResponse(dataResp, [354], \"DATA\");\n\n let finalResp: SMTPResponse;\n try {\n await sendRaw(adapter, encodeCommand({ type: \"DATA_BODY\", content: mime.raw }));\n finalResp = await readSMTPResponse(adapter);\n } catch (err) {\n await sendRaw(adapter, encodeCommand({ type: \"DATA_BODY\", content: mime.raw }));\n finalResp = await readSMTPResponse(adapter);\n if (finalResp.isError) {\n throw err;\n }\n }\n assertResponse(finalResp, [250], \"DATA end\");\n\n return {\n messageId: mime.messageId,\n accepted,\n rejected,\n response: finalResp.message,\n envelope: mime.envelope,\n };\n}\n\n/**\n * QUIT and close an SMTP session adapter.\n */\nexport async function closeSMTPSession(adapter: SocketAdapter): Promise<void> {\n try {\n await sendCommand(adapter, { type: \"QUIT\" });\n await readSMTPResponse(adapter);\n } catch {\n // ignore errors during shutdown\n } finally {\n await adapter.close();\n }\n}\n\nasync function ehlo(adapter: SocketAdapter, host: string): Promise<string[]> {\n await sendCommand(adapter, { type: \"EHLO\", domain: host });\n const response = await readSMTPResponse(adapter);\n assertResponse(response, [250], \"EHLO\");\n return parseEHLO(response);\n}\n\nasync function authenticate(\n adapter: SocketAdapter,\n auth: NonNullable<SMTPConfig[\"auth\"]>,\n capabilities: string[],\n): Promise<void> {\n if (auth.type === \"OAUTH2\" && auth.oauth2) {\n const client = new OAuth2Client(auth.oauth2);\n const xoauth2 = await client.buildXOAUTH2();\n await sendCommand(adapter, { type: \"AUTH_XOAUTH2\", xoauth2String: xoauth2 });\n let resp = await readSMTPResponse(adapter);\n if (resp.code === 334) {\n await sendRaw(adapter, encodeLine(\"\"));\n resp = await readSMTPResponse(adapter);\n }\n assertResponse(resp, [235], \"AUTH XOAUTH2\");\n return;\n }\n\n const method = auth.type ?? selectAuthMethod(capabilities);\n\n if (method === \"CRAM-MD5\") {\n const pass = requirePassword(auth, \"CRAM-MD5\");\n await sendCommand(adapter, { type: \"AUTH_CRAM_MD5_INIT\" });\n let resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH CRAM-MD5\");\n const challenge = resp.message.trim();\n const response = await computeCRAMMD5(challenge, auth.user, pass);\n await sendCommand(adapter, { type: \"AUTH_CRAM_MD5_RESPONSE\", response });\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH CRAM-MD5 response\");\n return;\n }\n\n if (method === \"PLAIN\") {\n const pass = requirePassword(auth, \"PLAIN\");\n await sendRaw(adapter, encodeCommand({ type: \"AUTH_PLAIN\", user: auth.user, pass }));\n const resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH PLAIN\");\n return;\n }\n\n const pass = requirePassword(auth, \"LOGIN\");\n await sendRaw(adapter, encodeCommand({ type: \"AUTH_LOGIN\", user: auth.user, pass }));\n let resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH LOGIN\");\n\n await sendRaw(adapter, encodeAuthLoginUser(auth.user));\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH LOGIN user\");\n\n await sendRaw(adapter, encodeAuthLoginPass(pass));\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH LOGIN pass\");\n}\n\nfunction requirePassword(auth: NonNullable<SMTPConfig[\"auth\"]>, method: string): string {\n if (!auth.pass) {\n throw new SMTPError(`Password required for ${method} authentication`, 0, `AUTH ${method}`, \"\");\n }\n return auth.pass;\n}\n\nasync function sendCommand(\n adapter: SocketAdapter,\n command: Parameters<typeof encodeCommand>[0],\n): Promise<void> {\n await sendRaw(adapter, encodeCommand(command));\n}\n\nasync function sendRaw(adapter: SocketAdapter, data: Uint8Array): Promise<void> {\n await adapter.write(data);\n}\n\n/** Reads and parses a complete SMTP response from the adapter. */\nasync function readSMTPResponse(adapter: SocketAdapter): Promise<SMTPResponse> {\n const chunks: Uint8Array[] = [];\n for await (const chunk of adapter.read()) {\n chunks.push(chunk);\n const complete = accumulateResponse(chunks);\n if (complete) {\n return parseResponse(complete);\n }\n }\n throw new SMTPError(\"Connection closed while reading SMTP response\", 0, \"READ\", \"\");\n}\n\nasync function resolveMX(domain: string): Promise<string> {\n const dns = await import(\"node:dns/promises\");\n const records = await dns.resolveMx(domain);\n if (records.length === 0) {\n throw new SMTPError(`No MX records for ${domain}`, 0, \"MX\", \"\");\n }\n records.sort((a: { priority: number }, b: { priority: number }) => a.priority - b.priority);\n return records[0]?.exchange ?? domain;\n}\n\n/** @internal Test helper for raw line writes. */\nexport { encodeLine, readSMTPResponse };\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDO,MAAM,cAAmC;AAAA,EAC7B;AAAA,EACT,UAAgC;AAAA,EAGxC,WAAW,CAAC,QAAoB;AAAA,IAC9B,KAAK,SAAS,kBAAkB,MAAM;AAAA;AAAA,OAIlC,KAAI,CAAC,SAA2C;AAAA,IACpD,MAAM,kBAAkB;AAAA,SACnB;AAAA,MACH,aAAa,MAAM,mBAAmB,QAAQ,WAAW;AAAA,IAC3D;AAAA,IACA,MAAM,OAAO,MAAM,UAAU,iBAAiB,KAAK,OAAO,IAAI;AAAA,IAC9D,MAAM,UAAU,MAAM,KAAK,WAAW;AAAA,IAEtC,MAAM,OAAO,KAAK,OAAO,SACrB,MAAM,UAAU,KAAK,SAAS,KAAK,MAAM,GAAG,EAAE,MAAM,KAAK,OAAO,IAAI,IACpE,KAAK,OAAO;AAAA,IAEhB,MAAM,QAAQ,QAAQ,MAAM,KAAK,OAAO,IAAI;AAAA,IAC5C,KAAK,UAAU;AAAA,IAEf,IAAI;AAAA,MACF,MAAM,gBAAgB,SAAS,KAAK,MAAM;AAAA,MAC1C,OAAO,MAAM,mBAAmB,SAAS,IAAI;AAAA,cAC7C;AAAA,MACA,MAAM,iBAAiB,OAAO;AAAA,MAC9B,KAAK,UAAU;AAAA;AAAA;AAAA,OAKb,OAAM,GAA0B;AAAA,IACpC,IAAI;AAAA,MACF,MAAM,UAAU,MAAM,KAAK,WAAW;AAAA,MACtC,MAAM,QAAQ,QAAQ,KAAK,OAAO,MAAM,KAAK,OAAO,IAAI;AAAA,MAExD,IAAI;AAAA,QACF,MAAM,gBAAgB,SAAS,KAAK,MAAM;AAAA,QAC1C,OAAO,EAAE,IAAI,MAAM,UAAU,OAAO;AAAA,gBACpC;AAAA,QACA,MAAM,iBAAiB,OAAO;AAAA;AAAA,MAEhC,OAAO,KAAK;AAAA,MACZ,OAAO;AAAA,QACL,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MAC1D;AAAA;AAAA;AAAA,OAKE,MAAK,GAAkB;AAAA,IAC3B,IAAI,KAAK,SAAS;AAAA,MAChB,MAAM,KAAK,QAAQ,MAAM;AAAA,MACzB,KAAK,UAAU;AAAA,IACjB;AAAA;AAAA,OAGY,WAAU,GAA2B;AAAA,IACjD,IAAI,CAAC,KAAK,OAAO,SAAS;AAAA,MACxB,MAAM,IAAI,UAAU,gCAAgC,GAAG,WAAW,EAAE;AAAA,IACtE;AAAA,IACA,OAAO,KAAK,OAAO;AAAA;AAEvB;AAkBO,SAAS,iBAAiB,CAAC,QAAwC;AAAA,EACxE,MAAM,SAAS,OAAO,UAAU;AAAA,EAChC,OAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,MAAM,OAAO,SAAS,SAAS,MAAM;AAAA,IACrC;AAAA,OACI,OAAO,SAAS,YAAY,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;AAAA,OACrD,OAAO,SAAS,YAAY,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;AAAA,OACrD,OAAO,QAAQ,YAAY,EAAE,KAAK,OAAO,IAAI,IAAI,CAAC;AAAA,OAClD,OAAO,sBAAsB,YAC7B,EAAE,mBAAmB,OAAO,kBAAkB,IAC9C,CAAC;AAAA,OACD,OAAO,oBAAoB,YAAY,EAAE,iBAAiB,OAAO,gBAAgB,IAAI,CAAC;AAAA,OACtF,OAAO,kBAAkB,YAAY,EAAE,eAAe,OAAO,cAAc,IAAI,CAAC;AAAA,OAChF,OAAO,WAAW,YAAY,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,OAC3D,OAAO,YAAY,YAAY,EAAE,SAAS,OAAO,QAAQ,IAAI,CAAC;AAAA,EACpE;AAAA;AAMF,eAAsB,eAAe,CACnC,SACA,QACe;AAAA,EACf,MAAM,WAAW,MAAM,iBAAiB,OAAO;AAAA,EAC/C,eAAe,UAAU,CAAC,GAAG,GAAG,UAAU;AAAA,EAE1C,IAAI,eAAe,MAAM,KAAK,SAAS,OAAO,IAAI;AAAA,EAClD,MAAM,mBAAmB,aAAa,KAAK,CAAC,QAAQ,IAAI,YAAY,MAAM,UAAU;AAAA,EACpF,IAAI,CAAC,OAAO,UAAU,CAAC,QAAQ,UAAU,kBAAkB;AAAA,IACzD,MAAM,QAAQ,SAAS,cAAc,EAAE,MAAM,WAAW,CAAC,CAAC;AAAA,IAC1D,MAAM,eAAe,MAAM,iBAAiB,OAAO;AAAA,IACnD,eAAe,cAAc,CAAC,GAAG,GAAG,UAAU;AAAA,IAC9C,MAAM,QAAQ,SAAS,OAAO,GAAG;AAAA,IACjC,eAAe,MAAM,KAAK,SAAS,OAAO,IAAI;AAAA,EAChD;AAAA,EAEA,IAAI,OAAO,MAAM;AAAA,IACf,MAAM,aAAa,SAAS,OAAO,MAAM,YAAY;AAAA,EACvD;AAAA;AAMF,eAAsB,kBAAkB,CACtC,SACA,MACqB;AAAA,EACrB,MAAM,YAAY,SAAS,EAAE,MAAM,aAAa,SAAS,KAAK,SAAS,KAAK,CAAC;AAAA,EAC7E,MAAM,WAAW,MAAM,iBAAiB,OAAO;AAAA,EAC/C,eAAe,UAAU,CAAC,GAAG,GAAG,WAAW;AAAA,EAE3C,MAAM,WAAqB,CAAC;AAAA,EAC5B,MAAM,WAAqB,CAAC;AAAA,EAE5B,WAAW,aAAa,KAAK,SAAS,IAAI;AAAA,IACxC,MAAM,QAAQ,SAAS,cAAc,EAAE,MAAM,WAAW,SAAS,UAAU,CAAC,CAAC;AAAA,IAC7E,MAAM,WAAW,MAAM,iBAAiB,OAAO;AAAA,IAC/C,IAAI,SAAS,WAAW;AAAA,MACtB,SAAS,KAAK,SAAS;AAAA,IACzB,EAAO;AAAA,MACL,SAAS,KAAK,SAAS;AAAA;AAAA,EAE3B;AAAA,EAEA,MAAM,YAAY,SAAS,EAAE,MAAM,OAAO,CAAC;AAAA,EAC3C,MAAM,WAAW,MAAM,iBAAiB,OAAO;AAAA,EAC/C,eAAe,UAAU,CAAC,GAAG,GAAG,MAAM;AAAA,EAEtC,IAAI;AAAA,EACJ,IAAI;AAAA,IACF,MAAM,QAAQ,SAAS,cAAc,EAAE,MAAM,aAAa,SAAS,KAAK,IAAI,CAAC,CAAC;AAAA,IAC9E,YAAY,MAAM,iBAAiB,OAAO;AAAA,IAC1C,OAAO,KAAK;AAAA,IACZ,MAAM,QAAQ,SAAS,cAAc,EAAE,MAAM,aAAa,SAAS,KAAK,IAAI,CAAC,CAAC;AAAA,IAC9E,YAAY,MAAM,iBAAiB,OAAO;AAAA,IAC1C,IAAI,UAAU,SAAS;AAAA,MACrB,MAAM;AAAA,IACR;AAAA;AAAA,EAEF,eAAe,WAAW,CAAC,GAAG,GAAG,UAAU;AAAA,EAE3C,OAAO;AAAA,IACL,WAAW,KAAK;AAAA,IAChB;AAAA,IACA;AAAA,IACA,UAAU,UAAU;AAAA,IACpB,UAAU,KAAK;AAAA,EACjB;AAAA;AAMF,eAAsB,gBAAgB,CAAC,SAAuC;AAAA,EAC5E,IAAI;AAAA,IACF,MAAM,YAAY,SAAS,EAAE,MAAM,OAAO,CAAC;AAAA,IAC3C,MAAM,iBAAiB,OAAO;AAAA,IAC9B,MAAM,WAEN;AAAA,IACA,MAAM,QAAQ,MAAM;AAAA;AAAA;AAIxB,eAAe,IAAI,CAAC,SAAwB,MAAiC;AAAA,EAC3E,MAAM,YAAY,SAAS,EAAE,MAAM,QAAQ,QAAQ,KAAK,CAAC;AAAA,EACzD,MAAM,WAAW,MAAM,iBAAiB,OAAO;AAAA,EAC/C,eAAe,UAAU,CAAC,GAAG,GAAG,MAAM;AAAA,EACtC,OAAO,UAAU,QAAQ;AAAA;AAG3B,eAAe,YAAY,CACzB,SACA,MACA,cACe;AAAA,EACf,IAAI,KAAK,SAAS,YAAY,KAAK,QAAQ;AAAA,IACzC,MAAM,SAAS,IAAI,aAAa,KAAK,MAAM;AAAA,IAC3C,MAAM,UAAU,MAAM,OAAO,aAAa;AAAA,IAC1C,MAAM,YAAY,SAAS,EAAE,MAAM,gBAAgB,eAAe,QAAQ,CAAC;AAAA,IAC3E,IAAI,QAAO,MAAM,iBAAiB,OAAO;AAAA,IACzC,IAAI,MAAK,SAAS,KAAK;AAAA,MACrB,MAAM,QAAQ,SAAS,WAAW,EAAE,CAAC;AAAA,MACrC,QAAO,MAAM,iBAAiB,OAAO;AAAA,IACvC;AAAA,IACA,eAAe,OAAM,CAAC,GAAG,GAAG,cAAc;AAAA,IAC1C;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAAK,QAAQ,iBAAiB,YAAY;AAAA,EAEzD,IAAI,WAAW,YAAY;AAAA,IACzB,MAAM,QAAO,gBAAgB,MAAM,UAAU;AAAA,IAC7C,MAAM,YAAY,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAAA,IACzD,IAAI,QAAO,MAAM,iBAAiB,OAAO;AAAA,IACzC,eAAe,OAAM,CAAC,GAAG,GAAG,eAAe;AAAA,IAC3C,MAAM,YAAY,MAAK,QAAQ,KAAK;AAAA,IACpC,MAAM,WAAW,MAAM,eAAe,WAAW,KAAK,MAAM,KAAI;AAAA,IAChE,MAAM,YAAY,SAAS,EAAE,MAAM,0BAA0B,SAAS,CAAC;AAAA,IACvE,QAAO,MAAM,iBAAiB,OAAO;AAAA,IACrC,eAAe,OAAM,CAAC,GAAG,GAAG,wBAAwB;AAAA,IACpD;AAAA,EACF;AAAA,EAEA,IAAI,WAAW,SAAS;AAAA,IACtB,MAAM,QAAO,gBAAgB,MAAM,OAAO;AAAA,IAC1C,MAAM,QAAQ,SAAS,cAAc,EAAE,MAAM,cAAc,MAAM,KAAK,MAAM,YAAK,CAAC,CAAC;AAAA,IACnF,MAAM,QAAO,MAAM,iBAAiB,OAAO;AAAA,IAC3C,eAAe,OAAM,CAAC,GAAG,GAAG,YAAY;AAAA,IACxC;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,gBAAgB,MAAM,OAAO;AAAA,EAC1C,MAAM,QAAQ,SAAS,cAAc,EAAE,MAAM,cAAc,MAAM,KAAK,MAAM,KAAK,CAAC,CAAC;AAAA,EACnF,IAAI,OAAO,MAAM,iBAAiB,OAAO;AAAA,EACzC,eAAe,MAAM,CAAC,GAAG,GAAG,YAAY;AAAA,EAExC,MAAM,QAAQ,SAAS,oBAAoB,KAAK,IAAI,CAAC;AAAA,EACrD,OAAO,MAAM,iBAAiB,OAAO;AAAA,EACrC,eAAe,MAAM,CAAC,GAAG,GAAG,iBAAiB;AAAA,EAE7C,MAAM,QAAQ,SAAS,oBAAoB,IAAI,CAAC;AAAA,EAChD,OAAO,MAAM,iBAAiB,OAAO;AAAA,EACrC,eAAe,MAAM,CAAC,GAAG,GAAG,iBAAiB;AAAA;AAG/C,SAAS,eAAe,CAAC,MAAuC,QAAwB;AAAA,EACtF,IAAI,CAAC,KAAK,MAAM;AAAA,IACd,MAAM,IAAI,UAAU,yBAAyB,yBAAyB,GAAG,QAAQ,UAAU,EAAE;AAAA,EAC/F;AAAA,EACA,OAAO,KAAK;AAAA;AAGd,eAAe,WAAW,CACxB,SACA,SACe;AAAA,EACf,MAAM,QAAQ,SAAS,cAAc,OAAO,CAAC;AAAA;AAG/C,eAAe,OAAO,CAAC,SAAwB,MAAiC;AAAA,EAC9E,MAAM,QAAQ,MAAM,IAAI;AAAA;AAI1B,eAAe,gBAAgB,CAAC,SAA+C;AAAA,EAC7E,MAAM,SAAuB,CAAC;AAAA,EAC9B,iBAAiB,SAAS,QAAQ,KAAK,GAAG;AAAA,IACxC,OAAO,KAAK,KAAK;AAAA,IACjB,MAAM,WAAW,mBAAmB,MAAM;AAAA,IAC1C,IAAI,UAAU;AAAA,MACZ,OAAO,cAAc,QAAQ;AAAA,IAC/B;AAAA,EACF;AAAA,EACA,MAAM,IAAI,UAAU,iDAAiD,GAAG,QAAQ,EAAE;AAAA;AAGpF,eAAe,SAAS,CAAC,QAAiC;AAAA,EACxD,MAAM,MAAM,MAAa;AAAA,EACzB,MAAM,UAAU,MAAM,IAAI,UAAU,MAAM;AAAA,EAC1C,IAAI,QAAQ,WAAW,GAAG;AAAA,IACxB,MAAM,IAAI,UAAU,qBAAqB,UAAU,GAAG,MAAM,EAAE;AAAA,EAChE;AAAA,EACA,QAAQ,KAAK,CAAC,GAAyB,MAA4B,EAAE,WAAW,EAAE,QAAQ;AAAA,EAC1F,OAAO,QAAQ,IAAI,YAAY;AAAA;",
8
+ "debugId": "44FB4148CD9604AA64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,270 @@
1
+ import {
2
+ closeSMTPSession,
3
+ deliverSMTPMessage,
4
+ openSMTPSession,
5
+ resolveSMTPConfig
6
+ } from "./chunk-8sm0vz0n.js";
7
+ import {
8
+ buildMIME
9
+ } from "./chunk-j6qw8ms6.js";
10
+ import {
11
+ resolveAttachments
12
+ } from "./chunk-bvxkmq94.js";
13
+
14
+ // src/pool/connection.ts
15
+ async function createPooledConnection(options) {
16
+ const config = resolveSMTPConfig(options.config);
17
+ const adapter = await options.createAdapter();
18
+ await adapter.connect(options.connectHost, config.port);
19
+ await openSMTPSession(adapter, config);
20
+ let messageCount = 0;
21
+ let idle = true;
22
+ let sendChain = Promise.resolve();
23
+ const maxMessages = options.maxMessages;
24
+ return {
25
+ get idle() {
26
+ return idle;
27
+ },
28
+ get messageCount() {
29
+ return messageCount;
30
+ },
31
+ get usable() {
32
+ return messageCount < maxMessages;
33
+ },
34
+ async send(mailOptions) {
35
+ const run = async () => {
36
+ idle = false;
37
+ try {
38
+ const resolvedOptions = {
39
+ ...mailOptions,
40
+ attachments: await resolveAttachments(mailOptions.attachments)
41
+ };
42
+ const mime = await buildMIME(resolvedOptions, config.dkim);
43
+ const result = await deliverSMTPMessage(adapter, mime);
44
+ messageCount += 1;
45
+ return result;
46
+ } finally {
47
+ idle = true;
48
+ }
49
+ };
50
+ const resultPromise = sendChain.then(run);
51
+ sendChain = resultPromise.then(() => {
52
+ return;
53
+ }, () => {
54
+ return;
55
+ });
56
+ return resultPromise;
57
+ },
58
+ async close() {
59
+ await sendChain;
60
+ await closeSMTPSession(adapter);
61
+ }
62
+ };
63
+ }
64
+
65
+ // src/pool/pool.ts
66
+ class RateLimiter {
67
+ rateDelta;
68
+ rateLimit;
69
+ now;
70
+ tokens;
71
+ lastRefill;
72
+ waiters = [];
73
+ constructor(rateDelta, rateLimit, now = Date.now) {
74
+ this.rateDelta = rateDelta;
75
+ this.rateLimit = rateLimit;
76
+ this.now = now;
77
+ this.tokens = rateDelta;
78
+ this.lastRefill = now();
79
+ }
80
+ async acquire() {
81
+ for (;; ) {
82
+ this.refill();
83
+ if (this.tokens > 0) {
84
+ this.tokens -= 1;
85
+ return;
86
+ }
87
+ await new Promise((resolve) => {
88
+ this.waiters.push(resolve);
89
+ });
90
+ }
91
+ }
92
+ notify() {
93
+ this.refill();
94
+ }
95
+ refill() {
96
+ const t = this.now();
97
+ const elapsed = t - this.lastRefill;
98
+ if (elapsed >= this.rateLimit) {
99
+ const periods = Math.floor(elapsed / this.rateLimit);
100
+ this.tokens = Math.min(this.rateDelta, this.tokens + periods * this.rateDelta);
101
+ this.lastRefill += periods * this.rateLimit;
102
+ while (this.tokens > 0 && this.waiters.length > 0) {
103
+ this.tokens -= 1;
104
+ const next = this.waiters.shift();
105
+ next?.();
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ class SMTPPool {
112
+ config;
113
+ maxConnections;
114
+ maxMessages;
115
+ createAdapterFn;
116
+ rateLimiter;
117
+ connections = [];
118
+ queue = [];
119
+ draining = false;
120
+ closed = false;
121
+ processChain = Promise.resolve();
122
+ constructor(config, options) {
123
+ this.config = config;
124
+ this.maxConnections = config.maxConnections ?? 5;
125
+ this.maxMessages = config.maxMessages ?? 100;
126
+ if (options?.createAdapter) {
127
+ const factory = options.createAdapter;
128
+ this.createAdapterFn = async () => factory();
129
+ } else if (config.adapter) {
130
+ this.createAdapterFn = async () => config.adapter;
131
+ } else {
132
+ throw new Error("SMTPPool requires config.adapter or options.createAdapter");
133
+ }
134
+ if (config.rateDelta !== undefined && config.rateDelta > 0) {
135
+ this.rateLimiter = new RateLimiter(config.rateDelta, config.rateLimit ?? 1000, options?.now);
136
+ } else {
137
+ this.rateLimiter = null;
138
+ }
139
+ }
140
+ async send(options) {
141
+ if (this.draining) {
142
+ throw new Error("SMTPPool is closing — no new messages accepted");
143
+ }
144
+ if (this.closed) {
145
+ throw new Error("SMTPPool is closed");
146
+ }
147
+ if (this.rateLimiter) {
148
+ await this.rateLimiter.acquire();
149
+ }
150
+ return new Promise((resolve, reject) => {
151
+ this.queue.push({ options, resolve, reject });
152
+ this.scheduleProcess();
153
+ });
154
+ }
155
+ scheduleProcess() {
156
+ this.processChain = this.processChain.then(() => this.processQueue()).catch(() => {
157
+ return;
158
+ });
159
+ }
160
+ async verify() {
161
+ try {
162
+ const conn = await this.spawnConnection();
163
+ try {
164
+ return { ok: true, provider: "smtp-pool" };
165
+ } finally {
166
+ await conn.close();
167
+ this.removeConnection(conn);
168
+ }
169
+ } catch (err) {
170
+ return {
171
+ ok: false,
172
+ provider: "smtp-pool",
173
+ message: err instanceof Error ? err.message : String(err)
174
+ };
175
+ }
176
+ }
177
+ async close() {
178
+ this.draining = true;
179
+ await this.drainQueue();
180
+ await Promise.allSettled(this.connections.map((c) => c.close()));
181
+ this.connections.length = 0;
182
+ this.closed = true;
183
+ }
184
+ get connectionCount() {
185
+ return this.connections.length;
186
+ }
187
+ get queueSize() {
188
+ return this.queue.length;
189
+ }
190
+ async processQueue() {
191
+ if (this.draining) {
192
+ return;
193
+ }
194
+ while (this.queue.length > 0) {
195
+ const idleConn = this.connections.find((c) => c.idle && c.usable);
196
+ if (idleConn) {
197
+ const entry = this.queue.shift();
198
+ if (!entry) {
199
+ break;
200
+ }
201
+ try {
202
+ const result = await idleConn.send(entry.options);
203
+ entry.resolve(result);
204
+ if (!idleConn.usable) {
205
+ await idleConn.close();
206
+ this.removeConnection(idleConn);
207
+ }
208
+ } catch (err) {
209
+ entry.reject(err);
210
+ await idleConn.close().catch(() => {
211
+ return;
212
+ });
213
+ this.removeConnection(idleConn);
214
+ }
215
+ continue;
216
+ }
217
+ if (this.connections.length < this.maxConnections) {
218
+ const entry = this.queue.shift();
219
+ if (!entry) {
220
+ break;
221
+ }
222
+ const conn = await this.spawnConnection();
223
+ try {
224
+ const result = await conn.send(entry.options);
225
+ entry.resolve(result);
226
+ if (!conn.usable) {
227
+ await conn.close();
228
+ this.removeConnection(conn);
229
+ }
230
+ } catch (err) {
231
+ entry.reject(err);
232
+ await conn.close().catch(() => {
233
+ return;
234
+ });
235
+ this.removeConnection(conn);
236
+ }
237
+ continue;
238
+ }
239
+ break;
240
+ }
241
+ }
242
+ async spawnConnection() {
243
+ const resolved = resolveSMTPConfig(this.config);
244
+ const conn = await createPooledConnection({
245
+ config: this.config,
246
+ maxMessages: this.maxMessages,
247
+ connectHost: resolved.host,
248
+ createAdapter: this.createAdapterFn
249
+ });
250
+ this.connections.push(conn);
251
+ return conn;
252
+ }
253
+ removeConnection(conn) {
254
+ const index = this.connections.indexOf(conn);
255
+ if (index >= 0) {
256
+ this.connections.splice(index, 1);
257
+ }
258
+ }
259
+ async drainQueue() {
260
+ while (this.queue.length > 0 || this.connections.some((c) => !c.idle)) {
261
+ await this.processQueue();
262
+ if (this.queue.length > 0) {
263
+ await new Promise((r) => setTimeout(r, 10));
264
+ }
265
+ }
266
+ }
267
+ }
268
+ export { RateLimiter, SMTPPool };
269
+
270
+ //# debugId=D47B35696A356F3464756E2164756E21