@upyo/smtp 0.1.0-dev.9 → 0.1.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.
package/README.md CHANGED
@@ -3,7 +3,16 @@
3
3
  @upyo/smtp
4
4
  ==========
5
5
 
6
- SMTP transport implementation for the Upyo email library.
6
+ [![JSR][JSR badge]][JSR]
7
+ [![npm][npm badge]][npm]
8
+
9
+ SMTP transport implementation for the [Upyo] email library.
10
+
11
+ [JSR]: https://jsr.io/@upyo/smtp
12
+ [JSR badge]: https://jsr.io/badges/@upyo/smtp
13
+ [npm]: https://www.npmjs.com/package/@upyo/smtp
14
+ [npm badge]: https://img.shields.io/npm/v/@upyo/smtp?logo=npm
15
+ [Upyo]: https://upyo.org/
7
16
 
8
17
 
9
18
  Features
@@ -47,8 +56,8 @@ Usage
47
56
  ### Basic Email Sending
48
57
 
49
58
  ~~~~ typescript
59
+ import { createMessage } from "@upyo/core";
50
60
  import { SmtpTransport } from "@upyo/smtp";
51
- import { type Message } from "@upyo/core";
52
61
 
53
62
  const transport = new SmtpTransport({
54
63
  host: "smtp.example.com",
@@ -60,53 +69,19 @@ const transport = new SmtpTransport({
60
69
  },
61
70
  });
62
71
 
63
- const message: Message = {
64
- sender: { name: "John Doe", address: "john@example.com" },
65
- recipients: [{ name: "Jane Doe", address: "jane@example.com" }],
66
- ccRecipients: [],
67
- bccRecipients: [],
68
- replyRecipients: [],
72
+ const message = createMessage({
73
+ from: "sender@example.com",
74
+ to: "recipient@example.net",
69
75
  subject: "Hello from Upyo!",
70
76
  content: { text: "This is a test email." },
71
- attachments: [],
72
- priority: "normal",
73
- tags: [],
74
- headers: new Headers(),
75
- };
76
-
77
- const receipt = await transport.send(message);
78
- console.log("Email sent:", receipt.successful);
79
- ~~~~
80
-
81
- ### HTML Email with Attachments
82
-
83
- ~~~~ typescript
84
- const message: Message = {
85
- sender: { address: "sender@example.com" },
86
- recipients: [{ address: "recipient@example.com" }],
87
- ccRecipients: [],
88
- bccRecipients: [],
89
- replyRecipients: [],
90
- subject: "HTML Email with Attachment",
91
- content: {
92
- html: "<h1>Hello!</h1><p>This is an <strong>HTML</strong> email.</p>",
93
- text: "Hello!\nThis is an HTML email.",
94
- },
95
- attachments: [
96
- {
97
- filename: "document.pdf",
98
- content: new Uint8Array(pdfBytes),
99
- contentType: "application/pdf",
100
- contentId: "doc1",
101
- inline: false,
102
- },
103
- ],
104
- priority: "high",
105
- tags: ["newsletter"],
106
- headers: new Headers([["X-Campaign-ID", "12345"]]),
107
- };
77
+ });
108
78
 
109
79
  const receipt = await transport.send(message);
80
+ if (receipt.successful) {
81
+ console.log("Message sent with ID:", receipt.messageId);
82
+ } else {
83
+ console.error("Send failed:", receipt.errorMessages.join(", "));
84
+ }
110
85
  ~~~~
111
86
 
112
87
  ### Sending Multiple Emails
@@ -115,7 +90,11 @@ const receipt = await transport.send(message);
115
90
  const messages = [message1, message2, message3];
116
91
 
117
92
  for await (const receipt of transport.sendMany(messages)) {
118
- console.log(`Email ${receipt.messageId}: ${receipt.successful ? "sent" : "failed"}`);
93
+ if (receipt.successful) {
94
+ console.log(`Email sent with ID: ${receipt.messageId}`);
95
+ } else {
96
+ console.error(`Email failed: ${receipt.errorMessages.join(", ")}`);
97
+ }
119
98
  }
120
99
  ~~~~
121
100
 
package/dist/index.cjs CHANGED
@@ -31,21 +31,11 @@ const __upyo_core = __toESM(require("@upyo/core"));
31
31
  *
32
32
  * This function takes a partial SMTP configuration and returns a complete
33
33
  * configuration with all optional fields filled with sensible defaults.
34
+ * It is used internally by the SMTP transport.
34
35
  *
35
36
  * @param config - The SMTP configuration with optional fields
36
37
  * @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
- * ```
38
+ * @internal
49
39
  */
50
40
  function createSmtpConfig(config) {
51
41
  return {
@@ -266,7 +256,7 @@ var SmtpConnection = class {
266
256
 
267
257
  //#endregion
268
258
  //#region src/message-converter.ts
269
- function convertMessage(message) {
259
+ async function convertMessage(message) {
270
260
  const envelope = {
271
261
  from: message.sender.address,
272
262
  to: [
@@ -275,23 +265,23 @@ function convertMessage(message) {
275
265
  ...message.bccRecipients.map((r) => r.address)
276
266
  ]
277
267
  };
278
- const raw = buildRawMessage(message);
268
+ const raw = await buildRawMessage(message);
279
269
  return {
280
270
  envelope,
281
271
  raw
282
272
  };
283
273
  }
284
- function buildRawMessage(message) {
274
+ async function buildRawMessage(message) {
285
275
  const lines = [];
286
276
  const boundary = generateBoundary();
287
277
  const hasAttachments = message.attachments.length > 0;
288
278
  const hasHtml = "html" in message.content;
289
279
  const hasText = "text" in message.content;
290
280
  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(", ")}`);
