@upyo/smtp 0.6.0-dev.311 → 0.6.0-dev.314
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 +789 -334
- package/dist/index.d.cts +27 -4
- package/dist/index.d.ts +27 -4
- package/dist/index.js +790 -336
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -25,6 +25,7 @@ const __upyo_core = __toESM(require("@upyo/core"));
|
|
|
25
25
|
const node_buffer = __toESM(require("node:buffer"));
|
|
26
26
|
const node_net = __toESM(require("node:net"));
|
|
27
27
|
const node_tls = __toESM(require("node:tls"));
|
|
28
|
+
const node_crypto = __toESM(require("node:crypto"));
|
|
28
29
|
|
|
29
30
|
//#region src/delivery-status.ts
|
|
30
31
|
/**
|
|
@@ -134,6 +135,40 @@ function assertParameterLength(parameter, maximum, name) {
|
|
|
134
135
|
if (parameter.length > maximum) throw new SmtpDsnValidationError(`${name} parameter exceeds the RFC 3461 limit of ${maximum} characters.`);
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/dkim/types.ts
|
|
140
|
+
/**
|
|
141
|
+
* Validates the runtime body-mode option without changing config identity.
|
|
142
|
+
* @param config Optional signing configuration.
|
|
143
|
+
* @throws {TypeError} If the body mode is unsupported.
|
|
144
|
+
*/
|
|
145
|
+
function validateDkimBodyMode(config) {
|
|
146
|
+
if (config?.bodyMode !== void 0 && config.bodyMode !== "buffered" && config.bodyMode !== "streaming") throw new TypeError("Expected DKIM bodyMode to be buffered or streaming.");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Default header fields to sign if not specified.
|
|
150
|
+
*
|
|
151
|
+
* @since 0.4.0
|
|
152
|
+
*/
|
|
153
|
+
const DEFAULT_SIGNED_HEADERS = [
|
|
154
|
+
"from",
|
|
155
|
+
"to",
|
|
156
|
+
"subject",
|
|
157
|
+
"date"
|
|
158
|
+
];
|
|
159
|
+
/**
|
|
160
|
+
* Default DKIM algorithm.
|
|
161
|
+
*
|
|
162
|
+
* @since 0.4.0
|
|
163
|
+
*/
|
|
164
|
+
const DEFAULT_ALGORITHM = "rsa-sha256";
|
|
165
|
+
/**
|
|
166
|
+
* Default canonicalization method.
|
|
167
|
+
*
|
|
168
|
+
* @since 0.4.0
|
|
169
|
+
*/
|
|
170
|
+
const DEFAULT_CANONICALIZATION = "relaxed/relaxed";
|
|
171
|
+
|
|
137
172
|
//#endregion
|
|
138
173
|
//#region src/config.ts
|
|
139
174
|
/**
|
|
@@ -148,6 +183,7 @@ function assertParameterLength(parameter, maximum, name) {
|
|
|
148
183
|
* @internal
|
|
149
184
|
*/
|
|
150
185
|
function createSmtpConfig(config) {
|
|
186
|
+
validateDkimBodyMode(config.dkim);
|
|
151
187
|
const port = config.port ?? 587;
|
|
152
188
|
return {
|
|
153
189
|
host: config.host,
|
|
@@ -327,7 +363,7 @@ function truncateErrorBody(text) {
|
|
|
327
363
|
* @returns A promise that settles with `promise`, or rejects when `signal`
|
|
328
364
|
* aborts.
|
|
329
365
|
*/
|
|
330
|
-
function abortable(promise, signal) {
|
|
366
|
+
function abortable$1(promise, signal) {
|
|
331
367
|
if (signal == null) return promise;
|
|
332
368
|
return new Promise((resolve, reject) => {
|
|
333
369
|
const abortReason = () => signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
@@ -401,7 +437,7 @@ var OAuth2TokenManager = class {
|
|
|
401
437
|
}).finally(() => {
|
|
402
438
|
this.pending = void 0;
|
|
403
439
|
});
|
|
404
|
-
const { accessToken } = await abortable(this.pending, signal);
|
|
440
|
+
const { accessToken } = await abortable$1(this.pending, signal);
|
|
405
441
|
return accessToken;
|
|
406
442
|
}
|
|
407
443
|
/**
|
|
@@ -469,6 +505,561 @@ var OAuth2TokenManager = class {
|
|
|
469
505
|
}
|
|
470
506
|
};
|
|
471
507
|
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/dkim/body-hash.ts
|
|
510
|
+
/** Incremental RFC 6376 body hash with bounded whitespace and line state. */
|
|
511
|
+
var BodyHasher = class {
|
|
512
|
+
hash = (0, node_crypto.createHash)("sha256");
|
|
513
|
+
buffer = new Uint8Array(65536);
|
|
514
|
+
used = 0;
|
|
515
|
+
cr = false;
|
|
516
|
+
whitespace = false;
|
|
517
|
+
newlines = 0;
|
|
518
|
+
nonempty = false;
|
|
519
|
+
constructor(mode) {
|
|
520
|
+
this.mode = mode;
|
|
521
|
+
}
|
|
522
|
+
update(bytes) {
|
|
523
|
+
const relaxed = this.mode === "relaxed";
|
|
524
|
+
for (let offset = 0; offset < bytes.length;) {
|
|
525
|
+
const byte = bytes[offset++];
|
|
526
|
+
if (this.cr) {
|
|
527
|
+
this.cr = false;
|
|
528
|
+
if (byte === 10) {
|
|
529
|
+
this.whitespace = false;
|
|
530
|
+
this.newlines++;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
this.content(13);
|
|
534
|
+
}
|
|
535
|
+
if (byte === 13) this.cr = true;
|
|
536
|
+
else if (relaxed && (byte === 32 || byte === 9)) this.whitespace = true;
|
|
537
|
+
else {
|
|
538
|
+
this.content(byte);
|
|
539
|
+
const start = offset;
|
|
540
|
+
while (offset < bytes.length && bytes[offset] !== 13 && (!relaxed || bytes[offset] !== 32 && bytes[offset] !== 9)) offset++;
|
|
541
|
+
this.emitSpan(bytes, start, offset);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
emitSpan(bytes, start, end) {
|
|
546
|
+
while (start < end) {
|
|
547
|
+
const length = Math.min(end - start, this.buffer.length - this.used);
|
|
548
|
+
this.buffer.set(bytes.subarray(start, start + length), this.used);
|
|
549
|
+
this.used += length;
|
|
550
|
+
start += length;
|
|
551
|
+
if (this.used === this.buffer.length) {
|
|
552
|
+
this.hash.update(this.buffer);
|
|
553
|
+
this.used = 0;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
content(byte) {
|
|
558
|
+
if (this.mode === "relaxed" && (byte === 32 || byte === 9)) {
|
|
559
|
+
this.whitespace = true;
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
while (this.newlines > 0) {
|
|
563
|
+
this.emit(13);
|
|
564
|
+
this.emit(10);
|
|
565
|
+
this.newlines--;
|
|
566
|
+
}
|
|
567
|
+
if (this.whitespace) {
|
|
568
|
+
this.emit(32);
|
|
569
|
+
this.whitespace = false;
|
|
570
|
+
}
|
|
571
|
+
this.emit(byte);
|
|
572
|
+
this.nonempty = true;
|
|
573
|
+
}
|
|
574
|
+
emit(byte) {
|
|
575
|
+
this.buffer[this.used++] = byte;
|
|
576
|
+
if (this.used === this.buffer.length) {
|
|
577
|
+
this.hash.update(this.buffer);
|
|
578
|
+
this.used = 0;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
digest() {
|
|
582
|
+
if (this.cr) this.content(13);
|
|
583
|
+
if (this.nonempty || this.mode === "simple") {
|
|
584
|
+
this.emit(13);
|
|
585
|
+
this.emit(10);
|
|
586
|
+
}
|
|
587
|
+
this.hash.update(this.buffer.subarray(0, this.used));
|
|
588
|
+
return this.hash.digest("base64");
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/dkim/canonicalize.ts
|
|
594
|
+
/**
|
|
595
|
+
* DKIM Canonicalization algorithms per RFC 6376 Section 3.4.
|
|
596
|
+
*
|
|
597
|
+
* @see https://www.rfc-editor.org/rfc/rfc6376#section-3.4
|
|
598
|
+
* @since 0.4.0
|
|
599
|
+
*/
|
|
600
|
+
/**
|
|
601
|
+
* Simple header canonicalization.
|
|
602
|
+
*
|
|
603
|
+
* The "simple" header canonicalization algorithm does not change header
|
|
604
|
+
* fields in any way. Header fields are presented to the signing or
|
|
605
|
+
* verification algorithm exactly as they are in the message.
|
|
606
|
+
*
|
|
607
|
+
* @param name - The header field name
|
|
608
|
+
* @param value - The header field value
|
|
609
|
+
* @returns The canonicalized header line (name:value)
|
|
610
|
+
* @see RFC 6376 Section 3.4.1
|
|
611
|
+
* @since 0.4.0
|
|
612
|
+
*/
|
|
613
|
+
function canonicalizeHeaderSimple(name, value) {
|
|
614
|
+
return `${name}:${value}`;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Relaxed header canonicalization.
|
|
618
|
+
*
|
|
619
|
+
* The "relaxed" header canonicalization algorithm:
|
|
620
|
+
* - Convert header field names to lowercase
|
|
621
|
+
* - Unfold header field continuation lines
|
|
622
|
+
* - Collapse whitespace sequences to a single space
|
|
623
|
+
* - Remove leading and trailing whitespace from header field values
|
|
624
|
+
*
|
|
625
|
+
* @param name - The header field name
|
|
626
|
+
* @param value - The header field value
|
|
627
|
+
* @returns The canonicalized header line (name:value)
|
|
628
|
+
* @see RFC 6376 Section 3.4.2
|
|
629
|
+
* @since 0.4.0
|
|
630
|
+
*/
|
|
631
|
+
function canonicalizeHeaderRelaxed(name, value) {
|
|
632
|
+
const canonicalName = name.toLowerCase();
|
|
633
|
+
const canonicalValue = value.replace(/\r\n[\t ]+/g, " ").replace(/[\t ]+/g, " ").trim();
|
|
634
|
+
return `${canonicalName}:${canonicalValue}`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
//#endregion
|
|
638
|
+
//#region src/dkim/sign.ts
|
|
639
|
+
/**
|
|
640
|
+
* Signs frozen headers using a body hash computed by the MIME reader.
|
|
641
|
+
* @param rawHeaders Frozen wire headers, including earlier signatures.
|
|
642
|
+
* @param config Signature configuration.
|
|
643
|
+
* @param bodyHash Canonical body SHA-256 digest, encoded as base64.
|
|
644
|
+
* @param signal Optional cancellation signal.
|
|
645
|
+
* @returns A DKIM-Signature header value.
|
|
646
|
+
* @throws {Error} If key import, signing, or cancellation fails.
|
|
647
|
+
*/
|
|
648
|
+
async function signWithBodyHash(rawHeaders, config, bodyHash, signal) {
|
|
649
|
+
signal?.throwIfAborted();
|
|
650
|
+
const algorithm = config.algorithm ?? DEFAULT_ALGORITHM;
|
|
651
|
+
const canonicalization = config.canonicalization ?? DEFAULT_CANONICALIZATION;
|
|
652
|
+
const headerFields = config.headerFields ?? DEFAULT_SIGNED_HEADERS;
|
|
653
|
+
const { headers } = parseMessage(rawHeaders);
|
|
654
|
+
const [headerCanon] = canonicalization.split("/");
|
|
655
|
+
const privateKey = await getPrivateKey(config.privateKey, algorithm);
|
|
656
|
+
signal?.throwIfAborted();
|
|
657
|
+
const dkimHeaderValue = buildDkimHeaderValue({
|
|
658
|
+
algorithm,
|
|
659
|
+
canonicalization,
|
|
660
|
+
signingDomain: config.signingDomain,
|
|
661
|
+
selector: config.selector,
|
|
662
|
+
headerFields,
|
|
663
|
+
bodyHash
|
|
664
|
+
});
|
|
665
|
+
const signatureData = buildSignatureData(headers, headerFields, headerCanon, dkimHeaderValue);
|
|
666
|
+
const signature = await signData(signatureData, privateKey, algorithm);
|
|
667
|
+
signal?.throwIfAborted();
|
|
668
|
+
return {
|
|
669
|
+
headerName: "DKIM-Signature",
|
|
670
|
+
signature: `${dkimHeaderValue} b=${signature}`
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Parses a raw email message into headers and body.
|
|
675
|
+
*/
|
|
676
|
+
function parseMessage(rawMessage) {
|
|
677
|
+
const separatorIndex = rawMessage.indexOf("\r\n\r\n");
|
|
678
|
+
if (separatorIndex === -1) return {
|
|
679
|
+
headers: parseHeaders(rawMessage),
|
|
680
|
+
body: ""
|
|
681
|
+
};
|
|
682
|
+
const headerSection = rawMessage.substring(0, separatorIndex);
|
|
683
|
+
const body = rawMessage.substring(separatorIndex + 4);
|
|
684
|
+
return {
|
|
685
|
+
headers: parseHeaders(headerSection),
|
|
686
|
+
body
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Parses header section into a map of header name to value.
|
|
691
|
+
* Handles folded headers (continuation lines).
|
|
692
|
+
*/
|
|
693
|
+
function parseHeaders(headerSection) {
|
|
694
|
+
const headers = /* @__PURE__ */ new Map();
|
|
695
|
+
const lines = headerSection.split("\r\n");
|
|
696
|
+
let currentName = "";
|
|
697
|
+
let currentValue = "";
|
|
698
|
+
for (const line of lines) if (line.startsWith(" ") || line.startsWith(" ")) currentValue += "\r\n" + line;
|
|
699
|
+
else {
|
|
700
|
+
if (currentName) headers.set(currentName.toLowerCase(), {
|
|
701
|
+
name: currentName,
|
|
702
|
+
value: currentValue
|
|
703
|
+
});
|
|
704
|
+
const colonIndex = line.indexOf(":");
|
|
705
|
+
if (colonIndex > 0) {
|
|
706
|
+
currentName = line.substring(0, colonIndex);
|
|
707
|
+
currentValue = line.substring(colonIndex + 1);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (currentName) headers.set(currentName.toLowerCase(), {
|
|
711
|
+
name: currentName,
|
|
712
|
+
value: currentValue
|
|
713
|
+
});
|
|
714
|
+
return headers;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Gets the private key, either using a provided CryptoKey or importing from PEM.
|
|
718
|
+
*/
|
|
719
|
+
function getPrivateKey(key, algorithm) {
|
|
720
|
+
if (typeof key !== "string") return key;
|
|
721
|
+
return importPrivateKey(key, algorithm);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Imports a PEM-encoded private key for use with Web Crypto API.
|
|
725
|
+
*/
|
|
726
|
+
async function importPrivateKey(pem, algorithm) {
|
|
727
|
+
try {
|
|
728
|
+
const pemContents = pem.replace(/-----BEGIN (?:RSA )?PRIVATE KEY-----/, "").replace(/-----END (?:RSA )?PRIVATE KEY-----/, "").replace(/\s/g, "");
|
|
729
|
+
const binaryString = atob(pemContents);
|
|
730
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
731
|
+
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
|
|
732
|
+
const keyAlgorithm = algorithm === "ed25519-sha256" ? { name: "Ed25519" } : {
|
|
733
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
734
|
+
hash: "SHA-256"
|
|
735
|
+
};
|
|
736
|
+
return await crypto.subtle.importKey("pkcs8", bytes, keyAlgorithm, false, ["sign"]);
|
|
737
|
+
} catch (error) {
|
|
738
|
+
throw new Error(`Failed to import private key: ${error instanceof Error ? error.message : String(error)}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Builds the DKIM-Signature header value without the b= signature.
|
|
743
|
+
*/
|
|
744
|
+
function buildDkimHeaderValue(params) {
|
|
745
|
+
const parts = [
|
|
746
|
+
"v=1",
|
|
747
|
+
`a=${params.algorithm}`,
|
|
748
|
+
`c=${params.canonicalization}`,
|
|
749
|
+
`d=${params.signingDomain}`,
|
|
750
|
+
`s=${params.selector}`,
|
|
751
|
+
`h=${params.headerFields.join(":")}`,
|
|
752
|
+
`bh=${params.bodyHash};`
|
|
753
|
+
];
|
|
754
|
+
return parts.join("; ");
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Builds the data to be signed (canonicalized headers + DKIM-Signature header).
|
|
758
|
+
*/
|
|
759
|
+
function buildSignatureData(headers, headerFields, canonMethod, dkimHeaderValue) {
|
|
760
|
+
const lines = [];
|
|
761
|
+
for (const field of headerFields) {
|
|
762
|
+
const header = headers.get(field.toLowerCase());
|
|
763
|
+
if (header !== void 0) {
|
|
764
|
+
const canonicalized = canonMethod === "relaxed" ? canonicalizeHeaderRelaxed(header.name, header.value) : canonicalizeHeaderSimple(header.name, header.value);
|
|
765
|
+
lines.push(canonicalized);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const dkimHeader = canonMethod === "relaxed" ? canonicalizeHeaderRelaxed("DKIM-Signature", " " + dkimHeaderValue + " b=") : canonicalizeHeaderSimple("DKIM-Signature", " " + dkimHeaderValue + " b=");
|
|
769
|
+
lines.push(dkimHeader);
|
|
770
|
+
return lines.join("\r\n");
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Signs data using the appropriate algorithm.
|
|
774
|
+
*/
|
|
775
|
+
async function signData(data, privateKey, algorithm) {
|
|
776
|
+
const encoder = new TextEncoder();
|
|
777
|
+
const dataBuffer = encoder.encode(data);
|
|
778
|
+
const signAlgorithm = algorithm === "ed25519-sha256" ? "Ed25519" : "RSASSA-PKCS1-v1_5";
|
|
779
|
+
const signingInput = algorithm === "ed25519-sha256" ? await crypto.subtle.digest("SHA-256", dataBuffer) : dataBuffer;
|
|
780
|
+
const signature = await crypto.subtle.sign(signAlgorithm, privateKey, signingInput);
|
|
781
|
+
return arrayBufferToBase64(signature);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Converts an ArrayBuffer to a Base64 string.
|
|
785
|
+
*/
|
|
786
|
+
function arrayBufferToBase64(buffer) {
|
|
787
|
+
const bytes = new Uint8Array(buffer);
|
|
788
|
+
let binary = "";
|
|
789
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
790
|
+
return btoa(binary);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
//#endregion
|
|
794
|
+
//#region src/message-stream.ts
|
|
795
|
+
/**
|
|
796
|
+
* A replayable attachment changed between DKIM body reads.
|
|
797
|
+
* @since 0.6.0
|
|
798
|
+
*/
|
|
799
|
+
var SmtpAttachmentReplayError = class extends TypeError {
|
|
800
|
+
/** Creates a replay validation failure. */
|
|
801
|
+
constructor() {
|
|
802
|
+
super("Attachment content changed between DKIM body reads.");
|
|
803
|
+
this.name = "SmtpAttachmentReplayError";
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
/** Prepares signatures and sizes without consuming unsigned factory sources. */
|
|
807
|
+
async function prepareMessageStream(plan, checkSize, progress, signal) {
|
|
808
|
+
const knownSize = await plan.size(signal, checkSize);
|
|
809
|
+
if (knownSize != null) checkSize(knownSize);
|
|
810
|
+
const signatures = plan.dkim?.signatures ?? [];
|
|
811
|
+
if (signatures.length === 0) return {
|
|
812
|
+
size: knownSize,
|
|
813
|
+
async *read(signal$1, progress$1) {
|
|
814
|
+
yield node_buffer.Buffer.from(plan.headers);
|
|
815
|
+
yield* plan.body(signal$1, progress$1);
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
const buffered = plan.dkim?.bodyMode !== "streaming";
|
|
819
|
+
const chunks = [];
|
|
820
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
821
|
+
for (const sig of signatures) {
|
|
822
|
+
const mode = sig.canonicalization?.endsWith("/simple") ? "simple" : "relaxed";
|
|
823
|
+
if (!hashes.has(mode)) hashes.set(mode, new BodyHasher(mode));
|
|
824
|
+
}
|
|
825
|
+
const rawHash = (0, node_crypto.createHash)("sha256");
|
|
826
|
+
let length = 0;
|
|
827
|
+
const headerLength = node_buffer.Buffer.byteLength(plan.headers);
|
|
828
|
+
for await (const chunk of plan.body(signal, progress)) {
|
|
829
|
+
length += chunk.length;
|
|
830
|
+
checkSize(headerLength + length);
|
|
831
|
+
if (chunk.length > 0) progress();
|
|
832
|
+
if (buffered) chunks.push(chunk.slice());
|
|
833
|
+
rawHash.update(chunk);
|
|
834
|
+
for (const hash of hashes.values()) hash.update(chunk);
|
|
835
|
+
}
|
|
836
|
+
const expectedDigest = rawHash.digest("hex");
|
|
837
|
+
const bodyHashes = new Map(Array.from(hashes, ([mode, hash]) => [mode, hash.digest()]));
|
|
838
|
+
let headers = plan.headers;
|
|
839
|
+
try {
|
|
840
|
+
for (const sig of signatures) {
|
|
841
|
+
const mode = sig.canonicalization?.endsWith("/simple") ? "simple" : "relaxed";
|
|
842
|
+
const result = await signWithBodyHash(headers, sig, bodyHashes.get(mode), signal);
|
|
843
|
+
headers = `${result.headerName}: ${result.signature}\r\n${headers}`;
|
|
844
|
+
}
|
|
845
|
+
} catch (error) {
|
|
846
|
+
signal?.throwIfAborted();
|
|
847
|
+
if (plan.dkim?.onSigningFailure !== "send-unsigned") throw error;
|
|
848
|
+
console.warn("DKIM signing failed, sending unsigned:", error);
|
|
849
|
+
}
|
|
850
|
+
const size = node_buffer.Buffer.byteLength(headers) + length;
|
|
851
|
+
checkSize(size);
|
|
852
|
+
return {
|
|
853
|
+
size,
|
|
854
|
+
async *read(signal$1, progress$1) {
|
|
855
|
+
yield node_buffer.Buffer.from(headers);
|
|
856
|
+
if (buffered) {
|
|
857
|
+
for (const chunk of chunks) {
|
|
858
|
+
signal$1?.throwIfAborted();
|
|
859
|
+
yield chunk;
|
|
860
|
+
}
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const replayHash = (0, node_crypto.createHash)("sha256");
|
|
864
|
+
let replayLength = 0;
|
|
865
|
+
for await (const chunk of plan.body(signal$1, progress$1)) {
|
|
866
|
+
replayLength += chunk.length;
|
|
867
|
+
if (replayLength > length) throw new SmtpAttachmentReplayError();
|
|
868
|
+
replayHash.update(chunk);
|
|
869
|
+
yield chunk;
|
|
870
|
+
}
|
|
871
|
+
if (replayLength !== length || replayHash.digest("hex") !== expectedDigest) throw new SmtpAttachmentReplayError();
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region src/data-stream.ts
|
|
878
|
+
/** Races preparation against source inactivity and remote socket termination. */
|
|
879
|
+
async function prepareOnSocket(socket, timeoutMs, prepare, signal) {
|
|
880
|
+
const controller = new AbortController();
|
|
881
|
+
const combined = (0, __upyo_core.combineSignals)(controller.signal, signal);
|
|
882
|
+
const fail = (error) => controller.abort(error);
|
|
883
|
+
const closed = () => fail(/* @__PURE__ */ new TypeError("SMTP connection closed during message preparation."));
|
|
884
|
+
const data = () => fail(/* @__PURE__ */ new TypeError("Unexpected SMTP reply during message preparation."));
|
|
885
|
+
let timer;
|
|
886
|
+
const progress = () => {
|
|
887
|
+
clearTimeout(timer);
|
|
888
|
+
timer = setTimeout(() => fail(/* @__PURE__ */ new TypeError("SMTP message preparation timeout.")), timeoutMs);
|
|
889
|
+
};
|
|
890
|
+
socket.on("error", fail);
|
|
891
|
+
socket.on("close", closed);
|
|
892
|
+
socket.on("data", data);
|
|
893
|
+
progress();
|
|
894
|
+
try {
|
|
895
|
+
if (socket.destroyed || !socket.writable) closed();
|
|
896
|
+
combined.signal.throwIfAborted();
|
|
897
|
+
return await abortable(prepare(combined.signal, progress), combined.signal);
|
|
898
|
+
} catch (error) {
|
|
899
|
+
const interrupted = combined.signal.aborted;
|
|
900
|
+
controller.abort(error);
|
|
901
|
+
if (interrupted) socket.destroy();
|
|
902
|
+
signal?.throwIfAborted();
|
|
903
|
+
throw error;
|
|
904
|
+
} finally {
|
|
905
|
+
clearTimeout(timer);
|
|
906
|
+
socket.off("error", fail);
|
|
907
|
+
socket.off("close", closed);
|
|
908
|
+
socket.off("data", data);
|
|
909
|
+
combined.cleanup();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function abortable(promise, signal) {
|
|
913
|
+
return new Promise((resolve, reject) => {
|
|
914
|
+
const abort = () => reject(signal.reason);
|
|
915
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
916
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
917
|
+
if (signal.aborted) abort();
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
/** Writes one DATA body with bounded transparency buffers and backpressure. */
|
|
921
|
+
async function writeMessageData(socket, source, timeoutMs, checkSize, signal) {
|
|
922
|
+
const controller = new AbortController();
|
|
923
|
+
const combined = (0, __upyo_core.combineSignals)(controller.signal, signal);
|
|
924
|
+
const owned = combined.signal;
|
|
925
|
+
let terminated = false;
|
|
926
|
+
let complete = false;
|
|
927
|
+
let replyBytes = 0;
|
|
928
|
+
let buffer = "";
|
|
929
|
+
const lines = [];
|
|
930
|
+
let resolveReply;
|
|
931
|
+
const reply = new Promise((resolve) => resolveReply = resolve);
|
|
932
|
+
const fail = (error) => {
|
|
933
|
+
if (!owned.aborted) controller.abort(error);
|
|
934
|
+
socket.destroy();
|
|
935
|
+
};
|
|
936
|
+
const close = () => fail(/* @__PURE__ */ new TypeError("SMTP connection closed during DATA."));
|
|
937
|
+
const onAbort = () => socket.destroy();
|
|
938
|
+
let timer;
|
|
939
|
+
const progress = () => {
|
|
940
|
+
clearTimeout(timer);
|
|
941
|
+
timer = setTimeout(() => fail(/* @__PURE__ */ new TypeError("SMTP DATA timeout.")), timeoutMs);
|
|
942
|
+
};
|
|
943
|
+
const data = (chunk) => {
|
|
944
|
+
replyBytes += chunk.length;
|
|
945
|
+
if (replyBytes > 65536) {
|
|
946
|
+
fail(/* @__PURE__ */ new RangeError("SMTP DATA reply exceeds 64 KiB."));
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
buffer += node_buffer.Buffer.from(chunk).toString("utf8");
|
|
950
|
+
let end;
|
|
951
|
+
while ((end = buffer.indexOf("\r\n")) >= 0) {
|
|
952
|
+
const line = buffer.slice(0, end);
|
|
953
|
+
buffer = buffer.slice(end + 2);
|
|
954
|
+
lines.push(line);
|
|
955
|
+
if (/^\d{3} /.test(line)) {
|
|
956
|
+
if (!terminated) fail(/* @__PURE__ */ new TypeError(`Premature SMTP DATA reply: ${line}`));
|
|
957
|
+
else resolveReply({
|
|
958
|
+
code: Number(line.slice(0, 3)),
|
|
959
|
+
message: line.slice(4),
|
|
960
|
+
raw: lines.join("\r\n")
|
|
961
|
+
});
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
socket.on("error", fail);
|
|
967
|
+
socket.on("close", close);
|
|
968
|
+
socket.on("data", data);
|
|
969
|
+
owned.addEventListener("abort", onAbort, { once: true });
|
|
970
|
+
progress();
|
|
971
|
+
let iterator;
|
|
972
|
+
async function write(bytes) {
|
|
973
|
+
owned.throwIfAborted();
|
|
974
|
+
await abortable(new Promise((resolve, reject) => {
|
|
975
|
+
let written = false;
|
|
976
|
+
let drained = false;
|
|
977
|
+
let returned = false;
|
|
978
|
+
const cleanup = () => {
|
|
979
|
+
socket.off("drain", drain);
|
|
980
|
+
owned.removeEventListener("abort", cleanup);
|
|
981
|
+
};
|
|
982
|
+
const finish = () => {
|
|
983
|
+
if (returned && written && drained) {
|
|
984
|
+
cleanup();
|
|
985
|
+
progress();
|
|
986
|
+
resolve();
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
const drain = () => {
|
|
990
|
+
drained = true;
|
|
991
|
+
finish();
|
|
992
|
+
};
|
|
993
|
+
socket.once("drain", drain);
|
|
994
|
+
owned.addEventListener("abort", cleanup, { once: true });
|
|
995
|
+
try {
|
|
996
|
+
const accepted = socket.write(bytes, (error) => {
|
|
997
|
+
if (error != null) {
|
|
998
|
+
cleanup();
|
|
999
|
+
reject(error);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
written = true;
|
|
1003
|
+
finish();
|
|
1004
|
+
});
|
|
1005
|
+
drained = accepted || drained;
|
|
1006
|
+
returned = true;
|
|
1007
|
+
finish();
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
cleanup();
|
|
1010
|
+
reject(error);
|
|
1011
|
+
}
|
|
1012
|
+
}), owned);
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
iterator = source(owned, progress)[Symbol.asyncIterator]();
|
|
1016
|
+
if (socket.destroyed || !socket.writable) close();
|
|
1017
|
+
let lineStart = true;
|
|
1018
|
+
let size = 0;
|
|
1019
|
+
while (true) {
|
|
1020
|
+
owned.throwIfAborted();
|
|
1021
|
+
const item = await abortable(Promise.resolve(iterator.next()), owned);
|
|
1022
|
+
if (item.done) break;
|
|
1023
|
+
const chunk = item.value;
|
|
1024
|
+
for (let offset = 0; offset < chunk.length; offset += 32768) {
|
|
1025
|
+
const window = chunk.subarray(offset, offset + 32768);
|
|
1026
|
+
size += window.length;
|
|
1027
|
+
checkSize(size);
|
|
1028
|
+
progress();
|
|
1029
|
+
const output = node_buffer.Buffer.allocUnsafe(window.length * 2);
|
|
1030
|
+
let length = 0;
|
|
1031
|
+
for (const byte of window) {
|
|
1032
|
+
if (lineStart && byte === 46) output[length++] = 46;
|
|
1033
|
+
output[length++] = byte;
|
|
1034
|
+
lineStart = byte === 10;
|
|
1035
|
+
}
|
|
1036
|
+
await write(output.subarray(0, length));
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
owned.throwIfAborted();
|
|
1040
|
+
terminated = true;
|
|
1041
|
+
await write(node_buffer.Buffer.from(".\r\n"));
|
|
1042
|
+
const result = await abortable(reply, owned);
|
|
1043
|
+
owned.throwIfAborted();
|
|
1044
|
+
complete = true;
|
|
1045
|
+
return result;
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
fail(error);
|
|
1048
|
+
signal?.throwIfAborted();
|
|
1049
|
+
throw owned.reason;
|
|
1050
|
+
} finally {
|
|
1051
|
+
if (!complete) try {
|
|
1052
|
+
Promise.resolve(iterator?.return?.()).catch(() => {});
|
|
1053
|
+
} catch {}
|
|
1054
|
+
clearTimeout(timer);
|
|
1055
|
+
socket.off("error", fail);
|
|
1056
|
+
socket.off("close", close);
|
|
1057
|
+
socket.off("data", data);
|
|
1058
|
+
owned.removeEventListener("abort", onAbort);
|
|
1059
|
+
combined.cleanup();
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
472
1063
|
//#endregion
|
|
473
1064
|
//#region src/smtp-status-code.ts
|
|
474
1065
|
/**
|
|
@@ -503,13 +1094,13 @@ function parseEnhancedSmtpStatusCode(replyCode, response) {
|
|
|
503
1094
|
* Error thrown when a message exceeds the fixed limit advertised through the
|
|
504
1095
|
* SMTP SIZE extension.
|
|
505
1096
|
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
1097
|
+
* Known sizes are checked before `MAIL FROM`. Unknown source sizes are also
|
|
1098
|
+
* checked while writing DATA; those failures make the connection unusable.
|
|
508
1099
|
*
|
|
509
1100
|
* @since 0.6.0
|
|
510
1101
|
*/
|
|
511
1102
|
var SmtpMessageSizeError = class extends RangeError {
|
|
512
|
-
/**
|
|
1103
|
+
/** Exact known size, or the octets counted when an unknown source is stopped. */
|
|
513
1104
|
actualSize;
|
|
514
1105
|
/** The fixed maximum advertised by the SMTP server. */
|
|
515
1106
|
maximumSize;
|
|
@@ -518,9 +1109,11 @@ var SmtpMessageSizeError = class extends RangeError {
|
|
|
518
1109
|
*
|
|
519
1110
|
* @param actualSize The encoded message size in octets.
|
|
520
1111
|
* @param maximumSize The fixed maximum advertised by the SMTP server.
|
|
1112
|
+
* @param phase Whether the failure occurred before or during DATA.
|
|
521
1113
|
*/
|
|
522
|
-
constructor(actualSize, maximumSize) {
|
|
1114
|
+
constructor(actualSize, maximumSize, phase = "preflight") {
|
|
523
1115
|
super(`Message size ${actualSize} octets exceeds the server's maximum of ${maximumSize} octets.`);
|
|
1116
|
+
this.phase = phase;
|
|
524
1117
|
this.name = "SmtpMessageSizeError";
|
|
525
1118
|
this.actualSize = actualSize;
|
|
526
1119
|
this.maximumSize = maximumSize;
|
|
@@ -715,6 +1308,16 @@ var SmtpConnection = class {
|
|
|
715
1308
|
authenticated = false;
|
|
716
1309
|
capabilities = [];
|
|
717
1310
|
tokenManager;
|
|
1311
|
+
active = false;
|
|
1312
|
+
get usable() {
|
|
1313
|
+
return this.socket != null && !this.socket.destroyed && this.socket.writable;
|
|
1314
|
+
}
|
|
1315
|
+
observeSocket(socket) {
|
|
1316
|
+
socket.on("error", () => socket.destroy());
|
|
1317
|
+
socket.on("timeout", () => {
|
|
1318
|
+
if (!this.active) socket.destroy();
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
718
1321
|
constructor(config, tokenManager) {
|
|
719
1322
|
this.config = createSmtpConfig(config);
|
|
720
1323
|
this.tokenManager = tokenManager ?? null;
|
|
@@ -724,17 +1327,31 @@ var SmtpConnection = class {
|
|
|
724
1327
|
signal?.throwIfAborted();
|
|
725
1328
|
return new Promise((resolve, reject) => {
|
|
726
1329
|
const timeout = setTimeout(() => {
|
|
1330
|
+
onError(/* @__PURE__ */ new TypeError("Connection timeout."));
|
|
727
1331
|
this.socket?.destroy();
|
|
728
|
-
reject(/* @__PURE__ */ new Error("Connection timeout"));
|
|
729
1332
|
}, this.config.connectionTimeout);
|
|
730
|
-
const
|
|
1333
|
+
const cleanup = () => {
|
|
731
1334
|
clearTimeout(timeout);
|
|
1335
|
+
this.socket?.off("connect", onConnect);
|
|
1336
|
+
this.socket?.off("error", onError);
|
|
1337
|
+
this.socket?.off("close", onClose);
|
|
1338
|
+
this.socket?.off("timeout", onTimeout);
|
|
1339
|
+
};
|
|
1340
|
+
const onConnect = () => {
|
|
1341
|
+
cleanup();
|
|
732
1342
|
resolve();
|
|
733
1343
|
};
|
|
734
1344
|
const onError = (error) => {
|
|
735
|
-
|
|
1345
|
+
cleanup();
|
|
736
1346
|
reject(error);
|
|
737
1347
|
};
|
|
1348
|
+
const onClose = () => {
|
|
1349
|
+
onError(/* @__PURE__ */ new TypeError("SMTP connection closed before establishment."));
|
|
1350
|
+
};
|
|
1351
|
+
const onTimeout = () => {
|
|
1352
|
+
onError(/* @__PURE__ */ new TypeError("Socket timeout."));
|
|
1353
|
+
this.socket?.destroy();
|
|
1354
|
+
};
|
|
738
1355
|
if (this.config.secure) this.socket = (0, node_tls.connect)({
|
|
739
1356
|
host: this.config.host,
|
|
740
1357
|
port: this.config.port,
|
|
@@ -750,13 +1367,11 @@ var SmtpConnection = class {
|
|
|
750
1367
|
this.socket.connect(this.config.port, this.config.host);
|
|
751
1368
|
}
|
|
752
1369
|
this.socket.setTimeout(this.config.socketTimeout);
|
|
1370
|
+
this.observeSocket(this.socket);
|
|
753
1371
|
this.socket.once("connect", onConnect);
|
|
754
1372
|
this.socket.once("error", onError);
|
|
755
|
-
this.socket.once("
|
|
756
|
-
|
|
757
|
-
this.socket?.destroy();
|
|
758
|
-
reject(/* @__PURE__ */ new Error("Socket timeout"));
|
|
759
|
-
});
|
|
1373
|
+
this.socket.once("close", onClose);
|
|
1374
|
+
this.socket.once("timeout", onTimeout);
|
|
760
1375
|
});
|
|
761
1376
|
}
|
|
762
1377
|
sendCommand(command, signal) {
|
|
@@ -789,7 +1404,7 @@ var SmtpConnection = class {
|
|
|
789
1404
|
buffer += data.toString();
|
|
790
1405
|
const lines = buffer.split("\r\n");
|
|
791
1406
|
const incompleteLine = lines.pop() || "";
|
|
792
|
-
for (const line of lines) {
|
|
1407
|
+
for (const [lineIndex, line] of lines.entries()) {
|
|
793
1408
|
responseLines.push(line);
|
|
794
1409
|
if (line.length >= 4 && line[3] === " ") {
|
|
795
1410
|
const code = parseInt(line.substring(0, 3), 10);
|
|
@@ -804,6 +1419,11 @@ var SmtpConnection = class {
|
|
|
804
1419
|
responseLines = [];
|
|
805
1420
|
if (responses.length === commands.length) {
|
|
806
1421
|
cleanup();
|
|
1422
|
+
if (commands[0] === "DATA" && (lineIndex + 1 < lines.length || incompleteLine.length > 0)) {
|
|
1423
|
+
this.socket?.destroy();
|
|
1424
|
+
reject(/* @__PURE__ */ new TypeError("Premature SMTP reply after DATA readiness."));
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
807
1427
|
resolve(responses);
|
|
808
1428
|
return;
|
|
809
1429
|
}
|
|
@@ -914,6 +1534,7 @@ var SmtpConnection = class {
|
|
|
914
1534
|
reject(/* @__PURE__ */ new Error("STARTTLS upgrade timeout"));
|
|
915
1535
|
}, this.config.connectionTimeout);
|
|
916
1536
|
const plainSocket = this.socket;
|
|
1537
|
+
plainSocket.setTimeout(0);
|
|
917
1538
|
const tlsSocket = (0, node_tls.connect)({
|
|
918
1539
|
socket: plainSocket,
|
|
919
1540
|
host: this.config.host,
|
|
@@ -926,7 +1547,9 @@ var SmtpConnection = class {
|
|
|
926
1547
|
});
|
|
927
1548
|
const onSecureConnect = () => {
|
|
928
1549
|
clearTimeout(timeout);
|
|
1550
|
+
tlsSocket.off("error", onError);
|
|
929
1551
|
this.socket = tlsSocket;
|
|
1552
|
+
this.observeSocket(tlsSocket);
|
|
930
1553
|
this.socket.setTimeout(this.config.socketTimeout);
|
|
931
1554
|
resolve();
|
|
932
1555
|
};
|
|
@@ -937,11 +1560,6 @@ var SmtpConnection = class {
|
|
|
937
1560
|
};
|
|
938
1561
|
tlsSocket.once("secureConnect", onSecureConnect);
|
|
939
1562
|
tlsSocket.once("error", onError);
|
|
940
|
-
tlsSocket.once("timeout", () => {
|
|
941
|
-
clearTimeout(timeout);
|
|
942
|
-
tlsSocket.destroy();
|
|
943
|
-
reject(/* @__PURE__ */ new Error("TLS upgrade timeout"));
|
|
944
|
-
});
|
|
945
1563
|
});
|
|
946
1564
|
}
|
|
947
1565
|
async authenticate(signal) {
|
|
@@ -1055,6 +1673,16 @@ var SmtpConnection = class {
|
|
|
1055
1673
|
throw new SmtpAuthResponseError(`${mechanism} authentication failed: ${response.message}`, response.code, `AUTH ${mechanism}`, response.message);
|
|
1056
1674
|
}
|
|
1057
1675
|
async sendMessage(message, signal) {
|
|
1676
|
+
this.active = true;
|
|
1677
|
+
this.socket?.setTimeout(0);
|
|
1678
|
+
try {
|
|
1679
|
+
return await this.sendPreparedMessage(message, signal);
|
|
1680
|
+
} finally {
|
|
1681
|
+
this.active = false;
|
|
1682
|
+
if (this.usable) this.socket?.setTimeout(this.config.socketTimeout);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
async sendPreparedMessage(message, signal) {
|
|
1058
1686
|
signal?.throwIfAborted();
|
|
1059
1687
|
let smtpUtf8Parameters = "";
|
|
1060
1688
|
if (message.requiresSmtpUtf8 === true) {
|
|
@@ -1064,14 +1692,25 @@ var SmtpConnection = class {
|
|
|
1064
1692
|
}
|
|
1065
1693
|
const sizeCapability = parseSizeCapability(this.capabilities);
|
|
1066
1694
|
let sizeParameter = "";
|
|
1067
|
-
if (sizeCapability != null) {
|
|
1068
|
-
const messageSize = node_buffer.Buffer.byteLength(message.raw, "utf8") + CRLF_LENGTH;
|
|
1069
|
-
if (sizeCapability.maximum != null && BigInt(messageSize) > sizeCapability.maximum) throw new SmtpMessageSizeError(messageSize, sizeCapability.maximum);
|
|
1070
|
-
sizeParameter = ` SIZE=${messageSize}`;
|
|
1071
|
-
}
|
|
1072
1695
|
const dsn = message.envelope.dsn;
|
|
1073
1696
|
if (dsn != null && !this.capabilities.some((capability) => /^DSN[ \t]*$/i.test(capability))) throw new SmtpDsnUnsupportedError();
|
|
1074
1697
|
const mailDsnParameters = dsn == null || dsn.mailParameters.length === 0 ? "" : ` ${dsn.mailParameters.join(" ")}`;
|
|
1698
|
+
const checkSize = (size, phase = "preflight") => {
|
|
1699
|
+
if (!Number.isSafeInteger(size)) throw new RangeError("Message size exceeds the safe integer range.");
|
|
1700
|
+
if (sizeCapability?.maximum != null && BigInt(size) > sizeCapability.maximum) throw new SmtpMessageSizeError(size, sizeCapability.maximum, phase);
|
|
1701
|
+
};
|
|
1702
|
+
if (!this.socket || !this.usable) throw new TypeError("SMTP connection is closed.");
|
|
1703
|
+
const stream = "raw" in message ? {
|
|
1704
|
+
size: node_buffer.Buffer.byteLength(message.raw) + 2,
|
|
1705
|
+
async *read(signal$1) {
|
|
1706
|
+
signal$1?.throwIfAborted();
|
|
1707
|
+
yield node_buffer.Buffer.from(message.raw + "\r\n");
|
|
1708
|
+
}
|
|
1709
|
+
} : await prepareOnSocket(this.socket, this.config.socketTimeout, (signal$1, progress) => prepareMessageStream(message, checkSize, progress, signal$1), signal);
|
|
1710
|
+
if (stream.size != null) {
|
|
1711
|
+
checkSize(stream.size);
|
|
1712
|
+
if (sizeCapability != null) sizeParameter = ` SIZE=${stream.size}`;
|
|
1713
|
+
}
|
|
1075
1714
|
const mailCommand = `MAIL FROM:<${message.envelope.from ?? ""}>${sizeParameter}${smtpUtf8Parameters}${mailDsnParameters}`;
|
|
1076
1715
|
const recipientCommands = message.envelope.to.map((recipient, index) => {
|
|
1077
1716
|
const parameters = dsn?.recipientParameters[index] ?? [];
|
|
@@ -1120,8 +1759,7 @@ var SmtpConnection = class {
|
|
|
1120
1759
|
}
|
|
1121
1760
|
const dataResponse = await this.sendCommand("DATA", signal);
|
|
1122
1761
|
if (dataResponse.code !== 354) throw new SmtpResponseError(`DATA failed: ${dataResponse.message}`, dataResponse.code, "DATA", dataResponse.message);
|
|
1123
|
-
const
|
|
1124
|
-
const finalResponse = await this.sendCommand(`${content}\r\n.`, signal);
|
|
1762
|
+
const finalResponse = await writeMessageData(this.socket, (signal$1, progress) => stream.read(signal$1, progress), this.config.socketTimeout, (size) => checkSize(size, "data"), signal);
|
|
1125
1763
|
if (finalResponse.code !== 250) throw new SmtpResponseError(`Message send failed: ${finalResponse.message}`, finalResponse.code, "DATA_END", finalResponse.message);
|
|
1126
1764
|
const messageId = this.extractMessageId(finalResponse.message);
|
|
1127
1765
|
return {
|
|
@@ -1271,296 +1909,71 @@ function resolveSmtpEnvelope(message, override) {
|
|
|
1271
1909
|
}
|
|
1272
1910
|
|
|
1273
1911
|
//#endregion
|
|
1274
|
-
//#region src/
|
|
1275
|
-
/**
|
|
1276
|
-
*
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
* @since 0.4.0
|
|
1293
|
-
*/
|
|
1294
|
-
function canonicalizeHeaderSimple(name, value) {
|
|
1295
|
-
return `${name}:${value}`;
|
|
1296
|
-
}
|
|
1297
|
-
/**
|
|
1298
|
-
* Relaxed header canonicalization.
|
|
1299
|
-
*
|
|
1300
|
-
* The "relaxed" header canonicalization algorithm:
|
|
1301
|
-
* - Convert header field names to lowercase
|
|
1302
|
-
* - Unfold header field continuation lines
|
|
1303
|
-
* - Collapse whitespace sequences to a single space
|
|
1304
|
-
* - Remove leading and trailing whitespace from header field values
|
|
1305
|
-
*
|
|
1306
|
-
* @param name - The header field name
|
|
1307
|
-
* @param value - The header field value
|
|
1308
|
-
* @returns The canonicalized header line (name:value)
|
|
1309
|
-
* @see RFC 6376 Section 3.4.2
|
|
1310
|
-
* @since 0.4.0
|
|
1311
|
-
*/
|
|
1312
|
-
function canonicalizeHeaderRelaxed(name, value) {
|
|
1313
|
-
const canonicalName = name.toLowerCase();
|
|
1314
|
-
const canonicalValue = value.replace(/\r\n[\t ]+/g, " ").replace(/[\t ]+/g, " ").trim();
|
|
1315
|
-
return `${canonicalName}:${canonicalValue}`;
|
|
1316
|
-
}
|
|
1317
|
-
/**
|
|
1318
|
-
* Simple body canonicalization.
|
|
1319
|
-
*
|
|
1320
|
-
* The "simple" body canonicalization algorithm:
|
|
1321
|
-
* - Ignores all empty lines at the end of the message body
|
|
1322
|
-
* - If the body is empty, a single CRLF is appended
|
|
1323
|
-
* - If there is no trailing CRLF on the body, a CRLF is added
|
|
1324
|
-
*
|
|
1325
|
-
* @param body - The message body
|
|
1326
|
-
* @returns The canonicalized body
|
|
1327
|
-
* @see RFC 6376 Section 3.4.3
|
|
1328
|
-
* @since 0.4.0
|
|
1329
|
-
*/
|
|
1330
|
-
function canonicalizeBodySimple(body) {
|
|
1331
|
-
if (body === "") return "\r\n";
|
|
1332
|
-
let result = body.replace(/(\r\n)+$/, "");
|
|
1333
|
-
if (!result.endsWith("\r\n")) result += "\r\n";
|
|
1334
|
-
return result;
|
|
1335
|
-
}
|
|
1336
|
-
/**
|
|
1337
|
-
* Relaxed body canonicalization.
|
|
1338
|
-
*
|
|
1339
|
-
* The "relaxed" body canonicalization algorithm:
|
|
1340
|
-
* - Reduce all sequences of WSP within a line to a single SP
|
|
1341
|
-
* - Remove all trailing WSP at the end of each line (before CRLF)
|
|
1342
|
-
* - Ignore all empty lines at the end of the message body
|
|
1343
|
-
* - If the body is non-empty and doesn't end with CRLF, add CRLF
|
|
1344
|
-
*
|
|
1345
|
-
* @param body - The message body
|
|
1346
|
-
* @returns The canonicalized body
|
|
1347
|
-
* @see RFC 6376 Section 3.4.4
|
|
1348
|
-
* @since 0.4.0
|
|
1349
|
-
*/
|
|
1350
|
-
function canonicalizeBodyRelaxed(body) {
|
|
1351
|
-
if (body === "") return "";
|
|
1352
|
-
let processedBody = body;
|
|
1353
|
-
if (!processedBody.endsWith("\r\n")) processedBody += "\r\n";
|
|
1354
|
-
const lines = processedBody.split("\r\n");
|
|
1355
|
-
const canonicalizedLines = [];
|
|
1356
|
-
for (let i = 0; i < lines.length - 1; i++) {
|
|
1357
|
-
let line = lines[i];
|
|
1358
|
-
line = line.replace(/[\t ]+/g, " ");
|
|
1359
|
-
line = line.replace(/[\t ]+$/, "");
|
|
1360
|
-
canonicalizedLines.push(line);
|
|
1361
|
-
}
|
|
1362
|
-
while (canonicalizedLines.length > 0 && canonicalizedLines[canonicalizedLines.length - 1] === "") canonicalizedLines.pop();
|
|
1363
|
-
if (canonicalizedLines.length === 0) return "";
|
|
1364
|
-
return canonicalizedLines.join("\r\n") + "\r\n";
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
//#endregion
|
|
1368
|
-
//#region src/dkim/types.ts
|
|
1369
|
-
/**
|
|
1370
|
-
* Default header fields to sign if not specified.
|
|
1371
|
-
*
|
|
1372
|
-
* @since 0.4.0
|
|
1373
|
-
*/
|
|
1374
|
-
const DEFAULT_SIGNED_HEADERS = [
|
|
1375
|
-
"from",
|
|
1376
|
-
"to",
|
|
1377
|
-
"subject",
|
|
1378
|
-
"date"
|
|
1379
|
-
];
|
|
1380
|
-
/**
|
|
1381
|
-
* Default DKIM algorithm.
|
|
1382
|
-
*
|
|
1383
|
-
* @since 0.4.0
|
|
1384
|
-
*/
|
|
1385
|
-
const DEFAULT_ALGORITHM = "rsa-sha256";
|
|
1386
|
-
/**
|
|
1387
|
-
* Default canonicalization method.
|
|
1388
|
-
*
|
|
1389
|
-
* @since 0.4.0
|
|
1390
|
-
*/
|
|
1391
|
-
const DEFAULT_CANONICALIZATION = "relaxed/relaxed";
|
|
1392
|
-
|
|
1393
|
-
//#endregion
|
|
1394
|
-
//#region src/dkim/sign.ts
|
|
1395
|
-
/**
|
|
1396
|
-
* Signs a raw email message with DKIM.
|
|
1397
|
-
*
|
|
1398
|
-
* @param rawMessage - The complete raw email message (headers + body)
|
|
1399
|
-
* @param config - DKIM signature configuration
|
|
1400
|
-
* @returns The DKIM-Signature header result
|
|
1401
|
-
* @throws Error if signing fails (e.g., invalid private key)
|
|
1402
|
-
* @since 0.4.0
|
|
1403
|
-
*/
|
|
1404
|
-
async function signMessage(rawMessage, config) {
|
|
1405
|
-
const algorithm = config.algorithm ?? DEFAULT_ALGORITHM;
|
|
1406
|
-
const canonicalization = config.canonicalization ?? DEFAULT_CANONICALIZATION;
|
|
1407
|
-
const headerFields = config.headerFields ?? DEFAULT_SIGNED_HEADERS;
|
|
1408
|
-
const { headers, body } = parseMessage(rawMessage);
|
|
1409
|
-
const [headerCanon, bodyCanon] = canonicalization.split("/");
|
|
1410
|
-
const privateKey = await getPrivateKey(config.privateKey, algorithm);
|
|
1411
|
-
const bodyHash = await computeBodyHash(body, bodyCanon);
|
|
1412
|
-
const dkimHeaderValue = buildDkimHeaderValue({
|
|
1413
|
-
algorithm,
|
|
1414
|
-
canonicalization,
|
|
1415
|
-
signingDomain: config.signingDomain,
|
|
1416
|
-
selector: config.selector,
|
|
1417
|
-
headerFields,
|
|
1418
|
-
bodyHash
|
|
1419
|
-
});
|
|
1420
|
-
const signatureData = buildSignatureData(headers, headerFields, headerCanon, dkimHeaderValue);
|
|
1421
|
-
const signature = await signData(signatureData, privateKey, algorithm);
|
|
1422
|
-
return {
|
|
1423
|
-
headerName: "DKIM-Signature",
|
|
1424
|
-
signature: `${dkimHeaderValue} b=${signature}`
|
|
1425
|
-
};
|
|
1426
|
-
}
|
|
1427
|
-
/**
|
|
1428
|
-
* Parses a raw email message into headers and body.
|
|
1429
|
-
*/
|
|
1430
|
-
function parseMessage(rawMessage) {
|
|
1431
|
-
const separatorIndex = rawMessage.indexOf("\r\n\r\n");
|
|
1432
|
-
if (separatorIndex === -1) return {
|
|
1433
|
-
headers: parseHeaders(rawMessage),
|
|
1434
|
-
body: ""
|
|
1435
|
-
};
|
|
1436
|
-
const headerSection = rawMessage.substring(0, separatorIndex);
|
|
1437
|
-
const body = rawMessage.substring(separatorIndex + 4);
|
|
1438
|
-
return {
|
|
1439
|
-
headers: parseHeaders(headerSection),
|
|
1440
|
-
body
|
|
1441
|
-
};
|
|
1442
|
-
}
|
|
1443
|
-
/**
|
|
1444
|
-
* Parses header section into a map of header name to value.
|
|
1445
|
-
* Handles folded headers (continuation lines).
|
|
1446
|
-
*/
|
|
1447
|
-
function parseHeaders(headerSection) {
|
|
1448
|
-
const headers = /* @__PURE__ */ new Map();
|
|
1449
|
-
const lines = headerSection.split("\r\n");
|
|
1450
|
-
let currentName = "";
|
|
1451
|
-
let currentValue = "";
|
|
1452
|
-
for (const line of lines) if (line.startsWith(" ") || line.startsWith(" ")) currentValue += "\r\n" + line;
|
|
1453
|
-
else {
|
|
1454
|
-
if (currentName) headers.set(currentName.toLowerCase(), currentValue);
|
|
1455
|
-
const colonIndex = line.indexOf(":");
|
|
1456
|
-
if (colonIndex > 0) {
|
|
1457
|
-
currentName = line.substring(0, colonIndex);
|
|
1458
|
-
currentValue = line.substring(colonIndex + 1);
|
|
1912
|
+
//#region src/mime-stream.ts
|
|
1913
|
+
/** Encodes base64 without retaining producer-owned carry bytes. */
|
|
1914
|
+
async function* encodeAttachment(content, signal, progress) {
|
|
1915
|
+
const carry = new Uint8Array(3);
|
|
1916
|
+
let carried = 0;
|
|
1917
|
+
let column = 0;
|
|
1918
|
+
function wrap(encoded) {
|
|
1919
|
+
const parts = [];
|
|
1920
|
+
let offset = 0;
|
|
1921
|
+
while (offset < encoded.length) {
|
|
1922
|
+
if (column === 76) {
|
|
1923
|
+
parts.push("\r\n");
|
|
1924
|
+
column = 0;
|
|
1925
|
+
}
|
|
1926
|
+
const take = Math.min(76 - column, encoded.length - offset);
|
|
1927
|
+
parts.push(encoded.slice(offset, offset + take));
|
|
1928
|
+
offset += take;
|
|
1929
|
+
column += take;
|
|
1459
1930
|
}
|
|
1931
|
+
return node_buffer.Buffer.from(parts.join(""));
|
|
1460
1932
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
}
|
|
1471
|
-
/**
|
|
1472
|
-
* Imports a PEM-encoded private key for use with Web Crypto API.
|
|
1473
|
-
*/
|
|
1474
|
-
async function importPrivateKey(pem, algorithm) {
|
|
1475
|
-
try {
|
|
1476
|
-
const pemContents = pem.replace(/-----BEGIN (?:RSA )?PRIVATE KEY-----/, "").replace(/-----END (?:RSA )?PRIVATE KEY-----/, "").replace(/\s/g, "");
|
|
1477
|
-
const binaryString = atob(pemContents);
|
|
1478
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
1479
|
-
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
|
|
1480
|
-
const keyAlgorithm = algorithm === "ed25519-sha256" ? { name: "Ed25519" } : {
|
|
1481
|
-
name: "RSASSA-PKCS1-v1_5",
|
|
1482
|
-
hash: "SHA-256"
|
|
1483
|
-
};
|
|
1484
|
-
return await crypto.subtle.importKey("pkcs8", bytes, keyAlgorithm, false, ["sign"]);
|
|
1485
|
-
} catch (error) {
|
|
1486
|
-
throw new Error(`Failed to import private key: ${error instanceof Error ? error.message : String(error)}`);
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Computes the body hash (bh= tag value).
|
|
1491
|
-
*/
|
|
1492
|
-
async function computeBodyHash(body, canonMethod) {
|
|
1493
|
-
const canonicalBody = canonMethod === "relaxed" ? canonicalizeBodyRelaxed(body) : canonicalizeBodySimple(body);
|
|
1494
|
-
const encoder = new TextEncoder();
|
|
1495
|
-
const data = encoder.encode(canonicalBody);
|
|
1496
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
1497
|
-
return arrayBufferToBase64(hashBuffer);
|
|
1498
|
-
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Builds the DKIM-Signature header value without the b= signature.
|
|
1501
|
-
*/
|
|
1502
|
-
function buildDkimHeaderValue(params) {
|
|
1503
|
-
const parts = [
|
|
1504
|
-
"v=1",
|
|
1505
|
-
`a=${params.algorithm}`,
|
|
1506
|
-
`c=${params.canonicalization}`,
|
|
1507
|
-
`d=${params.signingDomain}`,
|
|
1508
|
-
`s=${params.selector}`,
|
|
1509
|
-
`h=${params.headerFields.join(":")}`,
|
|
1510
|
-
`bh=${params.bodyHash};`
|
|
1511
|
-
];
|
|
1512
|
-
return parts.join("; ");
|
|
1513
|
-
}
|
|
1514
|
-
/**
|
|
1515
|
-
* Builds the data to be signed (canonicalized headers + DKIM-Signature header).
|
|
1516
|
-
*/
|
|
1517
|
-
function buildSignatureData(headers, headerFields, canonMethod, dkimHeaderValue) {
|
|
1518
|
-
const lines = [];
|
|
1519
|
-
for (const field of headerFields) {
|
|
1520
|
-
const value = headers.get(field.toLowerCase());
|
|
1521
|
-
if (value !== void 0) {
|
|
1522
|
-
const canonicalized = canonMethod === "relaxed" ? canonicalizeHeaderRelaxed(field, value) : canonicalizeHeaderSimple(field, value);
|
|
1523
|
-
lines.push(canonicalized);
|
|
1933
|
+
let processed = 0;
|
|
1934
|
+
for await (const chunk of (0, __upyo_core.iterateAttachmentContent)(content, signal)) {
|
|
1935
|
+
if (chunk.length > 0) progress?.();
|
|
1936
|
+
let offset = 0;
|
|
1937
|
+
if (carried > 0) {
|
|
1938
|
+
while (carried < 3 && offset < chunk.length) carry[carried++] = chunk[offset++];
|
|
1939
|
+
if (carried === 3) {
|
|
1940
|
+
yield wrap(node_buffer.Buffer.from(carry).toString("base64"));
|
|
1941
|
+
carried = 0;
|
|
1942
|
+
}
|
|
1524
1943
|
}
|
|
1944
|
+
while (offset + 3 <= chunk.length) {
|
|
1945
|
+
signal?.throwIfAborted();
|
|
1946
|
+
const length = Math.min(45 * 1024, Math.floor((chunk.length - offset) / 3) * 3);
|
|
1947
|
+
yield wrap(node_buffer.Buffer.from(chunk.buffer, chunk.byteOffset + offset, length).toString("base64"));
|
|
1948
|
+
offset += length;
|
|
1949
|
+
processed += length;
|
|
1950
|
+
if (processed >= 1024 * 1024) {
|
|
1951
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1952
|
+
processed = 0;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
while (offset < chunk.length) carry[carried++] = chunk[offset++];
|
|
1525
1956
|
}
|
|
1526
|
-
|
|
1527
|
-
lines.push(dkimHeader);
|
|
1528
|
-
return lines.join("\r\n");
|
|
1529
|
-
}
|
|
1530
|
-
/**
|
|
1531
|
-
* Signs data using the appropriate algorithm.
|
|
1532
|
-
*/
|
|
1533
|
-
async function signData(data, privateKey, algorithm) {
|
|
1534
|
-
const encoder = new TextEncoder();
|
|
1535
|
-
const dataBuffer = encoder.encode(data);
|
|
1536
|
-
const signAlgorithm = algorithm === "ed25519-sha256" ? "Ed25519" : "RSASSA-PKCS1-v1_5";
|
|
1537
|
-
const signature = await crypto.subtle.sign(signAlgorithm, privateKey, dataBuffer);
|
|
1538
|
-
return arrayBufferToBase64(signature);
|
|
1957
|
+
if (carried > 0) yield wrap(node_buffer.Buffer.from(carry.subarray(0, carried)).toString("base64"));
|
|
1539
1958
|
}
|
|
1540
|
-
/**
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
const bytes = new Uint8Array(buffer);
|
|
1545
|
-
let binary = "";
|
|
1546
|
-
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
1547
|
-
return btoa(binary);
|
|
1959
|
+
/** Encoded payload length, excluding MIME framing and a trailing CRLF. */
|
|
1960
|
+
function attachmentEncodedSize(length) {
|
|
1961
|
+
const encoded = 4 * Math.ceil(length / 3);
|
|
1962
|
+
return encoded + (encoded === 0 ? 0 : 2 * Math.floor((encoded - 1) / 76));
|
|
1548
1963
|
}
|
|
1549
1964
|
|
|
1550
1965
|
//#endregion
|
|
1551
1966
|
//#region src/message-converter.ts
|
|
1552
1967
|
/**
|
|
1553
|
-
*
|
|
1554
|
-
*
|
|
1555
|
-
* @param
|
|
1556
|
-
* @param
|
|
1557
|
-
* @param
|
|
1558
|
-
* @
|
|
1559
|
-
* @
|
|
1560
|
-
* @throws {RangeError} If a header contains a token that cannot be folded
|
|
1561
|
-
* within the RFC 5322 hard line-length limit.
|
|
1968
|
+
* Freezes message metadata without reading attachment content.
|
|
1969
|
+
* @param message Message whose metadata will be frozen.
|
|
1970
|
+
* @param dkimConfig Optional signing configuration.
|
|
1971
|
+
* @param dsn Validated delivery-status parameters.
|
|
1972
|
+
* @param resolvedEnvelope Validated effective SMTP envelope.
|
|
1973
|
+
* @returns A deterministic MIME plan for one send attempt.
|
|
1974
|
+
* @throws {RangeError} If a header cannot fit the RFC 5322 line limit.
|
|
1562
1975
|
*/
|
|
1563
|
-
|
|
1976
|
+
function prepareMessage(message, dkimConfig, dsn, resolvedEnvelope = resolveSmtpEnvelope(message)) {
|
|
1564
1977
|
const envelope = {
|
|
1565
1978
|
...resolvedEnvelope,
|
|
1566
1979
|
dsn
|
|
@@ -1573,23 +1986,45 @@ async function convertMessage(message, dkimConfig, dsn, resolvedEnvelope = resol
|
|
|
1573
1986
|
];
|
|
1574
1987
|
const envelopeAddresses = [...envelope.from == null ? [] : [envelope.from], ...envelope.to];
|
|
1575
1988
|
const requiresSmtpUtf8 = [...headerAddresses, ...envelopeAddresses].some((address) => Array.from(address).some((character) => (character.codePointAt(0) ?? 0) > 127));
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
} catch (error) {
|
|
1583
|
-
if (dkimConfig.onSigningFailure === "send-unsigned") console.warn("DKIM signing failed, sending unsigned:", error);
|
|
1584
|
-
else throw error;
|
|
1585
|
-
}
|
|
1989
|
+
const parts = buildMimeParts(message);
|
|
1990
|
+
const first = parts[0];
|
|
1991
|
+
if (typeof first !== "string") throw new TypeError("Missing MIME headers.");
|
|
1992
|
+
const separator = first.indexOf("\r\n\r\n") + 4;
|
|
1993
|
+
const headers = first.slice(0, separator);
|
|
1994
|
+
parts[0] = first.slice(separator);
|
|
1586
1995
|
return {
|
|
1587
1996
|
envelope,
|
|
1588
|
-
|
|
1589
|
-
|
|
1997
|
+
requiresSmtpUtf8,
|
|
1998
|
+
dkim: dkimConfig,
|
|
1999
|
+
headers,
|
|
2000
|
+
async *body(signal, progress) {
|
|
2001
|
+
for (const part of parts) {
|
|
2002
|
+
signal?.throwIfAborted();
|
|
2003
|
+
if (typeof part === "string") {
|
|
2004
|
+
const bytes = node_buffer.Buffer.from(part);
|
|
2005
|
+
for (let offset = 0; offset < bytes.length; offset += 65536) yield bytes.subarray(offset, offset + 65536);
|
|
2006
|
+
} else yield* encodeAttachment(part.content, signal, progress);
|
|
2007
|
+
}
|
|
2008
|
+
},
|
|
2009
|
+
async size(signal, checkSize) {
|
|
2010
|
+
let size = node_buffer.Buffer.byteLength(headers);
|
|
2011
|
+
let unknown = false;
|
|
2012
|
+
for (const part of parts) {
|
|
2013
|
+
signal?.throwIfAborted();
|
|
2014
|
+
if (typeof part === "string") size += node_buffer.Buffer.byteLength(part);
|
|
2015
|
+
else {
|
|
2016
|
+
if (part.content instanceof Promise) part.content = await (0, __upyo_core.readAttachmentContent)(part.content, signal);
|
|
2017
|
+
if (typeof part.content === "function") unknown = true;
|
|
2018
|
+
else size += attachmentEncodedSize(part.content instanceof Uint8Array ? part.content.byteLength : part.content.size);
|
|
2019
|
+
}
|
|
2020
|
+
if (!Number.isSafeInteger(size)) throw new RangeError("Message size exceeds the safe integer range.");
|
|
2021
|
+
}
|
|
2022
|
+
checkSize?.(size);
|
|
2023
|
+
return unknown ? void 0 : size;
|
|
2024
|
+
}
|
|
1590
2025
|
};
|
|
1591
2026
|
}
|
|
1592
|
-
|
|
2027
|
+
function buildMimeParts(message) {
|
|
1593
2028
|
const lines = [];
|
|
1594
2029
|
const boundary = generateBoundary();
|
|
1595
2030
|
const hasAttachments = message.attachments.length > 0;
|
|
@@ -1654,7 +2089,7 @@ async function buildRawMessage(message) {
|
|
|
1654
2089
|
lines.push(`Content-ID: <${attachment.contentId}>`);
|
|
1655
2090
|
} else lines.push(foldHeader("Content-Disposition", `attachment; ${encodeMimeParameter("filename", attachment.filename)}`));
|
|
1656
2091
|
lines.push("");
|
|
1657
|
-
lines.push(
|
|
2092
|
+
lines.push({ content: attachment.content });
|
|
1658
2093
|
}
|
|
1659
2094
|
lines.push("");
|
|
1660
2095
|
lines.push(`--${boundary}--`);
|
|
@@ -1669,7 +2104,18 @@ async function buildRawMessage(message) {
|
|
|
1669
2104
|
lines.push("");
|
|
1670
2105
|
lines.push(encodeQuotedPrintable(message.content.text));
|
|
1671
2106
|
}
|
|
1672
|
-
|
|
2107
|
+
const parts = [];
|
|
2108
|
+
let text = "";
|
|
2109
|
+
for (const line of lines) {
|
|
2110
|
+
if (typeof line === "string") text += line;
|
|
2111
|
+
else {
|
|
2112
|
+
parts.push(text, line);
|
|
2113
|
+
text = "";
|
|
2114
|
+
}
|
|
2115
|
+
text += "\r\n";
|
|
2116
|
+
}
|
|
2117
|
+
parts.push(text);
|
|
2118
|
+
return parts;
|
|
1673
2119
|
}
|
|
1674
2120
|
function generateBoundary() {
|
|
1675
2121
|
return `boundary-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -1784,10 +2230,6 @@ function encodeQuotedPrintable(text) {
|
|
|
1784
2230
|
}
|
|
1785
2231
|
return result;
|
|
1786
2232
|
}
|
|
1787
|
-
function encodeBase64(data) {
|
|
1788
|
-
const base64 = node_buffer.Buffer.from(data).toString("base64");
|
|
1789
|
-
return base64.replace(/(.{76})/g, "$1\r\n").trim();
|
|
1790
|
-
}
|
|
1791
2233
|
|
|
1792
2234
|
//#endregion
|
|
1793
2235
|
//#region src/smtp-transport.ts
|
|
@@ -1849,6 +2291,7 @@ var SmtpTransport = class {
|
|
|
1849
2291
|
* and options.
|
|
1850
2292
|
*/
|
|
1851
2293
|
constructor(config) {
|
|
2294
|
+
validateDkimBodyMode(config.dkim);
|
|
1852
2295
|
this.config = config;
|
|
1853
2296
|
this.poolSize = config.poolSize ?? 5;
|
|
1854
2297
|
const auth = config.auth;
|
|
@@ -1890,7 +2333,7 @@ var SmtpTransport = class {
|
|
|
1890
2333
|
const dsn = resolveSmtpDsn(envelope, options?.dsn);
|
|
1891
2334
|
connection = await this.getConnection(options?.signal);
|
|
1892
2335
|
options?.signal?.throwIfAborted();
|
|
1893
|
-
const smtpMessage =
|
|
2336
|
+
const smtpMessage = prepareMessage(message, this.config.dkim, dsn, envelope);
|
|
1894
2337
|
options?.signal?.throwIfAborted();
|
|
1895
2338
|
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1896
2339
|
await this.returnConnection(connection);
|
|
@@ -1901,7 +2344,7 @@ var SmtpTransport = class {
|
|
|
1901
2344
|
rejectedRecipients: result.rejectedRecipients
|
|
1902
2345
|
};
|
|
1903
2346
|
} catch (error) {
|
|
1904
|
-
if (connection != null) if (isReusableLocalFailure(error)) await this.returnConnection(connection);
|
|
2347
|
+
if (connection != null) if (connection.usable && isReusableLocalFailure(error)) await this.returnConnection(connection);
|
|
1905
2348
|
else await this.discardConnection(connection);
|
|
1906
2349
|
options?.signal?.throwIfAborted();
|
|
1907
2350
|
return createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
@@ -1965,7 +2408,7 @@ var SmtpTransport = class {
|
|
|
1965
2408
|
try {
|
|
1966
2409
|
const envelope = resolveEnvelopeOption(message, options?.envelope, index++);
|
|
1967
2410
|
const dsn = resolveSmtpDsn(envelope, options?.dsn);
|
|
1968
|
-
const smtpMessage =
|
|
2411
|
+
const smtpMessage = prepareMessage(message, this.config.dkim, dsn, envelope);
|
|
1969
2412
|
options?.signal?.throwIfAborted();
|
|
1970
2413
|
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1971
2414
|
yield {
|
|
@@ -1976,7 +2419,7 @@ var SmtpTransport = class {
|
|
|
1976
2419
|
};
|
|
1977
2420
|
} catch (error) {
|
|
1978
2421
|
options?.signal?.throwIfAborted();
|
|
1979
|
-
if (!isReusableLocalFailure(error)) connectionValid = false;
|
|
2422
|
+
if (!connection.usable || !isReusableLocalFailure(error)) connectionValid = false;
|
|
1980
2423
|
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1981
2424
|
}
|
|
1982
2425
|
}
|
|
@@ -1991,7 +2434,7 @@ var SmtpTransport = class {
|
|
|
1991
2434
|
try {
|
|
1992
2435
|
const envelope = resolveEnvelopeOption(message, options?.envelope, index++);
|
|
1993
2436
|
const dsn = resolveSmtpDsn(envelope, options?.dsn);
|
|
1994
|
-
const smtpMessage =
|
|
2437
|
+
const smtpMessage = prepareMessage(message, this.config.dkim, dsn, envelope);
|
|
1995
2438
|
options?.signal?.throwIfAborted();
|
|
1996
2439
|
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1997
2440
|
yield {
|
|
@@ -2002,7 +2445,7 @@ var SmtpTransport = class {
|
|
|
2002
2445
|
};
|
|
2003
2446
|
} catch (error) {
|
|
2004
2447
|
options?.signal?.throwIfAborted();
|
|
2005
|
-
if (!isReusableLocalFailure(error)) connectionValid = false;
|
|
2448
|
+
if (!connection.usable || !isReusableLocalFailure(error)) connectionValid = false;
|
|
2006
2449
|
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
2007
2450
|
}
|
|
2008
2451
|
}
|
|
@@ -2016,7 +2459,11 @@ var SmtpTransport = class {
|
|
|
2016
2459
|
}
|
|
2017
2460
|
async getConnection(signal) {
|
|
2018
2461
|
signal?.throwIfAborted();
|
|
2019
|
-
|
|
2462
|
+
while (this.connectionPool.length > 0) {
|
|
2463
|
+
const connection$1 = this.connectionPool.pop();
|
|
2464
|
+
if (connection$1.usable) return connection$1;
|
|
2465
|
+
await this.discardConnection(connection$1);
|
|
2466
|
+
}
|
|
2020
2467
|
const connection = new SmtpConnection(this.config, this.tokenManager);
|
|
2021
2468
|
try {
|
|
2022
2469
|
await this.connectAndSetup(connection, signal);
|
|
@@ -2044,7 +2491,7 @@ var SmtpTransport = class {
|
|
|
2044
2491
|
await connection.authenticate(signal);
|
|
2045
2492
|
}
|
|
2046
2493
|
async returnConnection(connection) {
|
|
2047
|
-
if (!connection.config.pool) {
|
|
2494
|
+
if (!connection.usable || !connection.config.pool) {
|
|
2048
2495
|
await connection.quit();
|
|
2049
2496
|
return;
|
|
2050
2497
|
}
|
|
@@ -2119,6 +2566,13 @@ function createSmtpFailure(message, error) {
|
|
|
2119
2566
|
retryable: false,
|
|
2120
2567
|
attempts: 1
|
|
2121
2568
|
});
|
|
2569
|
+
if (error instanceof SmtpAttachmentReplayError) return (0, __upyo_core.createFailedReceipt)(message, {
|
|
2570
|
+
provider: "smtp",
|
|
2571
|
+
code: "smtp.attachment-replay-mismatch",
|
|
2572
|
+
category: "validation",
|
|
2573
|
+
retryable: false,
|
|
2574
|
+
attempts: 1
|
|
2575
|
+
});
|
|
2122
2576
|
if (error instanceof SmtpMessageSizeError) return (0, __upyo_core.createFailedReceipt)(message, {
|
|
2123
2577
|
provider: "smtp",
|
|
2124
2578
|
code: "smtp.message-size-exceeded",
|
|
@@ -2161,7 +2615,7 @@ function createSmtpFailure(message, error) {
|
|
|
2161
2615
|
});
|
|
2162
2616
|
}
|
|
2163
2617
|
function isReusableLocalFailure(error) {
|
|
2164
|
-
return error instanceof SmtpMessageSizeError || error instanceof SmtpUtf8UnsupportedError || error instanceof SmtpEnvelopeValidationError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
|
|
2618
|
+
return error instanceof SmtpMessageSizeError && error.phase === "preflight" || error instanceof SmtpUtf8UnsupportedError || error instanceof SmtpEnvelopeValidationError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
|
|
2165
2619
|
}
|
|
2166
2620
|
function resolveEnvelopeOption(message, option, index) {
|
|
2167
2621
|
let override;
|
|
@@ -2213,6 +2667,7 @@ function isSmtpResponseProviderDetails(value) {
|
|
|
2213
2667
|
}
|
|
2214
2668
|
|
|
2215
2669
|
//#endregion
|
|
2670
|
+
exports.SmtpAttachmentReplayError = SmtpAttachmentReplayError;
|
|
2216
2671
|
exports.SmtpAuthError = SmtpAuthError;
|
|
2217
2672
|
exports.SmtpDsnUnsupportedError = SmtpDsnUnsupportedError;
|
|
2218
2673
|
exports.SmtpDsnValidationError = SmtpDsnValidationError;
|