@getstrata/core 2.2.22 → 2.2.23
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/CHANGELOG.md +4 -0
- package/dist/core/mail/mailer.d.ts +4 -0
- package/dist/entries/facades.js +279 -113
- package/dist/entries/mail/mailer.js +278 -112
- package/dist/index.js +278 -112
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10641,8 +10641,8 @@ function resolveSmtpConfig() {
|
|
|
10641
10641
|
if (!from) {
|
|
10642
10642
|
throw new Error('MAIL_DRIVER="smtp" requires MAIL_FROM to be set.');
|
|
10643
10643
|
}
|
|
10644
|
-
const port = Number
|
|
10645
|
-
if (!Number.isInteger(port) || port <= 0) {
|
|
10644
|
+
const port = Number(process.env.MAIL_PORT ?? "587");
|
|
10645
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
10646
10646
|
throw new Error('Environment variable "MAIL_PORT" must be a positive integer.');
|
|
10647
10647
|
}
|
|
10648
10648
|
return {
|
|
@@ -10650,6 +10650,7 @@ function resolveSmtpConfig() {
|
|
|
10650
10650
|
port,
|
|
10651
10651
|
from,
|
|
10652
10652
|
secure: (process.env.MAIL_SECURE ?? "false") === "true",
|
|
10653
|
+
...process.env.MAIL_TIMEOUT_MS !== undefined ? { timeoutMs: smtpTimeout(Number(process.env.MAIL_TIMEOUT_MS)) } : {},
|
|
10653
10654
|
...process.env.MAIL_USERNAME?.trim() ? { username: process.env.MAIL_USERNAME.trim() } : {},
|
|
10654
10655
|
...process.env.MAIL_PASSWORD?.trim() ? { password: process.env.MAIL_PASSWORD.trim() } : {}
|
|
10655
10656
|
};
|
|
@@ -10659,6 +10660,12 @@ function assertSafeSmtpField(value, field) {
|
|
|
10659
10660
|
if (/[\r\n]/u.test(value)) {
|
|
10660
10661
|
throw new Error(`Mail ${field} must not contain CR or LF.`);
|
|
10661
10662
|
}
|
|
10663
|
+
for (const character of value) {
|
|
10664
|
+
const code = character.charCodeAt(0);
|
|
10665
|
+
if (code < 32 && code !== 9 || code === 127) {
|
|
10666
|
+
throw new Error(`Mail ${field} must not contain control characters.`);
|
|
10667
|
+
}
|
|
10668
|
+
}
|
|
10662
10669
|
return value;
|
|
10663
10670
|
}
|
|
10664
10671
|
function smtpEnvelopeAddress(value, field) {
|
|
@@ -10679,148 +10686,307 @@ function redactMailBody(body) {
|
|
|
10679
10686
|
function encodeBase64(value) {
|
|
10680
10687
|
return Buffer.from(value, "utf8").toString("base64");
|
|
10681
10688
|
}
|
|
10682
|
-
function
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
while (remainder.includes(`\r
|
|
10686
|
-
`)) {
|
|
10687
|
-
const index = remainder.indexOf(`\r
|
|
10688
|
-
`);
|
|
10689
|
-
const line = remainder.slice(0, index);
|
|
10690
|
-
remainder = remainder.slice(index + 2);
|
|
10691
|
-
if (line.length >= 4 && line[3] === "-") {
|
|
10692
|
-
continue;
|
|
10693
|
-
}
|
|
10694
|
-
responses.push(line);
|
|
10689
|
+
function smtpTimeout(value = 1e4) {
|
|
10690
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 2147483647) {
|
|
10691
|
+
throw new RangeError("SMTP timeoutMs must be a positive integer within the timer range.");
|
|
10695
10692
|
}
|
|
10696
|
-
return
|
|
10693
|
+
return value;
|
|
10697
10694
|
}
|
|
10698
|
-
async function
|
|
10699
|
-
const
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
throw new Error(`Unexpected SMTP response: ${response}`);
|
|
10695
|
+
async function defaultSmtpTransport(config, message) {
|
|
10696
|
+
const timeoutMs = smtpTimeout(config.timeoutMs);
|
|
10697
|
+
if (!config.host.trim() || !Number.isInteger(config.port) || config.port < 1 || config.port > 65535) {
|
|
10698
|
+
throw new RangeError("SMTP requires a host and a port between 1 and 65535.");
|
|
10703
10699
|
}
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10700
|
+
if (Boolean(config.username) !== Boolean(config.password)) {
|
|
10701
|
+
throw new Error("SMTP authentication requires both username and password.");
|
|
10702
|
+
}
|
|
10703
|
+
if (config.username)
|
|
10704
|
+
assertSafeSmtpField(config.username, "username");
|
|
10705
|
+
if (config.password)
|
|
10706
|
+
assertSafeSmtpField(config.password, "password");
|
|
10707
|
+
const fromHeader = assertSafeSmtpAddress(config.from, "from");
|
|
10708
|
+
const toHeader = assertSafeSmtpAddress(message.to, "to");
|
|
10709
|
+
assertSafeSmtpField(message.subject, "subject");
|
|
10710
|
+
const fromEnvelope = smtpEnvelopeAddress(config.from, "from");
|
|
10711
|
+
const toEnvelope = smtpEnvelopeAddress(message.to, "to");
|
|
10712
|
+
if (/[^\x21-\x7e]/u.test(fromEnvelope + toEnvelope)) {
|
|
10713
|
+
throw new Error("Default SMTP transport requires ASCII envelopes; SMTPUTF8 is not negotiated.");
|
|
10714
|
+
}
|
|
10715
|
+
for (const address of [fromEnvelope, toEnvelope]) {
|
|
10716
|
+
const at = address.lastIndexOf("@");
|
|
10717
|
+
if (Buffer.byteLength(address) > 254 || Buffer.byteLength(address.slice(0, at)) > 64) {
|
|
10718
|
+
throw new RangeError("SMTP envelope address exceeds the path/local-part length limit.");
|
|
10719
|
+
}
|
|
10720
|
+
}
|
|
10721
|
+
message.signal?.throwIfAborted();
|
|
10722
|
+
let socket;
|
|
10723
|
+
let finished = false;
|
|
10724
|
+
let failed = false;
|
|
10725
|
+
let failure;
|
|
10726
|
+
let rejectFailure;
|
|
10727
|
+
const failurePromise = new Promise((_, reject) => {
|
|
10728
|
+
rejectFailure = reject;
|
|
10729
|
+
});
|
|
10730
|
+
failurePromise.catch(() => {});
|
|
10731
|
+
const responses = [];
|
|
10708
10732
|
const waiters = [];
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10733
|
+
let drain;
|
|
10734
|
+
let drainVersion = 0;
|
|
10735
|
+
let buffer = "";
|
|
10736
|
+
let multilineCode;
|
|
10737
|
+
let multilineLines = 0;
|
|
10738
|
+
const decoder = new TextDecoder;
|
|
10739
|
+
const fail = (reason) => {
|
|
10740
|
+
if (finished || failed)
|
|
10714
10741
|
return;
|
|
10742
|
+
failed = true;
|
|
10743
|
+
failure = reason;
|
|
10744
|
+
rejectFailure(reason);
|
|
10745
|
+
waiters.length = 0;
|
|
10746
|
+
drain?.();
|
|
10747
|
+
drain = undefined;
|
|
10748
|
+
socket?.terminate();
|
|
10749
|
+
};
|
|
10750
|
+
const onAbort = () => fail(message.signal?.reason ?? new DOMException("Mail send aborted.", "AbortError"));
|
|
10751
|
+
const timer = setTimeout(() => fail(new DOMException("SMTP send deadline exceeded.", "TimeoutError")), timeoutMs);
|
|
10752
|
+
message.signal?.addEventListener("abort", onAbort, { once: true });
|
|
10753
|
+
if (message.signal?.aborted)
|
|
10754
|
+
onAbort();
|
|
10755
|
+
const readResponse = async (expected) => {
|
|
10756
|
+
const queued = responses.shift();
|
|
10757
|
+
const response = queued ?? await Promise.race([
|
|
10758
|
+
failed ? Promise.reject(failure) : new Promise((resolve) => waiters.push(resolve)),
|
|
10759
|
+
failurePromise
|
|
10760
|
+
]);
|
|
10761
|
+
const code = response.slice(0, 3);
|
|
10762
|
+
if (!expected.includes(code))
|
|
10763
|
+
throw new Error(`Unexpected SMTP response code: ${code}`);
|
|
10764
|
+
};
|
|
10765
|
+
const write = async (text) => {
|
|
10766
|
+
const bytes = Buffer.from(text, "utf8");
|
|
10767
|
+
let offset = 0;
|
|
10768
|
+
while (offset < bytes.length) {
|
|
10769
|
+
if (failed)
|
|
10770
|
+
throw failure;
|
|
10771
|
+
const version = drainVersion;
|
|
10772
|
+
if (!socket)
|
|
10773
|
+
throw new Error("SMTP socket is not connected.");
|
|
10774
|
+
const written = socket.write(bytes.subarray(offset));
|
|
10775
|
+
if (written < 0)
|
|
10776
|
+
throw new Error("SMTP socket write failed.");
|
|
10777
|
+
offset += written;
|
|
10778
|
+
if (offset < bytes.length && version === drainVersion) {
|
|
10779
|
+
await Promise.race([
|
|
10780
|
+
new Promise((resolve) => {
|
|
10781
|
+
drain = resolve;
|
|
10782
|
+
}),
|
|
10783
|
+
failurePromise
|
|
10784
|
+
]);
|
|
10785
|
+
}
|
|
10715
10786
|
}
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
buffer = parsed.remainder;
|
|
10727
|
-
while (parsed.responses.length > 0 && waiters.length > 0) {
|
|
10728
|
-
const response = parsed.responses.shift();
|
|
10729
|
-
waiters.shift()?.resolve(response);
|
|
10787
|
+
};
|
|
10788
|
+
try {
|
|
10789
|
+
const connecting = Bun.connect({
|
|
10790
|
+
hostname: config.host,
|
|
10791
|
+
port: config.port,
|
|
10792
|
+
...config.secure ? {
|
|
10793
|
+
tls: {
|
|
10794
|
+
...config.tls,
|
|
10795
|
+
serverName: config.tls?.serverName ?? config.host,
|
|
10796
|
+
rejectUnauthorized: true
|
|
10730
10797
|
}
|
|
10731
|
-
},
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10798
|
+
} : {},
|
|
10799
|
+
socket: {
|
|
10800
|
+
open(value) {
|
|
10801
|
+
socket = value;
|
|
10802
|
+
if (failed || finished)
|
|
10803
|
+
value.terminate();
|
|
10804
|
+
},
|
|
10805
|
+
data(_socket, chunk) {
|
|
10806
|
+
if (failed || finished)
|
|
10807
|
+
return;
|
|
10808
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
10809
|
+
if (Buffer.byteLength(buffer, "utf8") > 16384) {
|
|
10810
|
+
fail(new Error("SMTP response exceeded the buffer limit."));
|
|
10811
|
+
return;
|
|
10812
|
+
}
|
|
10813
|
+
while (buffer.includes(`\r
|
|
10814
|
+
`)) {
|
|
10815
|
+
const index = buffer.indexOf(`\r
|
|
10816
|
+
`);
|
|
10817
|
+
const line = buffer.slice(0, index);
|
|
10818
|
+
buffer = buffer.slice(index + 2);
|
|
10819
|
+
const match = /^([2-5]\d{2})([- ]|$)/.exec(line);
|
|
10820
|
+
if (!match || multilineCode !== undefined && match[1] !== multilineCode) {
|
|
10821
|
+
fail(new Error("Malformed SMTP reply."));
|
|
10822
|
+
return;
|
|
10823
|
+
}
|
|
10824
|
+
if (++multilineLines > 128) {
|
|
10825
|
+
fail(new Error("SMTP reply exceeded the line limit."));
|
|
10826
|
+
return;
|
|
10827
|
+
}
|
|
10828
|
+
if (match[2] === "-") {
|
|
10829
|
+
multilineCode = match[1];
|
|
10830
|
+
continue;
|
|
10831
|
+
}
|
|
10832
|
+
multilineCode = undefined;
|
|
10833
|
+
multilineLines = 0;
|
|
10834
|
+
const waiter = waiters.shift();
|
|
10835
|
+
if (waiter)
|
|
10836
|
+
waiter(line);
|
|
10837
|
+
else {
|
|
10838
|
+
responses.push(line);
|
|
10839
|
+
if (responses.length > 32) {
|
|
10840
|
+
fail(new Error("Too many buffered SMTP replies."));
|
|
10841
|
+
return;
|
|
10842
|
+
}
|
|
10843
|
+
}
|
|
10844
|
+
}
|
|
10845
|
+
},
|
|
10846
|
+
drain() {
|
|
10847
|
+
drainVersion++;
|
|
10848
|
+
const waiting = drain;
|
|
10849
|
+
drain = undefined;
|
|
10850
|
+
waiting?.();
|
|
10851
|
+
},
|
|
10852
|
+
end() {
|
|
10853
|
+
fail(new Error("SMTP peer closed before the send completed."));
|
|
10854
|
+
},
|
|
10855
|
+
close() {
|
|
10856
|
+
fail(new Error("SMTP connection closed before the send completed."));
|
|
10857
|
+
},
|
|
10858
|
+
error() {
|
|
10859
|
+
fail(new Error("SMTP socket failed."));
|
|
10860
|
+
},
|
|
10861
|
+
connectError() {
|
|
10862
|
+
fail(new Error("SMTP connection failed."));
|
|
10863
|
+
},
|
|
10864
|
+
handshake(_socket, success) {
|
|
10865
|
+
if (!success)
|
|
10866
|
+
fail(new Error("SMTP TLS verification failed."));
|
|
10736
10867
|
}
|
|
10737
10868
|
}
|
|
10738
|
-
}
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
assertSafeSmtpField(config.password, "password");
|
|
10748
|
-
}
|
|
10749
|
-
const { socket, readResponse } = await openSmtpConnection(config);
|
|
10750
|
-
try {
|
|
10751
|
-
await waitForSmtpResponse(readResponse, ["220"]);
|
|
10752
|
-
await socket.write(`EHLO ${smtpEhloHost()}\r
|
|
10869
|
+
});
|
|
10870
|
+
connecting.then((value) => {
|
|
10871
|
+
socket = value;
|
|
10872
|
+
if (failed || finished)
|
|
10873
|
+
value.terminate();
|
|
10874
|
+
}, () => {});
|
|
10875
|
+
socket = await Promise.race([connecting, failurePromise]);
|
|
10876
|
+
await readResponse(["220"]);
|
|
10877
|
+
await write(`EHLO ${smtpEhloHost()}\r
|
|
10753
10878
|
`);
|
|
10754
|
-
await
|
|
10879
|
+
await readResponse(["250"]);
|
|
10755
10880
|
if (config.username && config.password) {
|
|
10756
|
-
await
|
|
10881
|
+
await write(`AUTH LOGIN\r
|
|
10757
10882
|
`);
|
|
10758
|
-
await
|
|
10759
|
-
await
|
|
10883
|
+
await readResponse(["334"]);
|
|
10884
|
+
await write(`${encodeBase64(config.username)}\r
|
|
10760
10885
|
`);
|
|
10761
|
-
await
|
|
10762
|
-
await
|
|
10886
|
+
await readResponse(["334"]);
|
|
10887
|
+
await write(`${encodeBase64(config.password)}\r
|
|
10763
10888
|
`);
|
|
10764
|
-
await
|
|
10889
|
+
await readResponse(["235"]);
|
|
10765
10890
|
}
|
|
10766
|
-
|
|
10767
|
-
const toHeader = assertSafeSmtpAddress(message.to, "to");
|
|
10768
|
-
const from = smtpEnvelopeAddress(config.from, "from");
|
|
10769
|
-
const to = smtpEnvelopeAddress(message.to, "to");
|
|
10770
|
-
await socket.write(`MAIL FROM:<${from}>\r
|
|
10891
|
+
await write(`MAIL FROM:<${fromEnvelope}>\r
|
|
10771
10892
|
`);
|
|
10772
|
-
await
|
|
10773
|
-
await
|
|
10893
|
+
await readResponse(["250"]);
|
|
10894
|
+
await write(`RCPT TO:<${toEnvelope}>\r
|
|
10774
10895
|
`);
|
|
10775
|
-
await
|
|
10776
|
-
await
|
|
10896
|
+
await readResponse(["250", "251"]);
|
|
10897
|
+
await write(`DATA\r
|
|
10777
10898
|
`);
|
|
10778
|
-
await
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
await socket.write(payload);
|
|
10785
|
-
await waitForSmtpResponse(readResponse, ["250"]);
|
|
10786
|
-
await socket.write(`QUIT\r
|
|
10899
|
+
await readResponse(["354"]);
|
|
10900
|
+
await write(buildSmtpPayload(fromHeader, { ...message, to: toHeader }));
|
|
10901
|
+
await readResponse(["250"]);
|
|
10902
|
+
finished = true;
|
|
10903
|
+
try {
|
|
10904
|
+
socket.write(`QUIT\r
|
|
10787
10905
|
`);
|
|
10788
|
-
|
|
10906
|
+
} catch {}
|
|
10789
10907
|
} finally {
|
|
10790
|
-
|
|
10908
|
+
finished = true;
|
|
10909
|
+
clearTimeout(timer);
|
|
10910
|
+
message.signal?.removeEventListener("abort", onAbort);
|
|
10911
|
+
waiters.length = 0;
|
|
10912
|
+
responses.length = 0;
|
|
10913
|
+
drain = undefined;
|
|
10914
|
+
socket?.end();
|
|
10791
10915
|
}
|
|
10792
10916
|
}
|
|
10917
|
+
function encodeSmtpHeader(value) {
|
|
10918
|
+
if (/^[\x20-\x7e]{0,60}$/u.test(value))
|
|
10919
|
+
return value;
|
|
10920
|
+
const words = [];
|
|
10921
|
+
let part = "";
|
|
10922
|
+
for (const character of value) {
|
|
10923
|
+
if (Buffer.byteLength(part + character, "utf8") > 45) {
|
|
10924
|
+
words.push(`=?UTF-8?B?${encodeBase64(part)}?=`);
|
|
10925
|
+
part = "";
|
|
10926
|
+
}
|
|
10927
|
+
part += character;
|
|
10928
|
+
}
|
|
10929
|
+
if (part)
|
|
10930
|
+
words.push(`=?UTF-8?B?${encodeBase64(part)}?=`);
|
|
10931
|
+
return words.join(`\r
|
|
10932
|
+
`);
|
|
10933
|
+
}
|
|
10934
|
+
function smtpAddressHeader(value) {
|
|
10935
|
+
if (!value.includes("<"))
|
|
10936
|
+
return value;
|
|
10937
|
+
const name = value.slice(0, value.lastIndexOf("<")).trim();
|
|
10938
|
+
const address = smtpEnvelopeAddress(value, "address");
|
|
10939
|
+
if (!name)
|
|
10940
|
+
return address;
|
|
10941
|
+
const encoded = encodeSmtpHeader(name);
|
|
10942
|
+
const display = encoded.startsWith("=?UTF-8?B?") ? encoded : `"${encoded.replace(/[\\"]/gu, "\\$&")}"`;
|
|
10943
|
+
return `${display} <${address}>`;
|
|
10944
|
+
}
|
|
10945
|
+
function smtpMimePart(type, value) {
|
|
10946
|
+
const normalized = value.replace(/\r\n|\r|\n/gu, `\r
|
|
10947
|
+
`);
|
|
10948
|
+
const base64 = /[^\t\r\n\x20-\x7e]/u.test(normalized) || normalized.split(`\r
|
|
10949
|
+
`).some((line) => line.length > 998);
|
|
10950
|
+
return [
|
|
10951
|
+
`Content-Type: ${type}; charset=utf-8`,
|
|
10952
|
+
`Content-Transfer-Encoding: ${base64 ? "base64" : "7bit"}`,
|
|
10953
|
+
"",
|
|
10954
|
+
base64 ? (encodeBase64(normalized).match(/.{1,76}/gu) ?? []).join(`\r
|
|
10955
|
+
`) : normalized
|
|
10956
|
+
];
|
|
10957
|
+
}
|
|
10793
10958
|
function buildSmtpPayload(from, message) {
|
|
10794
|
-
const safeFrom = assertSafeSmtpAddress(from, "from");
|
|
10795
|
-
const safeTo = assertSafeSmtpAddress(message.to, "to");
|
|
10796
|
-
const safeSubject = assertSafeSmtpField(message.subject, "subject");
|
|
10797
10959
|
const headers = [
|
|
10798
|
-
`From: ${
|
|
10799
|
-
`To: ${
|
|
10800
|
-
`Subject: ${
|
|
10960
|
+
`From: ${smtpAddressHeader(assertSafeSmtpAddress(from, "from"))}`,
|
|
10961
|
+
`To: ${smtpAddressHeader(assertSafeSmtpAddress(message.to, "to"))}`,
|
|
10962
|
+
`Subject: ${encodeSmtpHeader(assertSafeSmtpField(message.subject, "subject"))}`,
|
|
10801
10963
|
"MIME-Version: 1.0"
|
|
10802
10964
|
];
|
|
10965
|
+
let lines;
|
|
10803
10966
|
if (message.html) {
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10967
|
+
let boundary;
|
|
10968
|
+
do {
|
|
10969
|
+
boundary = `strata-${crypto.randomUUID()}`;
|
|
10970
|
+
} while (message.body.includes(boundary) || message.html.includes(boundary));
|
|
10971
|
+
lines = [
|
|
10972
|
+
...headers,
|
|
10973
|
+
`Content-Type: multipart/alternative; boundary="${boundary}"`,
|
|
10809
10974
|
"",
|
|
10810
|
-
message.body,
|
|
10811
10975
|
`--${boundary}`,
|
|
10812
|
-
"
|
|
10813
|
-
|
|
10814
|
-
message.html,
|
|
10976
|
+
...smtpMimePart("text/plain", message.body),
|
|
10977
|
+
`--${boundary}`,
|
|
10978
|
+
...smtpMimePart("text/html", message.html),
|
|
10815
10979
|
`--${boundary}--`,
|
|
10816
10980
|
""
|
|
10817
10981
|
];
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10821
|
-
|
|
10822
|
-
return
|
|
10823
|
-
`)
|
|
10982
|
+
} else
|
|
10983
|
+
lines = [...headers, ...smtpMimePart("text/plain", message.body)];
|
|
10984
|
+
const content = lines.join(`\r
|
|
10985
|
+
`).replace(/(^|\r\n)\./gu, "$1..");
|
|
10986
|
+
return `${content}${content.endsWith(`\r
|
|
10987
|
+
`) ? "" : `\r
|
|
10988
|
+
`}.\r
|
|
10989
|
+
`;
|
|
10824
10990
|
}
|
|
10825
10991
|
|
|
10826
10992
|
class LogMailDriver {
|