281
+ lines.push(`From: ${encodeHeaderValue((0, __upyo_core.formatAddress)(message.sender))}`);
282
+ lines.push(`To: ${encodeHeaderValue(message.recipients.map(__upyo_core.formatAddress).join(", "))}`);
283
+ if (message.ccRecipients.length > 0) lines.push(`Cc: ${encodeHeaderValue(message.ccRecipients.map(__upyo_core.formatAddress).join(", "))}`);
284
+ if (message.replyRecipients.length > 0) lines.push(`Reply-To: ${encodeHeaderValue(message.replyRecipients.map(__upyo_core.formatAddress).join(", "))}`);
295
285
  lines.push(`Subject: ${encodeHeaderValue(message.subject)}`);
296
286
  lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
297
287
  lines.push(`Message-ID: <${generateMessageId()}>`);
@@ -346,7 +336,7 @@ function buildRawMessage(message) {
346
336
  lines.push(`Content-ID: <${attachment.contentId}>`);
347
337
  } else lines.push(`Content-Disposition: attachment; filename="${attachment.filename}"`);
348
338
  lines.push("");
349
- lines.push(encodeBase64(attachment.content));
339
+ lines.push(encodeBase64(await attachment.content));
350
340
  }
351
341
  lines.push("");
352
342
  lines.push(`--${boundary}--`);
@@ -375,17 +365,48 @@ function encodeHeaderValue(value) {
375
365
  if (!/^[\x20-\x7E]*$/.test(value)) {
376
366
  const utf8Bytes = new TextEncoder().encode(value);
377
367
  const base64 = btoa(String.fromCharCode(...utf8Bytes));
378
- return `=?UTF-8?B?${base64}?=`;
368
+ const maxEncodedLength = 75;
369
+ const encodedWord = `=?UTF-8?B?${base64}?=`;
370
+ if (encodedWord.length <= maxEncodedLength) return encodedWord;
371
+ const words = [];
372
+ let currentBase64 = "";
373
+ for (let i = 0; i < base64.length; i += 4) {
374
+ const chunk = base64.slice(i, i + 4);
375
+ const testWord = `=?UTF-8?B?${currentBase64}${chunk}?=`;
376
+ if (testWord.length <= maxEncodedLength) currentBase64 += chunk;
377
+ else {
378
+ if (currentBase64) words.push(`=?UTF-8?B?${currentBase64}?=`);
379
+ currentBase64 = chunk;
380
+ }
381
+ }
382
+ if (currentBase64) words.push(`=?UTF-8?B?${currentBase64}?=`);
383
+ return words.join(" ");
379
384
  }
380
385
  return value;
381
386
  }
382
387
  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");
