sently 0.4.6 → 0.4.7

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 (75) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +94 -41
  3. package/dist/adapters/bun.js +2 -183
  4. package/dist/adapters/bun.js.map +2 -2
  5. package/dist/adapters/cf.js +2 -77
  6. package/dist/adapters/cf.js.map +2 -2
  7. package/dist/adapters/deno.js +2 -72
  8. package/dist/adapters/deno.js.map +2 -2
  9. package/dist/adapters/node.js +2 -180
  10. package/dist/adapters/node.js.map +2 -2
  11. package/dist/auth/oauth2.js +2 -13
  12. package/dist/auth/oauth2.js.map +1 -1
  13. package/dist/chunk-2kcwa9gt.js +4 -0
  14. package/dist/chunk-2kcwa9gt.js.map +11 -0
  15. package/dist/chunk-2t6hjer3.js +5 -0
  16. package/dist/chunk-2t6hjer3.js.map +10 -0
  17. package/dist/chunk-6yggz45h.js +5 -0
  18. package/dist/{chunk-794hc3m4.js.map → chunk-6yggz45h.js.map} +2 -2
  19. package/dist/chunk-dgkh77yp.js +4 -0
  20. package/dist/{chunk-ym3zzv8b.js.map → chunk-dgkh77yp.js.map} +2 -2
  21. package/dist/chunk-jfs80vhp.js +3 -0
  22. package/dist/{chunk-7fqv71z1.js.map → chunk-jfs80vhp.js.map} +2 -2
  23. package/dist/chunk-sqn04kae.js +4 -0
  24. package/dist/{chunk-v0bahtg2.js.map → chunk-sqn04kae.js.map} +1 -1
  25. package/dist/chunk-va2awz12.js +4 -0
  26. package/dist/{chunk-f4c9ttmr.js.map → chunk-va2awz12.js.map} +2 -2
  27. package/dist/chunk-wgtbr6ge.js +13 -0
  28. package/dist/{chunk-tymfm441.js.map → chunk-wgtbr6ge.js.map} +2 -2
  29. package/dist/core/smtp.js +2 -31
  30. package/dist/core/smtp.js.map +1 -1
  31. package/dist/core/types.d.ts +7 -0
  32. package/dist/detect.d.ts +2 -0
  33. package/dist/detect.js +2 -180
  34. package/dist/detect.js.map +4 -5
  35. package/dist/dkim.d.ts +21 -0
  36. package/dist/dkim.js +9 -0
  37. package/dist/dkim.js.map +10 -0
  38. package/dist/index.d.ts +74 -16
  39. package/dist/index.js +85 -14
  40. package/dist/mailer.d.ts +16 -0
  41. package/dist/mailer.js +3 -0
  42. package/dist/mailer.js.map +9 -0
  43. package/dist/plugins/template.js +2 -28
  44. package/dist/plugins/template.js.map +2 -2
  45. package/dist/pool/pool.js +2 -16
  46. package/dist/pool/pool.js.map +5 -3
  47. package/dist/transports/brevo.js +2 -115
  48. package/dist/transports/brevo.js.map +2 -2
  49. package/dist/transports/mailgun.js +2 -119
  50. package/dist/transports/mailgun.js.map +2 -2
  51. package/dist/transports/postmark.js +2 -113
  52. package/dist/transports/postmark.js.map +2 -2
  53. package/dist/transports/preview.js +2 -72
  54. package/dist/transports/preview.js.map +2 -2
  55. package/dist/transports/resend.js +2 -109
  56. package/dist/transports/resend.js.map +2 -2
  57. package/dist/transports/retry.js +2 -78
  58. package/dist/transports/retry.js.map +2 -2
  59. package/dist/transports/sendgrid.js +2 -132
  60. package/dist/transports/sendgrid.js.map +2 -2
  61. package/dist/transports/ses.js +5 -251
  62. package/dist/transports/ses.js.map +2 -2
  63. package/dist/transports/smtp.js +2 -26
  64. package/dist/transports/smtp.js.map +1 -1
  65. package/package.json +17 -6
  66. package/dist/chunk-794hc3m4.js +0 -105
  67. package/dist/chunk-7fqv71z1.js +0 -251
  68. package/dist/chunk-f4c9ttmr.js +0 -154
  69. package/dist/chunk-mp5c9bfd.js +0 -270
  70. package/dist/chunk-mp5c9bfd.js.map +0 -11
  71. package/dist/chunk-tymfm441.js +0 -405
  72. package/dist/chunk-v0bahtg2.js +0 -6
  73. package/dist/chunk-x3szga4k.js +0 -367
  74. package/dist/chunk-x3szga4k.js.map +0 -11
  75. package/dist/chunk-ym3zzv8b.js +0 -74
