@upyo/smtp 0.1.0-dev.10

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/dist/index.cjs ADDED
@@ -0,0 +1,665 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+ const node_net = __toESM(require("node:net"));
25
+ const node_tls = __toESM(require("node:tls"));
26
+ const __upyo_core = __toESM(require("@upyo/core"));
27
+
28
+ //#region src/config.ts
29
+ /**
30
+ * Creates a resolved SMTP configuration by applying default values to optional fields.
31
+ *
32
+ * This function takes a partial SMTP configuration and returns a complete
33
+ * configuration with all optional fields filled with sensible defaults.
34
+ *
35
+ * @param config - The SMTP configuration with optional fields
36
+ * @returns A resolved configuration with all defaults applied
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const resolved = createSmtpConfig({
41
+ * host: 'smtp.example.com',
42
+ * auth: { user: 'user', pass: 'pass' }
43
+ * });
44
+ *
45
+ * // resolved.port will be 587 (default)
46
+ * // resolved.secure will be true (default)
47
+ * // resolved.poolSize will be 5 (default)
48
+ * ```
49
+ */
50
+ function createSmtpConfig(config) {
51
+ return {
52
+ host: config.host,
53
+ port: config.port ?? 587,
54
+ secure: config.secure ?? true,
55
+ auth: config.auth,
56
+ tls: config.tls,
57
+ connectionTimeout: config.connectionTimeout ?? 6e4,
58
+ socketTimeout: config.socketTimeout ?? 6e4,
59
+ localName: config.localName ?? "localhost",
60
+ pool: config.pool ?? true,
61
+ poolSize: config.poolSize ?? 5
62
+ };
63
+ }
64
+
65
+ //#endregion
66
+ //#region src/smtp-connection.ts
67
+ var SmtpConnection = class {
68
+ socket = null;
69
+ config;
70
+ authenticated = false;
71
+ capabilities = [];
72
+ constructor(config) {
73
+ this.config = createSmtpConfig(config);
74
+ }
75
+ connect(signal) {
76
+ if (this.socket) throw new Error("Connection already established");
77
+ signal?.throwIfAborted();
78
+ return new Promise((resolve, reject) => {
79
+ const timeout = setTimeout(() => {
80
+ this.socket?.destroy();
81
+ reject(/* @__PURE__ */ new Error("Connection timeout"));
82
+ }, this.config.connectionTimeout);
83
+ const onConnect = () => {
84
+ clearTimeout(timeout);
85
+ resolve();
86
+ };
87
+ const onError = (error) => {
88
+ clearTimeout(timeout);
89
+ reject(error);
90
+ };
91
+ if (this.config.secure) this.socket = (0, node_tls.connect)({
92
+ host: this.config.host,
93
+ port: this.config.port,
94
+ rejectUnauthorized: this.config.tls?.rejectUnauthorized ?? true,
95
+ ca: this.config.tls?.ca,
96
+ key: this.config.tls?.key,
97
+ cert: this.config.tls?.cert,
98
+ minVersion: this.config.tls?.minVersion,
99
+ maxVersion: this.config.tls?.maxVersion
100
+ });
101
+ else {
102
+ this.socket = new node_net.Socket();
103
+ this.socket.connect(this.config.port, this.config.host);
104
+ }
105
+ this.socket.setTimeout(this.config.socketTimeout);
106
+ this.socket.once("connect", onConnect);
107
+ this.socket.once("error", onError);
108
+ this.socket.once("timeout", () => {
109
+ clearTimeout(timeout);
110
+ this.socket?.destroy();
111
+ reject(/* @__PURE__ */ new Error("Socket timeout"));
112
+ });
113
+ });
114
+ }
115
+ sendCommand(command, signal) {
116
+ if (!this.socket) throw new Error("Not connected");
117
+ signal?.throwIfAborted();
118
+ return new Promise((resolve, reject) => {
119
+ let buffer = "";
120
+ const timeout = setTimeout(() => {
121
+ reject(/* @__PURE__ */ new Error("Command timeout"));
122
+ }, this.config.socketTimeout);
123
+ const onData = (data) => {
124
+ buffer += data.toString();
125
+ const lines = buffer.split("\r\n");
126
+ const incompleteLine = lines.pop() || "";
127
+ for (let i = 0; i < lines.length; i++) {
128
+ const line = lines[i];
129
+ if (line.length >= 4 && line[3] === " ") {
130
+ const code = parseInt(line.substring(0, 3), 10);
131
+ const message = line.substring(4);
132
+ const fullResponse = lines.slice(0, i + 1).join("\r\n");
133
+ cleanup();
134
+ resolve({
135
+ code,
136
+ message,
137
+ raw: fullResponse
138
+ });
139
+ return;
140
+ }
141
+ }
142
+ buffer = incompleteLine;
143
+ };
144
+ const onError = (error) => {
145
+ cleanup();
146
+ reject(error);
147
+ };
148
+ const cleanup = () => {
149
+ clearTimeout(timeout);
150
+ this.socket?.off("data", onData);
151
+ this.socket?.off("error", onError);
152
+ };
153
+ this.socket.on("data", onData);
154
+ this.socket.on("error", onError);
155
+ this.socket.write(command + "\r\n");
156
+ });
157
+ }
158
+ greeting(signal) {
159
+ if (!this.socket) throw new Error("Not connected");
160
+ signal?.throwIfAborted();
161
+ return new Promise((resolve, reject) => {
162
+ let buffer = "";
163
+ const timeout = setTimeout(() => {
164
+ reject(/* @__PURE__ */ new Error("Greeting timeout"));
165
+ }, this.config.socketTimeout);
166
+ const onData = (data) => {
167
+ buffer += data.toString();
168
+ const lines = buffer.split("\r\n");
169
+ for (const line of lines) if (line.length >= 4 && line[3] === " ") {
170
+ const code = parseInt(line.substring(0, 3), 10);
171
+ const message = line.substring(4);
172
+ cleanup();
173
+ resolve({
174
+ code,
175
+ message,
176
+ raw: buffer
177
+ });
178
+ return;
179
+ }
180
+ };
181
+ const onError = (error) => {
182
+ cleanup();
183
+ reject(error);
184
+ };
185
+ const cleanup = () => {
186
+ clearTimeout(timeout);
187
+ this.socket?.off("data", onData);
188
+ this.socket?.off("error", onError);
189
+ };
190
+ this.socket.on("data", onData);
191
+ this.socket.on("error", onError);
192
+ });
193
+ }
194
+ async ehlo(signal) {
195
+ const response = await this.sendCommand(`EHLO ${this.config.localName}`, signal);
196
+ if (response.code !== 250) throw new Error(`EHLO failed: ${response.message}`);
197
+ this.capabilities = response.raw.split("\r\n").filter((line) => line.startsWith("250-") || line.startsWith("250 ")).map((line) => line.substring(4)).filter((line) => line.length > 0);
198
+ }
199
+ async authenticate(signal) {
200
+ if (!this.config.auth) return;
201
+ if (this.authenticated) return;
202
+ const authMethod = this.config.auth.method ?? "plain";
203
+ if (!this.capabilities.some((cap) => cap.toUpperCase().startsWith("AUTH"))) throw new Error("Server does not support authentication");
204
+ switch (authMethod) {
205
+ case "plain":
206
+ await this.authPlain(signal);
207
+ break;
208
+ case "login":
209
+ await this.authLogin(signal);
210
+ break;
211
+ default: throw new Error(`Unsupported authentication method: ${authMethod}`);
212
+ }
213
+ this.authenticated = true;
214
+ }
215
+ async authPlain(signal) {
216
+ const { user, pass } = this.config.auth;
217
+ const credentials = btoa(`\0${user}\0${pass}`);
218
+ const response = await this.sendCommand(`AUTH PLAIN ${credentials}`, signal);
219
+ if (response.code !== 235) throw new Error(`Authentication failed: ${response.message}`);
220
+ }
221
+ async authLogin(signal) {
222
+ const { user, pass } = this.config.auth;
223
+ let response = await this.sendCommand("AUTH LOGIN", signal);
224
+ if (response.code !== 334) throw new Error(`AUTH LOGIN failed: ${response.message}`);
225
+ response = await this.sendCommand(btoa(user), signal);
226
+ if (response.code !== 334) throw new Error(`Username authentication failed: ${response.message}`);
227
+ response = await this.sendCommand(btoa(pass), signal);
228
+ if (response.code !== 235) throw new Error(`Password authentication failed: ${response.message}`);
229
+ }
230
+ async sendMessage(message, signal) {
231
+ const mailResponse = await this.sendCommand(`MAIL FROM:<${message.envelope.from}>`, signal);
232
+ if (mailResponse.code !== 250) throw new Error(`MAIL FROM failed: ${mailResponse.message}`);
233
+ for (const recipient of message.envelope.to) {
234
+ signal?.throwIfAborted();
235
+ const rcptResponse = await this.sendCommand(`RCPT TO:<${recipient}>`, signal);
236
+ if (rcptResponse.code !== 250) throw new Error(`RCPT TO failed for ${recipient}: ${rcptResponse.message}`);
237
+ }
238
+ const dataResponse = await this.sendCommand("DATA", signal);
239
+ if (dataResponse.code !== 354) throw new Error(`DATA failed: ${dataResponse.message}`);
240
+ const content = message.raw.replace(/\n\./g, "\n..");
241
+ const finalResponse = await this.sendCommand(`${content}\r\n.`, signal);
242
+ if (finalResponse.code !== 250) throw new Error(`Message send failed: ${finalResponse.message}`);
243
+ const messageId = this.extractMessageId(finalResponse.message);
244
+ return messageId;
245
+ }
246
+ extractMessageId(response) {
247
+ const match = response.match(/(?:Message-ID:|id=)[\s<]*([^>\s]+)/i);
248
+ return match ? match[1] : `smtp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
249
+ }
250
+ async quit() {
251
+ if (!this.socket) return;
252
+ try {
253
+ await this.sendCommand("QUIT");
254
+ } catch {}
255
+ this.socket.destroy();
256
+ this.socket = null;
257
+ this.authenticated = false;
258
+ this.capabilities = [];
259
+ }
260
+ async reset(signal) {
261
+ if (!this.socket) throw new Error("Not connected");
262
+ const response = await this.sendCommand("RSET", signal);
263
+ if (response.code !== 250) throw new Error(`RESET failed: ${response.message}`);
264
+ }
265
+ };
266
+
267
+ //#endregion
268
+ //#region src/message-converter.ts
269
+ function convertMessage(message) {
270
+ const envelope = {
271
+ from: message.sender.address,
272
+ to: [
273
+ ...message.recipients.map((r) => r.address),
274
+ ...message.ccRecipients.map((r) => r.address),
275
+ ...message.bccRecipients.map((r) => r.address)
276
+ ]
277
+ };
278
+ const raw = buildRawMessage(message);
279
+ return {
280
+ envelope,
281
+ raw
282
+ };
283
+ }
284
+ function buildRawMessage(message) {
285
+ const lines = [];
286
+ const boundary = generateBoundary();
287
+ const hasAttachments = message.attachments.length > 0;
288
+ const hasHtml = "html" in message.content;
289
+ const hasText = "text" in message.content;
290
+ const isMultipart = hasAttachments || hasHtml && hasText;
291
+ lines.push(`From: ${(0, __upyo_core.formatAddress)(message.sender)}`);
292
+ lines.push(`To: ${message.recipients.map(__upyo_core.formatAddress).join(", ")}`);
293
+ if (message.ccRecipients.length > 0) lines.push(`Cc: ${message.ccRecipients.map(__upyo_core.formatAddress).join(", ")}`);
294
+ if (message.replyRecipients.length > 0) lines.push(`Reply-To: ${message.replyRecipients.map(__upyo_core.formatAddress).join(", ")}`);
295
+ lines.push(`Subject: ${encodeHeaderValue(message.subject)}`);
296
+ lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
297
+ lines.push(`Message-ID: <${generateMessageId()}>`);
298
+ if (message.priority !== "normal") {
299
+ const priorityValue = message.priority === "high" ? "1" : "5";
300
+ lines.push(`X-Priority: ${priorityValue}`);
301
+ lines.push(`X-MSMail-Priority: ${message.priority === "high" ? "High" : "Low"}`);
302
+ }
303
+ for (const [key, value] of message.headers) lines.push(`${key}: ${encodeHeaderValue(value)}`);
304
+ lines.push("MIME-Version: 1.0");
305
+ if (isMultipart) {
306
+ lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
307
+ lines.push("");
308
+ lines.push("This is a multi-part message in MIME format.");
309
+ lines.push("");
310
+ lines.push(`--${boundary}`);
311
+ if (hasHtml && hasText) {
312
+ const contentBoundary = generateBoundary();
313
+ lines.push(`Content-Type: multipart/alternative; boundary="${contentBoundary}"`);
314
+ lines.push("");
315
+ lines.push(`--${contentBoundary}`);
316
+ lines.push("Content-Type: text/plain; charset=utf-8");
317
+ lines.push("Content-Transfer-Encoding: quoted-printable");
318
+ lines.push("");
319
+ lines.push(encodeQuotedPrintable(message.content.text));
320
+ lines.push("");
321
+ lines.push(`--${contentBoundary}`);
322
+ lines.push("Content-Type: text/html; charset=utf-8");
323
+ lines.push("Content-Transfer-Encoding: quoted-printable");
324
+ lines.push("");
325
+ lines.push(encodeQuotedPrintable(message.content.html));
326
+ lines.push("");
327
+ lines.push(`--${contentBoundary}--`);
328
+ } else if (hasHtml) {
329
+ lines.push("Content-Type: text/html; charset=utf-8");
330
+ lines.push("Content-Transfer-Encoding: quoted-printable");
331
+ lines.push("");
332
+ lines.push(encodeQuotedPrintable(message.content.html));
333
+ } else {
334
+ lines.push("Content-Type: text/plain; charset=utf-8");
335
+ lines.push("Content-Transfer-Encoding: quoted-printable");
336
+ lines.push("");
337
+ lines.push(encodeQuotedPrintable(message.content.text));
338
+ }
339
+ for (const attachment of message.attachments) {
340
+ lines.push("");
341
+ lines.push(`--${boundary}`);
342
+ lines.push(`Content-Type: ${attachment.contentType}; name="${attachment.filename}"`);
343
+ lines.push("Content-Transfer-Encoding: base64");
344
+ if (attachment.inline) {
345
+ lines.push(`Content-Disposition: inline; filename="${attachment.filename}"`);
346
+ lines.push(`Content-ID: <${attachment.contentId}>`);
347
+ } else lines.push(`Content-Disposition: attachment; filename="${attachment.filename}"`);
348
+ lines.push("");
349
+ lines.push(encodeBase64(attachment.content));
350
+ }
351
+ lines.push("");
352
+ lines.push(`--${boundary}--`);
353
+ } else if (hasHtml) {
354
+ lines.push("Content-Type: text/html; charset=utf-8");
355
+ lines.push("Content-Transfer-Encoding: quoted-printable");
356
+ lines.push("");
357
+ lines.push(encodeQuotedPrintable(message.content.html));
358
+ } else {
359
+ lines.push("Content-Type: text/plain; charset=utf-8");
360
+ lines.push("Content-Transfer-Encoding: quoted-printable");
361
+ lines.push("");
362
+ lines.push(encodeQuotedPrintable(message.content.text));
363
+ }
364
+ return lines.join("\r\n");
365
+ }
366
+ function generateBoundary() {
367
+ return `boundary-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
368
+ }
369
+ function generateMessageId() {
370
+ const timestamp = Date.now();
371
+ const random = Math.random().toString(36).substr(2, 9);
372
+ return `${timestamp}.${random}@upyo.local`;
373
+ }
374
+ function encodeHeaderValue(value) {
375
+ if (!/^[\x20-\x7E]*$/.test(value)) {
376
+ const utf8Bytes = new TextEncoder().encode(value);
377
+ const base64 = btoa(String.fromCharCode(...utf8Bytes));
378
+ return `=?UTF-8?B?${base64}?=`;
379
+ }
380
+ return value;
381
+ }
382
+ function encodeQuotedPrintable(text) {
383
+ return text.replace(/[^\x20-\x7E]/g, (char) => {
384
+ const code = char.charCodeAt(0);
385
+ if (code < 256) return `=${code.toString(16).toUpperCase().padStart(2, "0")}`;
386
+ const utf8 = new TextEncoder().encode(char);
387
+ return Array.from(utf8).map((byte) => `=${byte.toString(16).toUpperCase().padStart(2, "0")}`).join("");
388
+ }).replace(/=$/gm, "=3D").replace(/^\./, "=2E");
389
+ }
390
+ function encodeBase64(data) {
391
+ const base64 = btoa(String.fromCharCode(...data));
392
+ return base64.replace(/(.{76})/g, "$1\r\n").trim();
393
+ }
394
+
395
+ //#endregion
396
+ //#region src/smtp-transport.ts
397
+ /**
398
+ * SMTP transport implementation for sending emails via SMTP protocol.
399
+ *
400
+ * This transport provides efficient email delivery with connection pooling,
401
+ * support for authentication, TLS/SSL encryption, and batch sending capabilities.
402
+ *
403
+ * @example
404
+ * ```typescript
405
+ * import { SmtpTransport } from '@upyo/smtp';
406
+ *
407
+ * // Automatic resource cleanup with using statement
408
+ * await using transport = new SmtpTransport({
409
+ * host: 'smtp.gmail.com',
410
+ * port: 465,
411
+ * secure: true, // Use TLS from start
412
+ * auth: {
413
+ * user: 'user@gmail.com',
414
+ * pass: 'app-password'
415
+ * }
416
+ * });
417
+ *
418
+ * const receipt = await transport.send(message);
419
+ * // Connections are automatically closed here
420
+ *
421
+ * // Or manual management
422
+ * const transport2 = new SmtpTransport(config);
423
+ * try {
424
+ * await transport2.send(message);
425
+ * } finally {
426
+ * await transport2.closeAllConnections();
427
+ * }
428
+ * ```
429
+ */
430
+ var SmtpTransport = class {
431
+ config;
432
+ connectionPool = [];
433
+ poolSize;
434
+ /**
435
+ * Creates a new SMTP transport instance.
436
+ *
437
+ * @param config SMTP configuration including server details, authentication,
438
+ * and options.
439
+ */
440
+ constructor(config) {
441
+ this.config = config;
442
+ this.poolSize = config.poolSize ?? 5;
443
+ }
444
+ /**
445
+ * Sends a single email message via SMTP.
446
+ *
447
+ * This method converts the message to SMTP format, establishes a connection
448
+ * to the SMTP server, sends the message, and returns a receipt with the result.
449
+ *
450
+ * @example
451
+ * ```typescript
452
+ * const receipt = await transport.send({
453
+ * sender: { address: 'from@example.com' },
454
+ * recipients: [{ address: 'to@example.com' }],
455
+ * subject: 'Hello',
456
+ * content: { text: 'Hello World!' }
457
+ * });
458
+ *
459
+ * if (receipt.successful) {
460
+ * console.log('Message sent with ID:', receipt.messageId);
461
+ * }
462
+ * ```
463
+ *
464
+ * @param message The email message to send.
465
+ * @param options Optional transport options including `AbortSignal` for
466
+ * cancellation.
467
+ * @returns A promise that resolves to a receipt indicating success or
468
+ * failure.
469
+ */
470
+ async send(message, options) {
471
+ options?.signal?.throwIfAborted();
472
+ const connection = await this.getConnection(options?.signal);
473
+ try {
474
+ options?.signal?.throwIfAborted();
475
+ const smtpMessage = convertMessage(message);
476
+ options?.signal?.throwIfAborted();
477
+ const messageId = await connection.sendMessage(smtpMessage, options?.signal);
478
+ await this.returnConnection(connection);
479
+ return {
480
+ messageId,
481
+ errorMessages: [],
482
+ successful: true
483
+ };
484
+ } catch (error) {
485
+ await this.discardConnection(connection);
486
+ return {
487
+ messageId: "",
488
+ errorMessages: [error instanceof Error ? error.message : String(error)],
489
+ successful: false
490
+ };
491
+ }
492
+ }
493
+ /**
494
+ * Sends multiple email messages efficiently using a single SMTP connection.
495
+ *
496
+ * This method is optimized for bulk email sending by reusing a single SMTP
497
+ * connection for all messages, which significantly improves performance
498
+ * compared to sending each message individually.
499
+ *
500
+ * @example
501
+ * ```typescript
502
+ * const messages = [
503
+ * { subject: 'Message 1', recipients: [{ address: 'user1@example.com' }], ... },
504
+ * { subject: 'Message 2', recipients: [{ address: 'user2@example.com' }], ... }
505
+ * ];
506
+ *
507
+ * for await (const receipt of transport.sendMany(messages)) {
508
+ * if (receipt.successful) {
509
+ * console.log('Sent:', receipt.messageId);
510
+ * } else {
511
+ * console.error('Failed:', receipt.errorMessages);
512
+ * }
513
+ * }
514
+ * ```
515
+ *
516
+ * @param messages An iterable or async iterable of messages to send.
517
+ * @param options Optional transport options including `AbortSignal` for
518
+ * cancellation.
519
+ * @returns An async iterable of receipts, one for each message.
520
+ */
521
+ async *sendMany(messages, options) {
522
+ options?.signal?.throwIfAborted();
523
+ const connection = await this.getConnection(options?.signal);
524
+ let connectionValid = true;
525
+ try {
526
+ const isAsyncIterable = Symbol.asyncIterator in messages;
527
+ if (isAsyncIterable) for await (const message of messages) {
528
+ options?.signal?.throwIfAborted();
529
+ if (!connectionValid) {
530
+ yield {
531
+ messageId: "",
532
+ errorMessages: ["Connection is no longer valid"],
533
+ successful: false
534
+ };
535
+ continue;
536
+ }
537
+ try {
538
+ const smtpMessage = convertMessage(message);
539
+ options?.signal?.throwIfAborted();
540
+ const messageId = await connection.sendMessage(smtpMessage, options?.signal);
541
+ yield {
542
+ messageId,
543
+ errorMessages: [],
544
+ successful: true
545
+ };
546
+ } catch (error) {
547
+ connectionValid = false;
548
+ yield {
549
+ messageId: "",
550
+ errorMessages: [error instanceof Error ? error.message : String(error)],
551
+ successful: false
552
+ };
553
+ }
554
+ }
555
+ else for (const message of messages) {
556
+ options?.signal?.throwIfAborted();
557
+ if (!connectionValid) {
558
+ yield {
559
+ messageId: "",
560
+ errorMessages: ["Connection is no longer valid"],
561
+ successful: false
562
+ };
563
+ continue;
564
+ }
565
+ try {
566
+ const smtpMessage = convertMessage(message);
567
+ options?.signal?.throwIfAborted();
568
+ const messageId = await connection.sendMessage(smtpMessage, options?.signal);
569
+ yield {
570
+ messageId,
571
+ errorMessages: [],
572
+ successful: true
573
+ };
574
+ } catch (error) {
575
+ connectionValid = false;
576
+ yield {
577
+ messageId: "",
578
+ errorMessages: [error instanceof Error ? error.message : String(error)],
579
+ successful: false
580
+ };
581
+ }
582
+ }
583
+ if (connectionValid) await this.returnConnection(connection);
584
+ else await this.discardConnection(connection);
585
+ } catch (error) {
586
+ await this.discardConnection(connection);
587
+ throw error;
588
+ }
589
+ }
590
+ async getConnection(signal) {
591
+ signal?.throwIfAborted();
592
+ if (this.connectionPool.length > 0) return this.connectionPool.pop();
593
+ const connection = new SmtpConnection(this.config);
594
+ await this.connectAndSetup(connection, signal);
595
+ return connection;
596
+ }
597
+ async connectAndSetup(connection, signal) {
598
+ signal?.throwIfAborted();
599
+ await connection.connect(signal);
600
+ signal?.throwIfAborted();
601
+ const greeting = await connection.greeting(signal);
602
+ if (greeting.code !== 220) throw new Error(`Server greeting failed: ${greeting.message}`);
603
+ signal?.throwIfAborted();
604
+ await connection.ehlo(signal);
605
+ signal?.throwIfAborted();
606
+ await connection.authenticate(signal);
607
+ }
608
+ async returnConnection(connection) {
609
+ if (!this.config.pool) {
610
+ await connection.quit();
611
+ return;
612
+ }
613
+ if (this.connectionPool.length < this.poolSize) try {
614
+ await connection.reset();
615
+ this.connectionPool.push(connection);
616
+ } catch {
617
+ await this.discardConnection(connection);
618
+ }
619
+ else await connection.quit();
620
+ }
621
+ async discardConnection(connection) {
622
+ try {
623
+ await connection.quit();
624
+ } catch {}
625
+ }
626
+ /**
627
+ * Closes all active SMTP connections in the connection pool.
628
+ *
629
+ * This method should be called when shutting down the application
630
+ * to ensure all connections are properly closed and resources are freed.
631
+ *
632
+ * @example
633
+ * ```typescript
634
+ * // At application shutdown
635
+ * await transport.closeAllConnections();
636
+ * ```
637
+ */
638
+ async closeAllConnections() {
639
+ const connections = [...this.connectionPool];
640
+ this.connectionPool = [];
641
+ await Promise.all(connections.map((connection) => this.discardConnection(connection)));
642
+ }
643
+ /**
644
+ * Implements AsyncDisposable interface for automatic resource cleanup.
645
+ *
646
+ * This method is called automatically when using the `using` keyword,
647
+ * ensuring that all SMTP connections are properly closed when the
648
+ * transport goes out of scope.
649
+ *
650
+ * @example
651
+ * ```typescript
652
+ * // Automatic cleanup with using statement
653
+ * await using transport = new SmtpTransport(config);
654
+ * await transport.send(message);
655
+ * // Connections are automatically closed here
656
+ * ```
657
+ */
658
+ async [Symbol.asyncDispose]() {
659
+ await this.closeAllConnections();
660
+ }
661
+ };
662
+
663
+ //#endregion
664
+ exports.SmtpTransport = SmtpTransport;
665
+ exports.createSmtpConfig = createSmtpConfig;