@upyo/smtp 0.6.0-dev.271 → 0.6.0-dev.272
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 +1 -0
- package/dist/index.cjs +93 -14
- package/dist/index.js +93 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Features
|
|
|
26
26
|
- HTML and plain text email support
|
|
27
27
|
- File attachments (regular and inline)
|
|
28
28
|
- Multiple recipients (To, CC, BCC)
|
|
29
|
+
- SMTP PIPELINING for faster multi-recipient delivery
|
|
29
30
|
- Custom headers
|
|
30
31
|
- Priority levels
|
|
31
32
|
- Comprehensive testing utilities
|
package/dist/index.cjs
CHANGED
|
@@ -361,6 +361,16 @@ var OAuth2TokenManager = class {
|
|
|
361
361
|
|
|
362
362
|
//#endregion
|
|
363
363
|
//#region src/smtp-connection.ts
|
|
364
|
+
var SmtpPipelineTerminatedError = class extends Error {
|
|
365
|
+
responseIndex;
|
|
366
|
+
response;
|
|
367
|
+
constructor(responseIndex, response) {
|
|
368
|
+
super("SMTP pipeline terminated by the server.");
|
|
369
|
+
this.name = "SmtpPipelineTerminatedError";
|
|
370
|
+
this.responseIndex = responseIndex;
|
|
371
|
+
this.response = response;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
364
374
|
/**
|
|
365
375
|
* The maximum length of an SMTP command line, including the terminating CRLF,
|
|
366
376
|
* as specified by RFC 5321 §4.5.3.1.4.
|
|
@@ -513,30 +523,59 @@ var SmtpConnection = class {
|
|
|
513
523
|
});
|
|
514
524
|
}
|
|
515
525
|
sendCommand(command, signal) {
|
|
526
|
+
return this.sendCommands([command], signal).then((responses) => responses[0]);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Sends a group of commands in one write and reads one reply per command.
|
|
530
|
+
*
|
|
531
|
+
* SMTP multiline replies are kept together and complete replies are matched
|
|
532
|
+
* to commands by their position in the returned array, as required for
|
|
533
|
+
* command pipelining by RFC 2920.
|
|
534
|
+
*/
|
|
535
|
+
sendCommands(commands, signal) {
|
|
516
536
|
if (!this.socket) throw new Error("Not connected");
|
|
517
537
|
signal?.throwIfAborted();
|
|
518
538
|
return new Promise((resolve, reject) => {
|
|
519
539
|
let buffer = "";
|
|
520
|
-
|
|
540
|
+
let responseLines = [];
|
|
541
|
+
const responses = [];
|
|
542
|
+
const startTimeout = () => setTimeout(() => {
|
|
543
|
+
cleanup();
|
|
521
544
|
reject(/* @__PURE__ */ new Error("Command timeout"));
|
|
522
545
|
}, this.config.socketTimeout);
|
|
546
|
+
let timeout = startTimeout();
|
|
547
|
+
const resetTimeout = () => {
|
|
548
|
+
clearTimeout(timeout);
|
|
549
|
+
timeout = startTimeout();
|
|
550
|
+
};
|
|
523
551
|
const onData = (data) => {
|
|
524
552
|
buffer += data.toString();
|
|
525
553
|
const lines = buffer.split("\r\n");
|
|
526
554
|
const incompleteLine = lines.pop() || "";
|
|
527
|
-
for (
|
|
528
|
-
|
|
555
|
+
for (const line of lines) {
|
|
556
|
+
responseLines.push(line);
|
|
529
557
|
if (line.length >= 4 && line[3] === " ") {
|
|
530
558
|
const code = parseInt(line.substring(0, 3), 10);
|
|
531
559
|
const message = line.substring(4);
|
|
532
|
-
const
|
|
533
|
-
cleanup();
|
|
534
|
-
resolve({
|
|
560
|
+
const response = {
|
|
535
561
|
code,
|
|
536
562
|
message,
|
|
537
|
-
raw:
|
|
538
|
-
}
|
|
539
|
-
|
|
563
|
+
raw: responseLines.join("\r\n")
|
|
564
|
+
};
|
|
565
|
+
const responseIndex = responses.length;
|
|
566
|
+
responses.push(response);
|
|
567
|
+
responseLines = [];
|
|
568
|
+
if (responses.length === commands.length) {
|
|
569
|
+
cleanup();
|
|
570
|
+
resolve(responses);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (response.code === 421) {
|
|
574
|
+
cleanup();
|
|
575
|
+
reject(new SmtpPipelineTerminatedError(responseIndex, response));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
resetTimeout();
|
|
540
579
|
}
|
|
541
580
|
}
|
|
542
581
|
buffer = incompleteLine;
|
|
@@ -545,14 +584,35 @@ var SmtpConnection = class {
|
|
|
545
584
|
cleanup();
|
|
546
585
|
reject(error);
|
|
547
586
|
};
|
|
587
|
+
const onClose = () => {
|
|
588
|
+
cleanup();
|
|
589
|
+
const responseIndex = responses.findIndex((response) => response.code >= 400);
|
|
590
|
+
if (responseIndex >= 0) {
|
|
591
|
+
reject(new SmtpPipelineTerminatedError(responseIndex, responses[responseIndex]));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
reject(/* @__PURE__ */ new Error("Connection closed before all command responses."));
|
|
595
|
+
};
|
|
596
|
+
const onAbort = () => {
|
|
597
|
+
cleanup();
|
|
598
|
+
try {
|
|
599
|
+
signal?.throwIfAborted();
|
|
600
|
+
} catch (error) {
|
|
601
|
+
reject(error);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
548
604
|
const cleanup = () => {
|
|
549
605
|
clearTimeout(timeout);
|
|
550
606
|
this.socket?.off("data", onData);
|
|
551
607
|
this.socket?.off("error", onError);
|
|
608
|
+
this.socket?.off("close", onClose);
|
|
609
|
+
signal?.removeEventListener("abort", onAbort);
|
|
552
610
|
};
|
|
553
611
|
this.socket.on("data", onData);
|
|
554
612
|
this.socket.on("error", onError);
|
|
555
|
-
this.socket.
|
|
613
|
+
this.socket.on("close", onClose);
|
|
614
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
615
|
+
this.socket.write(commands.map((command) => `${command}\r\n`).join(""));
|
|
556
616
|
});
|
|
557
617
|
}
|
|
558
618
|
greeting(signal) {
|
|
@@ -600,7 +660,7 @@ var SmtpConnection = class {
|
|
|
600
660
|
return;
|
|
601
661
|
}
|
|
602
662
|
if (response.code !== 250) throw new Error(`EHLO failed: ${response.message}`);
|
|
603
|
-
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);
|
|
663
|
+
this.capabilities = response.raw.split("\r\n").filter((line) => line.startsWith("250-") || line.startsWith("250 ")).slice(1).map((line) => line.substring(4)).filter((line) => line.length > 0);
|
|
604
664
|
}
|
|
605
665
|
async starttls(signal) {
|
|
606
666
|
if (!this.socket) throw new Error("Not connected");
|
|
@@ -753,12 +813,31 @@ var SmtpConnection = class {
|
|
|
753
813
|
throw new SmtpAuthError(`${mechanism} authentication failed: ${response.message}`);
|
|
754
814
|
}
|
|
755
815
|
async sendMessage(message, signal) {
|
|
756
|
-
const
|
|
816
|
+
const mailCommand = `MAIL FROM:<${message.envelope.from}>`;
|
|
817
|
+
const recipientCommands = message.envelope.to.map((recipient) => `RCPT TO:<${recipient}>`);
|
|
818
|
+
const pipelining = this.capabilities.some((capability) => /^PIPELINING(?:\s|$)/i.test(capability));
|
|
819
|
+
let mailResponse;
|
|
820
|
+
let recipientResponses;
|
|
821
|
+
if (pipelining) try {
|
|
822
|
+
const envelopeResponses = await this.sendCommands([mailCommand, ...recipientCommands], signal);
|
|
823
|
+
mailResponse = envelopeResponses[0];
|
|
824
|
+
recipientResponses = envelopeResponses.slice(1);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
if (!(error instanceof SmtpPipelineTerminatedError)) throw error;
|
|
827
|
+
const { response, responseIndex } = error;
|
|
828
|
+
if (responseIndex === 0) throw new SmtpResponseError(`MAIL FROM failed: ${response.message}`, response.code, "MAIL FROM", response.message);
|
|
829
|
+
const recipient = message.envelope.to[responseIndex - 1];
|
|
830
|
+
throw new SmtpResponseError(`RCPT TO failed for ${recipient}: ${response.message}`, response.code, "RCPT TO", response.message);
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
mailResponse = await this.sendCommand(mailCommand, signal);
|
|
834
|
+
recipientResponses = [];
|
|
835
|
+
}
|
|
757
836
|
if (mailResponse.code !== 250) throw new SmtpResponseError(`MAIL FROM failed: ${mailResponse.message}`, mailResponse.code, "MAIL FROM", mailResponse.message);
|
|
758
837
|
const rejectedRecipients = [];
|
|
759
|
-
for (const recipient of message.envelope.to) {
|
|
838
|
+
for (const [index, recipient] of message.envelope.to.entries()) {
|
|
760
839
|
signal?.throwIfAborted();
|
|
761
|
-
const rcptResponse = await this.sendCommand(
|
|
840
|
+
const rcptResponse = pipelining ? recipientResponses[index] : await this.sendCommand(recipientCommands[index], signal);
|
|
762
841
|
if (rcptResponse.code === 421) throw new SmtpResponseError(`RCPT TO failed for ${recipient}: ${rcptResponse.message}`, rcptResponse.code, "RCPT TO", rcptResponse.message);
|
|
763
842
|
if (rcptResponse.code !== 250 && rcptResponse.code !== 251) rejectedRecipients.push({
|
|
764
843
|
recipient,
|
package/dist/index.js
CHANGED
|
@@ -338,6 +338,16 @@ var OAuth2TokenManager = class {
|
|
|
338
338
|
|
|
339
339
|
//#endregion
|
|
340
340
|
//#region src/smtp-connection.ts
|
|
341
|
+
var SmtpPipelineTerminatedError = class extends Error {
|
|
342
|
+
responseIndex;
|
|
343
|
+
response;
|
|
344
|
+
constructor(responseIndex, response) {
|
|
345
|
+
super("SMTP pipeline terminated by the server.");
|
|
346
|
+
this.name = "SmtpPipelineTerminatedError";
|
|
347
|
+
this.responseIndex = responseIndex;
|
|
348
|
+
this.response = response;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
341
351
|
/**
|
|
342
352
|
* The maximum length of an SMTP command line, including the terminating CRLF,
|
|
343
353
|
* as specified by RFC 5321 §4.5.3.1.4.
|
|
@@ -490,30 +500,59 @@ var SmtpConnection = class {
|
|
|
490
500
|
});
|
|
491
501
|
}
|
|
492
502
|
sendCommand(command, signal) {
|
|
503
|
+
return this.sendCommands([command], signal).then((responses) => responses[0]);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Sends a group of commands in one write and reads one reply per command.
|
|
507
|
+
*
|
|
508
|
+
* SMTP multiline replies are kept together and complete replies are matched
|
|
509
|
+
* to commands by their position in the returned array, as required for
|
|
510
|
+
* command pipelining by RFC 2920.
|
|
511
|
+
*/
|
|
512
|
+
sendCommands(commands, signal) {
|
|
493
513
|
if (!this.socket) throw new Error("Not connected");
|
|
494
514
|
signal?.throwIfAborted();
|
|
495
515
|
return new Promise((resolve, reject) => {
|
|
496
516
|
let buffer = "";
|
|
497
|
-
|
|
517
|
+
let responseLines = [];
|
|
518
|
+
const responses = [];
|
|
519
|
+
const startTimeout = () => setTimeout(() => {
|
|
520
|
+
cleanup();
|
|
498
521
|
reject(/* @__PURE__ */ new Error("Command timeout"));
|
|
499
522
|
}, this.config.socketTimeout);
|
|
523
|
+
let timeout = startTimeout();
|
|
524
|
+
const resetTimeout = () => {
|
|
525
|
+
clearTimeout(timeout);
|
|
526
|
+
timeout = startTimeout();
|
|
527
|
+
};
|
|
500
528
|
const onData = (data) => {
|
|
501
529
|
buffer += data.toString();
|
|
502
530
|
const lines = buffer.split("\r\n");
|
|
503
531
|
const incompleteLine = lines.pop() || "";
|
|
504
|
-
for (
|
|
505
|
-
|
|
532
|
+
for (const line of lines) {
|
|
533
|
+
responseLines.push(line);
|
|
506
534
|
if (line.length >= 4 && line[3] === " ") {
|
|
507
535
|
const code = parseInt(line.substring(0, 3), 10);
|
|
508
536
|
const message = line.substring(4);
|
|
509
|
-
const
|
|
510
|
-
cleanup();
|
|
511
|
-
resolve({
|
|
537
|
+
const response = {
|
|
512
538
|
code,
|
|
513
539
|
message,
|
|
514
|
-
raw:
|
|
515
|
-
}
|
|
516
|
-
|
|
540
|
+
raw: responseLines.join("\r\n")
|
|
541
|
+
};
|
|
542
|
+
const responseIndex = responses.length;
|
|
543
|
+
responses.push(response);
|
|
544
|
+
responseLines = [];
|
|
545
|
+
if (responses.length === commands.length) {
|
|
546
|
+
cleanup();
|
|
547
|
+
resolve(responses);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (response.code === 421) {
|
|
551
|
+
cleanup();
|
|
552
|
+
reject(new SmtpPipelineTerminatedError(responseIndex, response));
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
resetTimeout();
|
|
517
556
|
}
|
|
518
557
|
}
|
|
519
558
|
buffer = incompleteLine;
|
|
@@ -522,14 +561,35 @@ var SmtpConnection = class {
|
|
|
522
561
|
cleanup();
|
|
523
562
|
reject(error);
|
|
524
563
|
};
|
|
564
|
+
const onClose = () => {
|
|
565
|
+
cleanup();
|
|
566
|
+
const responseIndex = responses.findIndex((response) => response.code >= 400);
|
|
567
|
+
if (responseIndex >= 0) {
|
|
568
|
+
reject(new SmtpPipelineTerminatedError(responseIndex, responses[responseIndex]));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
reject(/* @__PURE__ */ new Error("Connection closed before all command responses."));
|
|
572
|
+
};
|
|
573
|
+
const onAbort = () => {
|
|
574
|
+
cleanup();
|
|
575
|
+
try {
|
|
576
|
+
signal?.throwIfAborted();
|
|
577
|
+
} catch (error) {
|
|
578
|
+
reject(error);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
525
581
|
const cleanup = () => {
|
|
526
582
|
clearTimeout(timeout);
|
|
527
583
|
this.socket?.off("data", onData);
|
|
528
584
|
this.socket?.off("error", onError);
|
|
585
|
+
this.socket?.off("close", onClose);
|
|
586
|
+
signal?.removeEventListener("abort", onAbort);
|
|
529
587
|
};
|
|
530
588
|
this.socket.on("data", onData);
|
|
531
589
|
this.socket.on("error", onError);
|
|
532
|
-
this.socket.
|
|
590
|
+
this.socket.on("close", onClose);
|
|
591
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
592
|
+
this.socket.write(commands.map((command) => `${command}\r\n`).join(""));
|
|
533
593
|
});
|
|
534
594
|
}
|
|
535
595
|
greeting(signal) {
|
|
@@ -577,7 +637,7 @@ var SmtpConnection = class {
|
|
|
577
637
|
return;
|
|
578
638
|
}
|
|
579
639
|
if (response.code !== 250) throw new Error(`EHLO failed: ${response.message}`);
|
|
580
|
-
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);
|
|
640
|
+
this.capabilities = response.raw.split("\r\n").filter((line) => line.startsWith("250-") || line.startsWith("250 ")).slice(1).map((line) => line.substring(4)).filter((line) => line.length > 0);
|
|
581
641
|
}
|
|
582
642
|
async starttls(signal) {
|
|
583
643
|
if (!this.socket) throw new Error("Not connected");
|
|
@@ -730,12 +790,31 @@ var SmtpConnection = class {
|
|
|
730
790
|
throw new SmtpAuthError(`${mechanism} authentication failed: ${response.message}`);
|
|
731
791
|
}
|
|
732
792
|
async sendMessage(message, signal) {
|
|
733
|
-
const
|
|
793
|
+
const mailCommand = `MAIL FROM:<${message.envelope.from}>`;
|
|
794
|
+
const recipientCommands = message.envelope.to.map((recipient) => `RCPT TO:<${recipient}>`);
|
|
795
|
+
const pipelining = this.capabilities.some((capability) => /^PIPELINING(?:\s|$)/i.test(capability));
|
|
796
|
+
let mailResponse;
|
|
797
|
+
let recipientResponses;
|
|
798
|
+
if (pipelining) try {
|
|
799
|
+
const envelopeResponses = await this.sendCommands([mailCommand, ...recipientCommands], signal);
|
|
800
|
+
mailResponse = envelopeResponses[0];
|
|
801
|
+
recipientResponses = envelopeResponses.slice(1);
|
|
802
|
+
} catch (error) {
|
|
803
|
+
if (!(error instanceof SmtpPipelineTerminatedError)) throw error;
|
|
804
|
+
const { response, responseIndex } = error;
|
|
805
|
+
if (responseIndex === 0) throw new SmtpResponseError(`MAIL FROM failed: ${response.message}`, response.code, "MAIL FROM", response.message);
|
|
806
|
+
const recipient = message.envelope.to[responseIndex - 1];
|
|
807
|
+
throw new SmtpResponseError(`RCPT TO failed for ${recipient}: ${response.message}`, response.code, "RCPT TO", response.message);
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
mailResponse = await this.sendCommand(mailCommand, signal);
|
|
811
|
+
recipientResponses = [];
|
|
812
|
+
}
|
|
734
813
|
if (mailResponse.code !== 250) throw new SmtpResponseError(`MAIL FROM failed: ${mailResponse.message}`, mailResponse.code, "MAIL FROM", mailResponse.message);
|
|
735
814
|
const rejectedRecipients = [];
|
|
736
|
-
for (const recipient of message.envelope.to) {
|
|
815
|
+
for (const [index, recipient] of message.envelope.to.entries()) {
|
|
737
816
|
signal?.throwIfAborted();
|
|
738
|
-
const rcptResponse = await this.sendCommand(
|
|
817
|
+
const rcptResponse = pipelining ? recipientResponses[index] : await this.sendCommand(recipientCommands[index], signal);
|
|
739
818
|
if (rcptResponse.code === 421) throw new SmtpResponseError(`RCPT TO failed for ${recipient}: ${rcptResponse.message}`, rcptResponse.code, "RCPT TO", rcptResponse.message);
|
|
740
819
|
if (rcptResponse.code !== 250 && rcptResponse.code !== 251) rejectedRecipients.push({
|
|
741
820
|
recipient,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upyo/smtp",
|
|
3
|
-
"version": "0.6.0-dev.
|
|
3
|
+
"version": "0.6.0-dev.272",
|
|
4
4
|
"description": "SMTP transport for Upyo email library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"sideEffects": false,
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@upyo/core": "0.6.0-dev.
|
|
56
|
+
"@upyo/core": "0.6.0-dev.272+71461772"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"tsdown": "^0.12.7",
|