@@ -1,154 +0,0 @@
1
- import {
2
- encodeHeader
3
- } from "./chunk-794hc3m4.js";
4
- import {
5
- __require
6
- } from "./chunk-v0bahtg2.js";
7
-
8
- // src/core/address.ts
9
- function findForbiddenChar(value) {
10
- for (let i = 0;i < value.length; i++) {
11
- const code = value.charCodeAt(i);
12
- if (code <= 31 || code === 127 || code === 8232 || code === 8233) {
13
- return code;
14
- }
15
- }
16
- return -1;
17
- }
18
- function assertSafeAddress(value, label = "address") {
19
- const code = findForbiddenChar(value);
20
- if (code !== -1) {
21
- const hex = code.toString(16).padStart(2, "0");
22
- throw new Error(`Email ${label} contains a forbidden control character (0x${hex}). ` + "CR, LF, NUL, and other control characters are not allowed.");
23
- }
24
- }
25
- function parseAddresses(input) {
26
- if (Array.isArray(input)) {
27
- return input.flatMap((item) => parseAddresses(item));
28
- }
29
- if (typeof input === "object") {
30
- assertSafeAddress(input.address, "address");
31
- if (input.name !== undefined) {
32
- assertSafeAddress(input.name, "display name");
33
- }
34
- return [{ ...input }];
35
- }
36
- assertSafeAddress(input, "address");
37
- const trimmed = input.trim();
38
- if (!trimmed) {
39
- return [];
40
- }
41
- return splitAddressList(trimmed).map(parseSingleAddress);
42
- }
43
- function toMIMEHeader(address) {
44
- assertSafeAddress(address.address, "address");
45
- if (address.name) {
46
- assertSafeAddress(address.name, "display name");
47
- const name = encodeHeader(address.name);
48
- return `${name} <${address.address}>`;
49
- }
50
- return address.address;
51
- }
52
- function extractEmails(input) {
53
- return parseAddresses(input).map((addr) => addr.address);
54
- }
55
- function isValidEmail(email) {
56
- if (findForbiddenChar(email) !== -1)
57
- return false;
58
- return /^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(email);
59
- }
60
- function splitAddressList(input) {
61
- const parts = [];
62
- let current = "";
63
- let inQuotes = false;
64
- let inAngle = false;
65
- for (let i = 0;i < input.length; i++) {
66
- const char = input[i] ?? "";
67
- if (char === '"' && input[i - 1] !== "\\") {
68
- inQuotes = !inQuotes;
69
- current += char;
70
- continue;
71
- }
72
- if (char === "<" && !inQuotes) {
73
- inAngle = true;
74
- current += char;
75
- continue;
76
- }
77
- if (char === ">" && !inQuotes) {
78
- inAngle = false;
79
- current += char;
80
- continue;
81
- }
82
- if (char === "," && !inQuotes && !inAngle) {
83
- if (current.trim()) {
84
- parts.push(current.trim());
85
- }
86
- current = "";
87
- continue;
88
- }
89
- current += char;
90
- }
91
- if (current.trim()) {
92
- parts.push(current.trim());
93
- }
94
- return parts;
95
- }
96
- function parseSingleAddress(input) {
97
- const trimmed = input.trim();
98
- const angleMatch = trimmed.match(/^(?:"([^"]*)"|([^<]*?))\s*<([^>]+)>$/);
99
- if (angleMatch) {
100
- const name = (angleMatch[1] ?? angleMatch[2] ?? "").trim();
101
- const address = (angleMatch[3] ?? "").trim();
102
- if (name) {
103
- return { name, address };
104
- }
105
- return { address };
106
- }
107
- if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
108
- return { address: trimmed.slice(1, -1) };
109
- }
110
- return { address: trimmed };
111
- }
112
-
113
- // src/transports/resolve-attachments.ts
114
- async function resolveAttachments(attachments, options) {
115
- const list = attachments ?? [];
116
- const resolved = [];
117
- for (const attachment of list) {
118
- if (attachment.content instanceof Uint8Array) {
119
- resolved.push(attachment);
120
- continue;
121
- }
122
- if (attachment.path) {
123
- let fs;
124
- try {
125
- fs = await import("node:fs/promises");
126
- } catch {
127
- throw new Error("attachment.path is not supported on this runtime — use attachment.content (Uint8Array) instead");
128
- }
129
- if (options?.basePath) {
130
- const { resolve, sep } = await import("node:path");
131
- const resolvedPath = resolve(attachment.path);
132
- const resolvedBase = resolve(options.basePath);
133
- const isWithin = resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + sep);
134
- if (!isWithin) {
135
- throw new Error(`[sently] Attachment path "${resolvedPath}" escapes basePath "${resolvedBase}". Use absolute paths within the allowed directory.`);
136
- }
137
- }
138
- const data = await fs.readFile(attachment.path);
139
- const { path: _path, ...rest } = attachment;
140
- resolved.push({ ...rest, content: new Uint8Array(data) });
141
- continue;
142
- }
143
- if (typeof attachment.content === "string") {
144
- resolved.push(attachment);
145
- continue;
146
- }
147
- resolved.push(attachment);
148
- }
149
- return resolved;
150
- }
151
-
152
- export { assertSafeAddress, parseAddresses, toMIMEHeader, extractEmails, isValidEmail, resolveAttachments };
153
-
154
- //# debugId=419CD2BE8A029CA164756E2164756E21
@@ -1,270 +0,0 @@
1
- import {
2
- closeSMTPSession,
3
- deliverSMTPMessage,
4
- openSMTPSession,
5
- resolveSMTPConfig
6
- } from "./chunk-7fqv71z1.js";
7
- import {
8
- buildMIME
9
- } from "./chunk-x3szga4k.js";
10
- import {
11
- resolveAttachments
12
- } from "./chunk-f4c9ttmr.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
@@ -1,11 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/pool/connection.ts", "../src/pool/pool.ts"],
4
- "sourcesContent": [
5
- "import { buildMIME } from \"../core/mime.js\";\nimport type { MailOptions, SendResult, SMTPConfig } from \"../core/types.js\";\nimport { resolveAttachments } from \"../transports/resolve-attachments.js\";\nimport {\n closeSMTPSession,\n deliverSMTPMessage,\n openSMTPSession,\n resolveSMTPConfig,\n} from \"../transports/smtp.js\";\n\n/** A single pooled SMTP connection with a persistent session. */\nexport interface PooledConnection {\n /** Send one message over this connection. */\n send(options: MailOptions): Promise<SendResult>;\n /** Whether this connection is idle and available for work. */\n readonly idle: boolean;\n /** Number of messages sent on this connection. */\n readonly messageCount: number;\n /** Whether this connection can accept more messages before recycle. */\n readonly usable: boolean;\n /** Close the connection and end the SMTP session. */\n close(): Promise<void>;\n}\n\n/** Options for creating a pooled connection. */\nexport interface PooledConnectionOptions {\n /** SMTP configuration for the pooled session. */\n config: SMTPConfig;\n /** Maximum messages before this connection is recycled. */\n maxMessages: number;\n /** Hostname to connect to (may differ from config.host for direct MX). */\n connectHost: string;\n /** Factory that creates the socket adapter for this connection. */\n createAdapter: () => Promise<import(\"../core/types.js\").SocketAdapter>;\n}\n\n/**\n * Create a pooled SMTP connection with an open authenticated session.\n */\nexport async function createPooledConnection(\n options: PooledConnectionOptions,\n): Promise<PooledConnection> {\n const config = resolveSMTPConfig(options.config);\n const adapter = await options.createAdapter();\n await adapter.connect(options.connectHost, config.port);\n await openSMTPSession(adapter, config);\n\n let messageCount = 0;\n let idle = true;\n let sendChain: Promise<void> = Promise.resolve();\n\n const maxMessages = options.maxMessages;\n\n return {\n get idle(): boolean {\n return idle;\n },\n get messageCount(): number {\n return messageCount;\n },\n get usable(): boolean {\n return messageCount < maxMessages;\n },\n\n async send(mailOptions: MailOptions): Promise<SendResult> {\n const run = async (): Promise<SendResult> => {\n idle = false;\n try {\n const resolvedOptions = {\n ...mailOptions,\n attachments: await resolveAttachments(mailOptions.attachments),\n };\n const mime = await buildMIME(resolvedOptions, config.dkim);\n const result = await deliverSMTPMessage(adapter, mime);\n messageCount += 1;\n return result;\n } finally {\n idle = true;\n }\n };\n\n const resultPromise = sendChain.then(run);\n sendChain = resultPromise.then(\n () => undefined,\n () => undefined,\n );\n return resultPromise;\n },\n\n async close(): Promise<void> {\n await sendChain;\n await closeSMTPSession(adapter);\n },\n };\n}\n",
6
- "/**\n * @module\n * SMTP connection pool with optional rate limiting.\n *\n * @example\n * ```ts\n * import { SMTPPool } from \"sently/pool\";\n * import { NodeAdapter } from \"sently/adapters/node\";\n *\n * const pool = new SMTPPool({\n * host: \"smtp.example.com\",\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * adapter: new NodeAdapter(),\n * pool: true,\n * maxConnections: 5,\n * });\n *\n * await pool.send({\n * from: \"you@example.com\",\n * to: \"recipient@example.com\",\n * subject: \"Hello\",\n * text: \"Pooled send\",\n * });\n * ```\n */\nimport type {\n MailOptions,\n PoolConfig,\n SendResult,\n SMTPConfig,\n SocketAdapter,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveSMTPConfig } from \"../transports/smtp.js\";\nimport { createPooledConnection, type PooledConnection } from \"./connection.js\";\n\n/** Options for {@link SMTPPool}. */\nexport interface SMTPPoolOptions {\n /** Factory for a new socket adapter per pooled connection. */\n createAdapter?: () => Promise<SocketAdapter> | SocketAdapter;\n /** Injectable clock for rate limiting (testing). */\n now?: () => number;\n}\n\ninterface QueueEntry {\n options: MailOptions;\n resolve: (result: SendResult) => void;\n reject: (error: unknown) => void;\n}\n\n/**\n * Token bucket rate limiter with lazy refill on acquire.\n */\nclass RateLimiter {\n /** Remaining tokens in the current rate-limit window. */\n private tokens: number;\n /** Timestamp (ms) of the last token refill. */\n private lastRefill: number;\n /** Resolvers waiting for a token when the bucket is empty. */\n private waiters: Array<() => void> = [];\n\n /** Creates a rate limiter with the given burst size and window duration. */\n constructor(\n private readonly rateDelta: number,\n private readonly rateLimit: number,\n private readonly now: () => number = Date.now,\n ) {\n this.tokens = rateDelta;\n this.lastRefill = now();\n }\n\n /** Wait until a token is available, then consume one. */\n async acquire(): Promise<void> {\n for (;;) {\n this.refill();\n if (this.tokens > 0) {\n this.tokens -= 1;\n return;\n }\n await new Promise<void>((resolve) => {\n this.waiters.push(resolve);\n });\n }\n }\n\n /** Wake waiters after the clock advances (for testing). */\n notify(): void {\n this.refill();\n }\n\n /** Refills tokens based on elapsed time and wakes waiting acquirers. */\n private refill(): void {\n const t = this.now();\n const elapsed = t - this.lastRefill;\n if (elapsed >= this.rateLimit) {\n const periods = Math.floor(elapsed / this.rateLimit);\n this.tokens = Math.min(this.rateDelta, this.tokens + periods * this.rateDelta);\n this.lastRefill += periods * this.rateLimit;\n while (this.tokens > 0 && this.waiters.length > 0) {\n this.tokens -= 1;\n const next = this.waiters.shift();\n next?.();\n }\n }\n }\n}\n\n/**\n * SMTP connection pool with optional rate limiting.\n */\nexport class SMTPPool implements Transport {\n /** Resolved SMTP and pool configuration. */\n private readonly config: SMTPConfig & PoolConfig;\n /** Maximum simultaneous pooled connections. */\n private readonly maxConnections: number;\n /** Maximum messages per connection before recycle. */\n private readonly maxMessages: number;\n /** Factory that creates a socket adapter for each new connection. */\n private readonly createAdapterFn: () => Promise<SocketAdapter>;\n /** Optional token-bucket rate limiter, or null when disabled. */\n private readonly rateLimiter: RateLimiter | null;\n /** Active pooled SMTP connections. */\n private readonly connections: PooledConnection[] = [];\n /** Pending send operations waiting for a connection. */\n private readonly queue: QueueEntry[] = [];\n /** True while {@link close} is draining; rejects new sends. */\n private draining = false;\n /** True after the pool has fully closed. */\n private closed = false;\n /** Serializes queue processing to avoid concurrent drain races. */\n private processChain: Promise<void> = Promise.resolve();\n\n /** Creates an SMTP connection pool. */\n constructor(config: SMTPConfig & PoolConfig, options?: SMTPPoolOptions) {\n this.config = config;\n this.maxConnections = config.maxConnections ?? 5;\n this.maxMessages = config.maxMessages ?? 100;\n\n if (options?.createAdapter) {\n const factory = options.createAdapter;\n this.createAdapterFn = async () => factory();\n } else if (config.adapter) {\n this.createAdapterFn = async () => config.adapter as SocketAdapter;\n } else {\n throw new Error(\"SMTPPool requires config.adapter or options.createAdapter\");\n }\n\n if (config.rateDelta !== undefined && config.rateDelta > 0) {\n this.rateLimiter = new RateLimiter(config.rateDelta, config.rateLimit ?? 1000, options?.now);\n } else {\n this.rateLimiter = null;\n }\n }\n\n /** Sends a message through the pool. */\n async send(options: MailOptions): Promise<SendResult> {\n if (this.draining) {\n throw new Error(\"SMTPPool is closing — no new messages accepted\");\n }\n if (this.closed) {\n throw new Error(\"SMTPPool is closed\");\n }\n if (this.rateLimiter) {\n await this.rateLimiter.acquire();\n }\n return new Promise<SendResult>((resolve, reject) => {\n this.queue.push({ options, resolve, reject });\n this.scheduleProcess();\n });\n }\n\n /** Schedules asynchronous processing of the send queue. */\n private scheduleProcess(): void {\n this.processChain = this.processChain.then(() => this.processQueue()).catch(() => undefined);\n }\n\n /** Verifies connectivity using a temporary connection. */\n async verify(): Promise<VerifyResult> {\n try {\n const conn = await this.spawnConnection();\n try {\n return { ok: true, provider: \"smtp-pool\" };\n } finally {\n await conn.close();\n this.removeConnection(conn);\n }\n } catch (err) {\n return {\n ok: false,\n provider: \"smtp-pool\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n\n /** Drains the queue and closes all connections. */\n async close(): Promise<void> {\n this.draining = true;\n await this.drainQueue();\n await Promise.allSettled(this.connections.map((c) => c.close()));\n this.connections.length = 0;\n this.closed = true;\n }\n\n /** Current number of open pooled connections. */\n get connectionCount(): number {\n return this.connections.length;\n }\n\n /** Number of messages waiting in the send queue. */\n get queueSize(): number {\n return this.queue.length;\n }\n\n /** Dispatches queued messages to idle or newly spawned connections. */\n private async processQueue(): Promise<void> {\n if (this.draining) {\n return;\n }\n\n while (this.queue.length > 0) {\n const idleConn = this.connections.find((c) => c.idle && c.usable);\n if (idleConn) {\n const entry = this.queue.shift();\n if (!entry) {\n break;\n }\n try {\n const result = await idleConn.send(entry.options);\n entry.resolve(result);\n if (!idleConn.usable) {\n await idleConn.close();\n this.removeConnection(idleConn);\n }\n } catch (err) {\n entry.reject(err);\n await idleConn.close().catch(() => undefined);\n this.removeConnection(idleConn);\n }\n continue;\n }\n\n if (this.connections.length < this.maxConnections) {\n const entry = this.queue.shift();\n if (!entry) {\n break;\n }\n const conn = await this.spawnConnection();\n try {\n const result = await conn.send(entry.options);\n entry.resolve(result);\n if (!conn.usable) {\n await conn.close();\n this.removeConnection(conn);\n }\n } catch (err) {\n entry.reject(err);\n await conn.close().catch(() => undefined);\n this.removeConnection(conn);\n }\n continue;\n }\n\n break;\n }\n }\n\n /** Opens a new authenticated pooled SMTP connection. */\n private async spawnConnection(): Promise<PooledConnection> {\n const resolved = resolveSMTPConfig(this.config);\n const conn = await createPooledConnection({\n config: this.config,\n maxMessages: this.maxMessages,\n connectHost: resolved.host,\n createAdapter: this.createAdapterFn,\n });\n this.connections.push(conn);\n return conn;\n }\n\n /** Removes a connection from the active pool list. */\n private removeConnection(conn: PooledConnection): void {\n const index = this.connections.indexOf(conn);\n if (index >= 0) {\n this.connections.splice(index, 1);\n }\n }\n\n /** Waits until the send queue and in-flight work are fully drained. */\n private async drainQueue(): Promise<void> {\n while (this.queue.length > 0 || this.connections.some((c) => !c.idle)) {\n await this.processQueue();\n if (this.queue.length > 0) {\n await new Promise((r) => setTimeout(r, 10));\n }\n }\n }\n}\n\n/** @internal Exposed for deterministic rate limiter tests. */\nexport { RateLimiter };\n"
7
- ],
8
- "mappings": "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",
9
- "debugId": "D47B35696A356F3464756E2164756E21",
10
- "names": []
11
- }