@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 +25 -46
- package/dist/index.cjs +75 -57
- package/dist/index.d.cts +12 -69
- package/dist/index.d.ts +12 -69
- package/dist/index.js +75 -56
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
@upyo/smtp
|
|
4
4
|
==========
|
|
5
5
|
|
|
6
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
successful: true
|
|
507
|
+
successful: true,
|
|
508
|
+
messageId
|
|
483
509
|
};
|
|
484
510
|
} catch (error) {
|
|
485
511
|
await this.discardConnection(connection);
|
|
486
512
|
return {
|
|
487
|
-
|
|
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
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
successful: true
|
|
566
|
+
successful: true,
|
|
567
|
+
messageId
|
|
545
568
|
};
|
|
546
569
|
} catch (error) {
|
|
547
570
|
connectionValid = false;
|
|
548
571
|
yield {
|
|
549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
successful: true
|
|
591
|
+
successful: true,
|
|
592
|
+
messageId
|
|
573
593
|
};
|
|
574
594
|
} catch (error) {
|
|
575
595
|
connectionValid = false;
|
|
576
596
|
yield {
|
|
577
|
-
|
|
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
|
-
|
|
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
|
|
321
|
-
connectAndSetup
|
|
322
|
-
returnConnection
|
|
323
|
-
discardConnection
|
|
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
|
|
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
|
-
|
|
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
|
|
321
|
-
connectAndSetup
|
|
322
|
-
returnConnection
|
|
323
|
-
discardConnection
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
successful: true
|
|
484
|
+
successful: true,
|
|
485
|
+
messageId
|
|
460
486
|
};
|
|
461
487
|
} catch (error) {
|
|
462
488
|
await this.discardConnection(connection);
|
|
463
489
|
return {
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
successful: true
|
|
543
|
+
successful: true,
|
|
544
|
+
messageId
|
|
522
545
|
};
|
|
523
546
|
} catch (error) {
|
|
524
547
|
connectionValid = false;
|
|
525
548
|
yield {
|
|
526
|
-
|
|
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
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
successful: true
|
|
568
|
+
successful: true,
|
|
569
|
+
messageId
|
|
550
570
|
};
|
|
551
571
|
} catch (error) {
|
|
552
572
|
connectionValid = false;
|
|
553
573
|
yield {
|
|
554
|
-
|
|
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
|
|
660
|
+
export { SmtpTransport };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upyo/smtp",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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": "
|
|
61
|
+
"typescript": "5.8.3"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"build": "tsdown",
|