388
+ const utf8Bytes = new TextEncoder().encode(text);
389
+ let result = "";
390
+ let lineLength = 0;
391
+ const maxLineLength = 76;
392
+ for (let i = 0; i < utf8Bytes.length; i++) {
393
+ const byte = utf8Bytes[i];
394
+ let encoded = "";
395
+ if (byte < 32 || byte > 126 || byte === 61 || byte === 46 && lineLength === 0) encoded = `=${byte.toString(16).toUpperCase().padStart(2, "0")}`;
396
+ else encoded = String.fromCharCode(byte);
397
+ if (lineLength + encoded.length > maxLineLength) {
398
+ result += "=\r\n";
399
+ lineLength = 0;
400
+ }
401
+ result += encoded;
402
+ lineLength += encoded.length;
403
+ if (byte === 13 && i + 1 < utf8Bytes.length && utf8Bytes[i + 1] === 10) {
404
+ i++;
405
+ result += String.fromCharCode(10);
406
+ lineLength = 0;
407
+ } else if (byte === 10 && (i === 0 || utf8Bytes[i - 1] !== 13)) lineLength = 0;
408
+ }
409
+ return result;
389
410
  }
390
411
  function encodeBase64(data) {
391
412
  const base64 = btoa(String.fromCharCode(...data));
@@ -428,9 +449,15 @@ function encodeBase64(data) {
428
449
  * ```
429
450
  */
430
451
  var SmtpTransport = class {
452
+ /**
453
+ * The SMTP configuration used by this transport.
454
+ */
431
455
  config;
432
- connectionPool = [];
456
+ /**
457
+ * The maximum number of connections in the pool.
458
+ */
433
459
  poolSize;
460
+ connectionPool = [];
434
461
  /**
435
462
  * Creates a new SMTP transport instance.
436
463
  *
@@ -472,21 +499,19 @@ var SmtpTransport = class {
472
499
  const connection = await this.getConnection(options?.signal);
473
500
  try {
474
501
  options?.signal?.throwIfAborted();
475
- const smtpMessage = convertMessage(message);
502
+ const smtpMessage = await convertMessage(message);
476
503
  options?.signal?.throwIfAborted();
477
504
  const messageId = await connection.sendMessage(smtpMessage, options?.signal);
478
505
  await this.returnConnection(connection);
479
506
  return {
480
- messageId,
481
- errorMessages: [],
482
- successful: true
507
+ successful: true,
508
+ messageId
483
509
  };
484
510
  } catch (error) {
485
511
  await this.discardConnection(connection);
486
512
  return {
487
- messageId: "",
488
- errorMessages: [error instanceof Error ? error.message : String(error)],
489
- successful: false
513
+ successful: false,
514
+ errorMessages: [error instanceof Error ? error.message : String(error)]
490
515
  };
491
516
  }
492
517
  }
@@ -528,27 +553,24 @@ var SmtpTransport = class {
528
553
  options?.signal?.throwIfAborted();
529
554
  if (!connectionValid) {
530
555
  yield {
531
- messageId: "",
532
- errorMessages: ["Connection is no longer valid"],
533
- successful: false
556
+ successful: false,
557
+ errorMessages: ["Connection is no longer valid"]
534
558
  };
535
559
  continue;
536
560
  }
537
561
  try {
538
- const smtpMessage = convertMessage(message);
562
+ const smtpMessage = await convertMessage(message);
539
563
  options?.signal?.throwIfAborted();
540
564
  const messageId = await connection.sendMessage(smtpMessage, options?.signal);
541
565
  yield {
542
- messageId,
543
- errorMessages: [],
544
- successful: true
566
+ successful: true,
567
+ messageId
545
568
  };
546
569
  } catch (error) {
547
570
  connectionValid = false;
548
571
  yield {
549
- messageId: "",
550
- errorMessages: [error instanceof Error ? error.message : String(error)],
551
- successful: false
572
+ successful: false,
573
+ errorMessages: [error instanceof Error ? error.message : String(error)]
552
574
  };
553
575
  }
554
576
  }
@@ -556,27 +578,24 @@ var SmtpTransport = class {
556
578
  options?.signal?.throwIfAborted();
557
579
  if (!connectionValid) {
558
580
  yield {
559
- messageId: "",
560
- errorMessages: ["Connection is no longer valid"],
561
- successful: false
581
+ successful: false,
582
+ errorMessages: ["Connection is no longer valid"]
562
583
  };
563
584
  continue;
564
585
  }
565
586
  try {
566
- const smtpMessage = convertMessage(message);
587
+ const smtpMessage = await convertMessage(message);
567
588
  options?.signal?.throwIfAborted();
568
589
  const messageId = await connection.sendMessage(smtpMessage, options?.signal);
569
590
  yield {
570
- messageId,
571
- errorMessages: [],
572
- successful: true
591
+ successful: true,
592
+ messageId
573
593
  };
574
594
  } catch (error) {
575
595
  connectionValid = false;
576
596
  yield {
577
- messageId: "",
578
- errorMessages: [error instanceof Error ? error.message : String(error)],
579
- successful: false
597
+ successful: false,
598
+ errorMessages: [error instanceof Error ? error.message : String(error)]
580
599
  };
581
600
  }
582
601
  }
@@ -661,5 +680,4 @@ var SmtpTransport = class {
661
680
  };
662
681
 
663
682
  //#endregion
664
- exports.SmtpTransport = SmtpTransport;
665
- exports.createSmtpConfig = createSmtpConfig;
683
+ exports.SmtpTransport = SmtpTransport;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,4 @@
1
1
  import { Message, Receipt, Transport, TransportOptions } from "@upyo/core";
2
- import { Socket } from "node:net";
3
- import { TLSSocket } from "node:tls";
4
2
 
5
3
  //#region src/config.d.ts
6
4
 
@@ -154,67 +152,6 @@ interface SmtpTlsOptions {
154
152
  * This type represents the final configuration after applying defaults,
155
153
  * used internally by the SMTP transport implementation.
156
154
  */
157
- type ResolvedSmtpConfig = Omit<Required<SmtpConfig>, "auth" | "tls"> & {
158
- readonly auth?: SmtpAuth;
159
- readonly tls?: SmtpTlsOptions;
160
- };
161
- /**
162
- * Creates a resolved SMTP configuration by applying default values to optional fields.
163
- *
164
- * This function takes a partial SMTP configuration and returns a complete
165
- * configuration with all optional fields filled with sensible defaults.
166
- *
167
- * @param config - The SMTP configuration with optional fields
168
- * @returns A resolved configuration with all defaults applied
169
- *
170
- * @example
171
- * ```typescript
172
- * const resolved = createSmtpConfig({
173
- * host: 'smtp.example.com',
174
- * auth: { user: 'user', pass: 'pass' }
175
- * });
176
- *
177
- * // resolved.port will be 587 (default)
178
- * // resolved.secure will be true (default)
179
- * // resolved.poolSize will be 5 (default)
180
- * ```
181
- */
182
- declare function createSmtpConfig(config: SmtpConfig): ResolvedSmtpConfig;
183
- //#endregion
184
- //#region src/message-converter.d.ts
185
- interface SmtpMessage {
186
- readonly envelope: SmtpEnvelope;
187
- readonly raw: string;
188
- }
189
- interface SmtpEnvelope {
190
- readonly from: string;
191
- readonly to: string[];
192
- }
193
- //#endregion
194
- //#region src/smtp-connection.d.ts
195
- declare class SmtpConnection {
196
- socket: Socket | TLSSocket | null;
197
- config: ResolvedSmtpConfig;
198
- authenticated: boolean;
199
- capabilities: string[];
200
- constructor(config: SmtpConfig);
201
- connect(signal?: AbortSignal): Promise<void>;
202
- sendCommand(command: string, signal?: AbortSignal): Promise<SmtpResponse>;
203
- greeting(signal?: AbortSignal): Promise<SmtpResponse>;
204
- ehlo(signal?: AbortSignal): Promise<void>;
205
- authenticate(signal?: AbortSignal): Promise<void>;
206
- private authPlain;
207
- authLogin(signal?: AbortSignal): Promise<void>;
208
- sendMessage(message: SmtpMessage, signal?: AbortSignal): Promise<string>;
209
- extractMessageId(response: string): string;
210
- quit(): Promise<void>;
211
- reset(signal?: AbortSignal): Promise<void>;
212
- }
213
- interface SmtpResponse {
214
- readonly code: number;
215
- readonly message: string;
216
- readonly raw: string;
217
- }
218
155
  //#endregion
219
156
  //#region src/smtp-transport.d.ts
220
157
  /**
@@ -251,9 +188,15 @@ interface SmtpResponse {
251
188
  * ```
252
189
  */
253
190
  declare class SmtpTransport implements Transport, AsyncDisposable {
191
+ /**
192
+ * The SMTP configuration used by this transport.
193
+ */
254
194
  config: SmtpConfig;
255
- connectionPool: SmtpConnection[];
195
+ /**
196
+ * The maximum number of connections in the pool.
197
+ */
256
198
  poolSize: number;
199
+ private connectionPool;
257
200
  /**
258
201
  * Creates a new SMTP transport instance.
259
202
  *
@@ -317,10 +260,10 @@ declare class SmtpTransport implements Transport, AsyncDisposable {
317
260
  * @returns An async iterable of receipts, one for each message.
318
261
  */
319
262
  sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
320
- getConnection(signal?: AbortSignal): Promise<SmtpConnection>;
321
- connectAndSetup(connection: SmtpConnection, signal?: AbortSignal): Promise<void>;
322
- returnConnection(connection: SmtpConnection): Promise<void>;
323
- discardConnection(connection: SmtpConnection): Promise<void>;
263
+ private getConnection;
264
+ private connectAndSetup;
265
+ private returnConnection;
266
+ private discardConnection;
324
267
  /**
325
268
  * Closes all active SMTP connections in the connection pool.
326
269
  *
@@ -352,4 +295,4 @@ declare class SmtpTransport implements Transport, AsyncDisposable {
352
295
  [Symbol.asyncDispose](): Promise<void>;
353
296
  }
354
297
  //#endregion
355
- export { SmtpAuth, SmtpConfig, SmtpTlsOptions, SmtpTransport, createSmtpConfig };
298
+ export { SmtpAuth, SmtpConfig, SmtpTlsOptions, SmtpTransport };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { Socket } from "node:net";
2
- import { TLSSocket } from "node:tls";
3
1
  import { Message, Receipt, Transport, TransportOptions } from "@upyo/core";
4
2
 
5
3
  //#region src/config.d.ts
@@ -154,67 +152,6 @@ interface SmtpTlsOptions {
154
152
  * This type represents the final configuration after applying defaults,
155
153
  * used internally by the SMTP transport implementation.
156
154
  */
157
- type ResolvedSmtpConfig = Omit<Required<SmtpConfig>, "auth" | "tls"> & {
158
- readonly auth?: SmtpAuth;
159
- readonly tls?: SmtpTlsOptions;
160
- };
161
- /**
162
- * Creates a resolved SMTP configuration by applying default values to optional fields.
163
- *
164
- * This function takes a partial SMTP configuration and returns a complete
165
- * configuration with all optional fields filled with sensible defaults.
166
- *
167
- * @param config - The SMTP configuration with optional fields
168
- * @returns A resolved configuration with all defaults applied
169
- *
170
- * @example
171
- * ```typescript
172
- * const resolved = createSmtpConfig({
173
- * host: 'smtp.example.com',
174
- * auth: { user: 'user', pass: 'pass' }
175
- * });
176
- *
177
- * // resolved.port will be 587 (default)
178
- * // resolved.secure will be true (default)
179
- * // resolved.poolSize will be 5 (default)
180
- * ```
181
- */
182
- declare function createSmtpConfig(config: SmtpConfig): ResolvedSmtpConfig;
183
- //#endregion
184
- //#region src/message-converter.d.ts
185
- interface SmtpMessage {
186
- readonly envelope: SmtpEnvelope;
187
- readonly raw: string;
188
- }
189
- interface SmtpEnvelope {
190
- readonly from: string;
191
- readonly to: string[];
192
- }
193
- //#endregion
194
- //#region src/smtp-connection.d.ts
195
- declare class SmtpConnection {
196
- socket: Socket | TLSSocket | null;
197
- config: ResolvedSmtpConfig;
198
- authenticated: boolean;
199
- capabilities: string[];
200
- constructor(config: SmtpConfig);
201
- connect(signal?: AbortSignal): Promise<void>;
202
- sendCommand(command: string, signal?: AbortSignal): Promise<SmtpResponse>;
203
- greeting(signal?: AbortSignal): Promise<SmtpResponse>;
204
- ehlo(signal?: AbortSignal): Promise<void>;
205
- authenticate(signal?: AbortSignal): Promise<void>;
206
- private authPlain;
207
- authLogin(signal?: AbortSignal): Promise<void>;
208
- sendMessage(message: SmtpMessage, signal?: AbortSignal): Promise<string>;
209
- extractMessageId(response: string): string;
210
- quit(): Promise<void>;
211
- reset(signal?: AbortSignal): Promise<void>;
212
- }
213
- interface SmtpResponse {
214
- readonly code: number;
215
- readonly message: string;
216
- readonly raw: string;
217
- }
218
155
  //#endregion
219
156
  //#region src/smtp-transport.d.ts
220
157
  /**
@@ -251,9 +188,15 @@ interface SmtpResponse {
251
188
  * ```
252
189
  */
253
190
  declare class SmtpTransport implements Transport, AsyncDisposable {
191
+ /**
192
+ * The SMTP configuration used by this transport.
193
+ */
254
194
  config: SmtpConfig;
255
- connectionPool: SmtpConnection[];
195
+ /**
196
+ * The maximum number of connections in the pool.
197
+ */
256
198
  poolSize: number;
199
+ private connectionPool;
257
200
  /**
258
201
  * Creates a new SMTP transport instance.
259
202
  *
@@ -317,10 +260,10 @@ declare class SmtpTransport implements Transport, AsyncDisposable {
317
260
  * @returns An async iterable of receipts, one for each message.
318
261
  */
319
262
  sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
320
- getConnection(signal?: AbortSignal): Promise<SmtpConnection>;
321
- connectAndSetup(connection: SmtpConnection, signal?: AbortSignal): Promise<void>;
322
- returnConnection(connection: SmtpConnection): Promise<void>;
323
- discardConnection(connection: SmtpConnection): Promise<void>;
263
+ private getConnection;
264
+ private connectAndSetup;
265
+ private returnConnection;
266
+ private discardConnection;
324
267
  /**
325
268
  * Closes all active SMTP connections in the connection pool.
326
269
  *
@@ -352,4 +295,4 @@ declare class SmtpTransport implements Transport, AsyncDisposable {
352
295
  [Symbol.asyncDispose](): Promise<void>;
353
296
  }
354
297
  //#endregion
355
- export { SmtpAuth, SmtpConfig, SmtpTlsOptions, SmtpTransport, createSmtpConfig };
298
+ export { SmtpAuth, SmtpConfig, SmtpTlsOptions, SmtpTransport };
package/dist/index.js CHANGED
@@ -8,21 +8,11 @@ import { formatAddress } from "@upyo/core";
8
8
  *
9
9
  * This function takes a partial SMTP configuration and returns a complete
10
10
  * configuration with all optional fields filled with sensible defaults.
11
+ * It is used internally by the SMTP transport.
11
12
  *
12
13
  * @param config - The SMTP configuration with optional fields
13
14
  * @returns A resolved configuration with all defaults applied
14
- *
15
- * @example
16
- * ```typescript
17
- * const resolved = createSmtpConfig({
18
- * host: 'smtp.example.com',
19
- * auth: { user: 'user', pass: 'pass' }
20
- * });
21
- *
22
- * // resolved.port will be 587 (default)
23
- * // resolved.secure will be true (default)
24
- * // resolved.poolSize will be 5 (default)
25
- * ```
15
+ * @internal
26
16
  */
27
17
  function createSmtpConfig(config) {
28
18
  return {
@@ -243,7 +233,7 @@ var SmtpConnection = class {
243
233
 
244
234
  //#endregion
245
235
  //#region src/message-converter.ts
246
- function convertMessage(message) {
236
+ async function convertMessage(message) {
247
237
  const envelope = {
248
238
  from: message.sender.address,
249
239
  to: [
@@ -252,23 +242,23 @@ function convertMessage(message) {
252
242
  ...message.bccRecipients.map((r) => r.address)
253
243
  ]
254
244
  };
255
- const raw = buildRawMessage(message);
245
+ const raw = await buildRawMessage(message);
256
246
  return {
257
247
  envelope,
258
248
  raw
259
249
  };
260
250
  }
261
- function buildRawMessage(message) {
251
+ async function buildRawMessage(message) {
262
252
  const lines = [];
263
253
  const boundary = generateBoundary();
264
254
  const hasAttachments = message.attachments.length > 0;
265
255
  const hasHtml = "html" in message.content;
266
256
  const hasText = "text" in message.content;
267
257
  const isMultipart = hasAttachments || hasHtml && hasText;
268
- lines.push(`From: ${formatAddress(message.sender)}`);
269
- lines.push(`To: ${message.recipients.map(formatAddress).join(", ")}`);
270
- if (message.ccRecipients.length > 0) lines.push(`Cc: ${message.ccRecipients.map(formatAddress).join(", ")}`);
271
- if (message.replyRecipients.length > 0) lines.push(`Reply-To: ${message.replyRecipients.map(formatAddress).join(", ")}`);
258
+ lines.push(`From: ${encodeHeaderValue(formatAddress(message.sender))}`);
259
+ lines.push(`To: ${encodeHeaderValue(message.recipients.map(formatAddress).join(", "))}`);
260
+ if (message.ccRecipients.length > 0) lines.push(`Cc: ${encodeHeaderValue(message.ccRecipients.map(formatAddress).join(", "))}`);
261
+ if (message.replyRecipients.length > 0) lines.push(`Reply-To: ${encodeHeaderValue(message.replyRecipients.map(formatAddress).join(", "))}`);
272
262
  lines.push(`Subject: ${encodeHeaderValue(message.subject)}`);
273
263
  lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
274
264
  lines.push(`Message-ID: <${generateMessageId()}>`);
@@ -323,7 +313,7 @@ function buildRawMessage(message) {
323
313
  lines.push(`Content-ID: <${attachment.contentId}>`);
324
314
  } else lines.push(`Content-Disposition: attachment; filename="${attachment.filename}"`);
325
315
  lines.push("");
326
- lines.push(encodeBase64(attachment.content));
316
+ lines.push(encodeBase64(await attachment.content));
327
317
  }
328
318
  lines.push("");
329
319
  lines.push(`--${boundary}--`);
@@ -352,17 +342,48 @@ function encodeHeaderValue(value) {
352
342
  if (!/^[\x20-\x7E]*$/.test(value)) {
353
343
  const utf8Bytes = new TextEncoder().encode(value);
354
344
  const base64 = btoa(String.fromCharCode(...utf8Bytes));
355
- return `=?UTF-8?B?${base64}?=`;
345
+ const maxEncodedLength = 75;
346
+ const encodedWord = `=?UTF-8?B?${base64}?=`;
347
+ if (encodedWord.length <= maxEncodedLength) return encodedWord;
348
+ const words = [];
349
+ let currentBase64 = "";
350
+ for (let i = 0; i < base64.length; i += 4) {
351
+ const chunk = base64.slice(i, i + 4);
352
+ const testWord = `=?UTF-8?B?${currentBase64}${chunk}?=`;
353
+ if (testWord.length <= maxEncodedLength) currentBase64 += chunk;
354
+ else {
355
+ if (currentBase64) words.push(`=?UTF-8?B?${currentBase64}?=`);
356
+ currentBase64 = chunk;
357
+ }
358
+ }
359
+ if (currentBase64) words.push(`=?UTF-8?B?${currentBase64}?=`);
360
+ return words.join(" ");
356
361
  }
357
362
  return value;
358
363
  }
359
364
  function encodeQuotedPrintable(text) {
360
- return text.replace(/[^\x20-\x7E]/g, (char) => {
361
- const code = char.charCodeAt(0);
362
- if (code < 256) return `=${code.toString(16).toUpperCase().padStart(2, "0")}`;
363
- const utf8 = new TextEncoder().encode(char);
364
- return Array.from(utf8).map((byte) => `=${byte.toString(16).toUpperCase().padStart(2, "0")}`).join("");
365
- }).replace(/=$/gm, "=3D").replace(/^\./, "=2E");
365
+ const utf8Bytes = new TextEncoder().encode(text);
366
+ let result = "";
367
+ let lineLength = 0;
368
+ const maxLineLength = 76;
369
+ for (let i = 0; i < utf8Bytes.length; i++) {
370
+ const byte = utf8Bytes[i];
371
+ let encoded = "";
372
+ if (byte < 32 || byte > 126 || byte === 61 || byte === 46 && lineLength === 0) encoded = `=${byte.toString(16).toUpperCase().padStart(2, "0")}`;
373
+ else encoded = String.fromCharCode(byte);
374
+ if (lineLength + encoded.length > maxLineLength) {
375
+ result += "=\r\n";
376
+ lineLength = 0;
377
+ }
378
+ result += encoded;
379
+ lineLength += encoded.length;
380
+ if (byte === 13 && i + 1 < utf8Bytes.length && utf8Bytes[i + 1] === 10) {
381
+ i++;
382
+ result += String.fromCharCode(10);
383
+ lineLength = 0;
384
+ } else if (byte === 10 && (i === 0 || utf8Bytes[i - 1] !== 13)) lineLength = 0;
385
+ }
386
+ return result;
366
387
  }
367
388
  function encodeBase64(data) {
368
389
  const base64 = btoa(String.fromCharCode(...data));
@@ -405,9 +426,15 @@ function encodeBase64(data) {
405
426
  * ```
406
427
  */
407
428
  var SmtpTransport = class {
429
+ /**
430
+ * The SMTP configuration used by this transport.
431
+ */
408
432
  config;
409
- connectionPool = [];
433
+ /**
434
+ * The maximum number of connections in the pool.
435
+ */
410
436
  poolSize;
437
+ connectionPool = [];
411
438
  /**
412
439
  * Creates a new SMTP transport instance.
413
440
  *
@@ -449,21 +476,19 @@ var SmtpTransport = class {
449
476
  const connection = await this.getConnection(options?.signal);
450
477
  try {
451
478
  options?.signal?.throwIfAborted();
452
- const smtpMessage = convertMessage(message);
479
+ const smtpMessage = await convertMessage(message);
453
480
  options?.signal?.throwIfAborted();
454
481
  const messageId = await connection.sendMessage(smtpMessage, options?.signal);
455
482
  await this.returnConnection(connection);
456
483
  return {
457
- messageId,
458
- errorMessages: [],
459
- successful: true
484
+ successful: true,
485
+ messageId
460
486
  };
461
487
  } catch (error) {
462
488
  await this.discardConnection(connection);
463
489
  return {
464
- messageId: "",
465
- errorMessages: [error instanceof Error ? error.message : String(error)],
466
- successful: false
490
+ successful: false,
491
+ errorMessages: [error instanceof Error ? error.message : String(error)]
467
492
  };
468
493
  }
469
494
  }
@@ -505,27 +530,24 @@ var SmtpTransport = class {
505
530
  options?.signal?.throwIfAborted();
506
531
  if (!connectionValid) {
507
532
  yield {
508
- messageId: "",
509
- errorMessages: ["Connection is no longer valid"],
510
- successful: false
533
+ successful: false,
534
+ errorMessages: ["Connection is no longer valid"]
511
535
  };
512
536
  continue;
513
537
  }
514
538
  try {
515
- const smtpMessage = convertMessage(message);
539
+ const smtpMessage = await convertMessage(message);
516
540
  options?.signal?.throwIfAborted();
517
541
  const messageId = await connection.sendMessage(smtpMessage, options?.signal);
518
542
  yield {
519
- messageId,
520
- errorMessages: [],
521
- successful: true
543
+ successful: true,
544
+ messageId
522
545
  };
523
546
  } catch (error) {
524
547
  connectionValid = false;
525
548
  yield {
526
- messageId: "",
527
- errorMessages: [error instanceof Error ? error.message : String(error)],
528
- successful: false
549
+ successful: false,
550
+ errorMessages: [error instanceof Error ? error.message : String(error)]
529
551
  };
530
552
  }
531
553
  }
@@ -533,27 +555,24 @@ var SmtpTransport = class {
533
555
  options?.signal?.throwIfAborted();
534
556
  if (!connectionValid) {
535
557
  yield {
536
- messageId: "",
537
- errorMessages: ["Connection is no longer valid"],
538
- successful: false
558
+ successful: false,
559
+ errorMessages: ["Connection is no longer valid"]
539
560
  };
540
561
  continue;
541
562
  }
542
563
  try {
543
- const smtpMessage = convertMessage(message);
564
+ const smtpMessage = await convertMessage(message);
544
565
  options?.signal?.throwIfAborted();
545
566
  const messageId = await connection.sendMessage(smtpMessage, options?.signal);
546
567
  yield {
547
- messageId,
548
- errorMessages: [],
549
- successful: true
568
+ successful: true,
569
+ messageId
550
570
  };
551
571
  } catch (error) {
552
572
  connectionValid = false;
553
573
  yield {
554
- messageId: "",
555
- errorMessages: [error instanceof Error ? error.message : String(error)],
556
- successful: false
574
+ successful: false,
575
+ errorMessages: [error instanceof Error ? error.message : String(error)]
557
576
  };
558
577
  }
559
578
  }
@@ -638,4 +657,4 @@ var SmtpTransport = class {
638
657
  };
639
658
 
640
659
  //#endregion
641
- export { SmtpTransport, createSmtpConfig };
660
+ export { SmtpTransport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/smtp",
3
- "version": "0.1.0-dev.9+a57dde18",
3
+ "version": "0.1.1",
4
4
  "description": "SMTP transport for Upyo email library",
5
5
  "keywords": [
6
6
  "email",
@@ -53,12 +53,12 @@
53
53
  },
54
54
  "sideEffects": false,
55
55
  "peerDependencies": {
56
- "@upyo/core": "0.1.0-dev.9+a57dde18"
56
+ "@upyo/core": "0.1.1"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@dotenvx/dotenvx": "^1.47.3",
60
60
  "tsdown": "^0.12.7",
61
- "typescript": "^5.8.3"
61
+ "typescript": "5.8.3"
62
62
  },
63
63
  "scripts": {
64
64
  "build": "tsdown",