@getstrata/core 2.2.22 → 2.2.24
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 +8 -0
- package/dist/core/cache/redisCacheStore.d.ts +3 -1
- package/dist/core/mail/mailer.d.ts +4 -0
- package/dist/entries/cache/createCacheStore.js +30 -5
- package/dist/entries/facades.js +279 -113
- package/dist/entries/mail/mailer.js +278 -112
- package/dist/index.js +308 -117
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3893,6 +3893,7 @@ return redis.error_reply('Unknown cache operation')
|
|
|
3893
3893
|
`;
|
|
3894
3894
|
|
|
3895
3895
|
class RedisCacheStore {
|
|
3896
|
+
redisUrl;
|
|
3896
3897
|
ttlMs;
|
|
3897
3898
|
maxEntries;
|
|
3898
3899
|
client;
|
|
@@ -3901,6 +3902,7 @@ class RedisCacheStore {
|
|
|
3901
3902
|
fills = new Set;
|
|
3902
3903
|
closed = false;
|
|
3903
3904
|
constructor(redisUrl, ttlMs, maxEntries, options = {}) {
|
|
3905
|
+
this.redisUrl = redisUrl;
|
|
3904
3906
|
this.ttlMs = ttlMs;
|
|
3905
3907
|
this.maxEntries = maxEntries;
|
|
3906
3908
|
if (!Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
@@ -3925,7 +3927,10 @@ class RedisCacheStore {
|
|
|
3925
3927
|
const namespace = namespacedRedisKey("cache:v3:");
|
|
3926
3928
|
const slot = new Bun.CryptoHasher("sha256").update(namespace).digest("hex");
|
|
3927
3929
|
this.keys = ["entries", "expiry", "order", "metadata", "metadata-expiry"].map((name) => `${namespace}{${slot}}:${name}`);
|
|
3928
|
-
this.client =
|
|
3930
|
+
this.client = this.createClient();
|
|
3931
|
+
}
|
|
3932
|
+
createClient() {
|
|
3933
|
+
return new RedisClient(this.redisUrl, {
|
|
3929
3934
|
connectionTimeout: this.options.commandTimeoutMs,
|
|
3930
3935
|
maxRetries: 1
|
|
3931
3936
|
});
|
|
@@ -3934,7 +3939,8 @@ class RedisCacheStore {
|
|
|
3934
3939
|
this.closed = true;
|
|
3935
3940
|
for (const fill of this.fills)
|
|
3936
3941
|
fill.abort(new Error("Redis cache store is closed."));
|
|
3937
|
-
this.client
|
|
3942
|
+
this.client?.close();
|
|
3943
|
+
this.client = undefined;
|
|
3938
3944
|
}
|
|
3939
3945
|
async get(key) {
|
|
3940
3946
|
const result = await this.execute("get", key);
|
|
@@ -4013,6 +4019,7 @@ class RedisCacheStore {
|
|
|
4013
4019
|
Promise.resolve().then(() => loader(controller.signal)),
|
|
4014
4020
|
aborted
|
|
4015
4021
|
]);
|
|
4022
|
+
controller.signal.throwIfAborted();
|
|
4016
4023
|
done = true;
|
|
4017
4024
|
clearInterval(renew);
|
|
4018
4025
|
await this.execute("publish", key, {
|
|
@@ -4054,16 +4061,28 @@ class RedisCacheStore {
|
|
|
4054
4061
|
const ttlMs = extra.ttlMs ?? this.ttlMs;
|
|
4055
4062
|
if (typeof ttlMs !== "number" || !Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
4056
4063
|
throw new RangeError("ttlMs must be a non-negative safe integer.");
|
|
4064
|
+
if (!this.client)
|
|
4065
|
+
this.client = this.createClient();
|
|
4066
|
+
const client = this.client;
|
|
4067
|
+
const retire = (reason) => {
|
|
4068
|
+
if (this.client !== client)
|
|
4069
|
+
return;
|
|
4070
|
+
this.client = undefined;
|
|
4071
|
+
for (const fill of this.fills)
|
|
4072
|
+
fill.abort(reason);
|
|
4073
|
+
client.close();
|
|
4074
|
+
};
|
|
4057
4075
|
let timer;
|
|
4058
4076
|
const timeout = new Promise((_, reject) => {
|
|
4059
4077
|
timer = setTimeout(() => {
|
|
4060
|
-
|
|
4061
|
-
|
|
4078
|
+
const error = new Error("Redis cache command deadline exceeded.");
|
|
4079
|
+
reject(error);
|
|
4080
|
+
retire(error);
|
|
4062
4081
|
}, this.options.commandTimeoutMs);
|
|
4063
4082
|
});
|
|
4064
4083
|
try {
|
|
4065
4084
|
const raw = await Promise.race([
|
|
4066
|
-
|
|
4085
|
+
client.send("EVAL", [
|
|
4067
4086
|
CACHE_SCRIPT,
|
|
4068
4087
|
String(this.keys.length),
|
|
4069
4088
|
...this.keys,
|
|
@@ -4081,7 +4100,13 @@ class RedisCacheStore {
|
|
|
4081
4100
|
]),
|
|
4082
4101
|
timeout
|
|
4083
4102
|
]);
|
|
4103
|
+
if (this.closed || this.client !== client)
|
|
4104
|
+
throw new Error("Redis cache connection was retired.");
|
|
4084
4105
|
return JSON.parse(String(raw));
|
|
4106
|
+
} catch (error) {
|
|
4107
|
+
if (error && typeof error === "object" && "code" in error && typeof error.code === "string" && error.code.startsWith("ERR_REDIS_CONNECTION"))
|
|
4108
|
+
retire(error);
|
|
4109
|
+
throw error;
|
|
4085
4110
|
} finally {
|
|
4086
4111
|
clearTimeout(timer);
|
|
4087
4112
|
}
|
|
@@ -10641,8 +10666,8 @@ function resolveSmtpConfig() {
|
|
|
10641
10666
|
if (!from) {
|
|
10642
10667
|
throw new Error('MAIL_DRIVER="smtp" requires MAIL_FROM to be set.');
|
|
10643
10668
|
}
|
|
10644
|
-
const port = Number
|
|
10645
|
-
if (!Number.isInteger(port) || port <= 0) {
|
|
10669
|
+
const port = Number(process.env.MAIL_PORT ?? "587");
|
|
10670
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
10646
10671
|
throw new Error('Environment variable "MAIL_PORT" must be a positive integer.');
|
|
10647
10672
|
}
|
|
10648
10673
|
return {
|
|
@@ -10650,6 +10675,7 @@ function resolveSmtpConfig() {
|
|
|
10650
10675
|
port,
|
|
10651
10676
|
from,
|
|
10652
10677
|
secure: (process.env.MAIL_SECURE ?? "false") === "true",
|
|
10678
|
+
...process.env.MAIL_TIMEOUT_MS !== undefined ? { timeoutMs: smtpTimeout(Number(process.env.MAIL_TIMEOUT_MS)) } : {},
|
|
10653
10679
|
...process.env.MAIL_USERNAME?.trim() ? { username: process.env.MAIL_USERNAME.trim() } : {},
|
|
10654
10680
|
...process.env.MAIL_PASSWORD?.trim() ? { password: process.env.MAIL_PASSWORD.trim() } : {}
|
|
10655
10681
|
};
|
|
@@ -10659,6 +10685,12 @@ function assertSafeSmtpField(value, field) {
|
|
|
10659
10685
|
if (/[\r\n]/u.test(value)) {
|
|
10660
10686
|
throw new Error(`Mail ${field} must not contain CR or LF.`);
|
|
10661
10687
|
}
|
|
10688
|
+
for (const character of value) {
|
|
10689
|
+
const code = character.charCodeAt(0);
|
|
10690
|
+
if (code < 32 && code !== 9 || code === 127) {
|
|
10691
|
+
throw new Error(`Mail ${field} must not contain control characters.`);
|
|
10692
|
+
}
|
|
10693
|
+
}
|
|
10662
10694
|
return value;
|
|
10663
10695
|
}
|
|
10664
10696
|
function smtpEnvelopeAddress(value, field) {
|
|
@@ -10679,148 +10711,307 @@ function redactMailBody(body) {
|
|
|
10679
10711
|
function encodeBase64(value) {
|
|
10680
10712
|
return Buffer.from(value, "utf8").toString("base64");
|
|
10681
10713
|
}
|
|
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);
|
|
10714
|
+
function smtpTimeout(value = 1e4) {
|
|
10715
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 2147483647) {
|
|
10716
|
+
throw new RangeError("SMTP timeoutMs must be a positive integer within the timer range.");
|
|
10695
10717
|
}
|
|
10696
|
-
return
|
|
10718
|
+
return value;
|
|
10697
10719
|
}
|
|
10698
|
-
async function
|
|
10699
|
-
const
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
throw new Error(`Unexpected SMTP response: ${response}`);
|
|
10720
|
+
async function defaultSmtpTransport(config, message) {
|
|
10721
|
+
const timeoutMs = smtpTimeout(config.timeoutMs);
|
|
10722
|
+
if (!config.host.trim() || !Number.isInteger(config.port) || config.port < 1 || config.port > 65535) {
|
|
10723
|
+
throw new RangeError("SMTP requires a host and a port between 1 and 65535.");
|
|
10703
10724
|
}
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10725
|
+
if (Boolean(config.username) !== Boolean(config.password)) {
|
|
10726
|
+
throw new Error("SMTP authentication requires both username and password.");
|
|
10727
|
+
}
|
|
10728
|
+
if (config.username)
|
|
10729
|
+
assertSafeSmtpField(config.username, "username");
|
|
10730
|
+
if (config.password)
|
|
10731
|
+
assertSafeSmtpField(config.password, "password");
|
|
10732
|
+
const fromHeader = assertSafeSmtpAddress(config.from, "from");
|
|
10733
|
+
const toHeader = assertSafeSmtpAddress(message.to, "to");
|
|
10734
|
+
assertSafeSmtpField(message.subject, "subject");
|
|
10735
|
+
const fromEnvelope = smtpEnvelopeAddress(config.from, "from");
|
|
10736
|
+
const toEnvelope = smtpEnvelopeAddress(message.to, "to");
|
|
10737
|
+
if (/[^\x21-\x7e]/u.test(fromEnvelope + toEnvelope)) {
|
|
10738
|
+
throw new Error("Default SMTP transport requires ASCII envelopes; SMTPUTF8 is not negotiated.");
|
|
10739
|
+
}
|
|
10740
|
+
for (const address of [fromEnvelope, toEnvelope]) {
|
|
10741
|
+
const at = address.lastIndexOf("@");
|
|
10742
|
+
if (Buffer.byteLength(address) > 254 || Buffer.byteLength(address.slice(0, at)) > 64) {
|
|
10743
|
+
throw new RangeError("SMTP envelope address exceeds the path/local-part length limit.");
|
|
10744
|
+
}
|
|
10745
|
+
}
|
|
10746
|
+
message.signal?.throwIfAborted();
|
|
10747
|
+
let socket;
|
|
10748
|
+
let finished = false;
|
|
10749
|
+
let failed = false;
|
|
10750
|
+
let failure;
|
|
10751
|
+
let rejectFailure;
|
|
10752
|
+
const failurePromise = new Promise((_, reject) => {
|
|
10753
|
+
rejectFailure = reject;
|
|
10754
|
+
});
|
|
10755
|
+
failurePromise.catch(() => {});
|
|
10756
|
+
const responses = [];
|
|
10708
10757
|
const waiters = [];
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10758
|
+
let drain;
|
|
10759
|
+
let drainVersion = 0;
|
|
10760
|
+
let buffer = "";
|
|
10761
|
+
let multilineCode;
|
|
10762
|
+
let multilineLines = 0;
|
|
10763
|
+
const decoder = new TextDecoder;
|
|
10764
|
+
const fail = (reason) => {
|
|
10765
|
+
if (finished || failed)
|
|
10714
10766
|
return;
|
|
10767
|
+
failed = true;
|
|
10768
|
+
failure = reason;
|
|
10769
|
+
rejectFailure(reason);
|
|
10770
|
+
waiters.length = 0;
|
|
10771
|
+
drain?.();
|
|
10772
|
+
drain = undefined;
|
|
10773
|
+
socket?.terminate();
|
|
10774
|
+
};
|
|
10775
|
+
const onAbort = () => fail(message.signal?.reason ?? new DOMException("Mail send aborted.", "AbortError"));
|
|
10776
|
+
const timer = setTimeout(() => fail(new DOMException("SMTP send deadline exceeded.", "TimeoutError")), timeoutMs);
|
|
10777
|
+
message.signal?.addEventListener("abort", onAbort, { once: true });
|
|
10778
|
+
if (message.signal?.aborted)
|
|
10779
|
+
onAbort();
|
|
10780
|
+
const readResponse = async (expected) => {
|
|
10781
|
+
const queued = responses.shift();
|
|
10782
|
+
const response = queued ?? await Promise.race([
|
|
10783
|
+
failed ? Promise.reject(failure) : new Promise((resolve) => waiters.push(resolve)),
|
|
10784
|
+
failurePromise
|
|
10785
|
+
]);
|
|
10786
|
+
const code = response.slice(0, 3);
|
|
10787
|
+
if (!expected.includes(code))
|
|
10788
|
+
throw new Error(`Unexpected SMTP response code: ${code}`);
|
|
10789
|
+
};
|
|
10790
|
+
const write = async (text) => {
|
|
10791
|
+
const bytes = Buffer.from(text, "utf8");
|
|
10792
|
+
let offset = 0;
|
|
10793
|
+
while (offset < bytes.length) {
|
|
10794
|
+
if (failed)
|
|
10795
|
+
throw failure;
|
|
10796
|
+
const version = drainVersion;
|
|
10797
|
+
if (!socket)
|
|
10798
|
+
throw new Error("SMTP socket is not connected.");
|
|
10799
|
+
const written = socket.write(bytes.subarray(offset));
|
|
10800
|
+
if (written < 0)
|
|
10801
|
+
throw new Error("SMTP socket write failed.");
|
|
10802
|
+
offset += written;
|
|
10803
|
+
if (offset < bytes.length && version === drainVersion) {
|
|
10804
|
+
await Promise.race([
|
|
10805
|
+
new Promise((resolve) => {
|
|
10806
|
+
drain = resolve;
|
|
10807
|
+
}),
|
|
10808
|
+
failurePromise
|
|
10809
|
+
]);
|
|
10810
|
+
}
|
|
10715
10811
|
}
|
|
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);
|
|
10812
|
+
};
|
|
10813
|
+
try {
|
|
10814
|
+
const connecting = Bun.connect({
|
|
10815
|
+
hostname: config.host,
|
|
10816
|
+
port: config.port,
|
|
10817
|
+
...config.secure ? {
|
|
10818
|
+
tls: {
|
|
10819
|
+
...config.tls,
|
|
10820
|
+
serverName: config.tls?.serverName ?? config.host,
|
|
10821
|
+
rejectUnauthorized: true
|
|
10730
10822
|
}
|
|
10731
|
-
},
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10823
|
+
} : {},
|
|
10824
|
+
socket: {
|
|
10825
|
+
open(value) {
|
|
10826
|
+
socket = value;
|
|
10827
|
+
if (failed || finished)
|
|
10828
|
+
value.terminate();
|
|
10829
|
+
},
|
|
10830
|
+
data(_socket, chunk) {
|
|
10831
|
+
if (failed || finished)
|
|
10832
|
+
return;
|
|
10833
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
10834
|
+
if (Buffer.byteLength(buffer, "utf8") > 16384) {
|
|
10835
|
+
fail(new Error("SMTP response exceeded the buffer limit."));
|
|
10836
|
+
return;
|
|
10837
|
+
}
|
|
10838
|
+
while (buffer.includes(`\r
|
|
10839
|
+
`)) {
|
|
10840
|
+
const index = buffer.indexOf(`\r
|
|
10841
|
+
`);
|
|
10842
|
+
const line = buffer.slice(0, index);
|
|
10843
|
+
buffer = buffer.slice(index + 2);
|
|
10844
|
+
const match = /^([2-5]\d{2})([- ]|$)/.exec(line);
|
|
10845
|
+
if (!match || multilineCode !== undefined && match[1] !== multilineCode) {
|
|
10846
|
+
fail(new Error("Malformed SMTP reply."));
|
|
10847
|
+
return;
|
|
10848
|
+
}
|
|
10849
|
+
if (++multilineLines > 128) {
|
|
10850
|
+
fail(new Error("SMTP reply exceeded the line limit."));
|
|
10851
|
+
return;
|
|
10852
|
+
}
|
|
10853
|
+
if (match[2] === "-") {
|
|
10854
|
+
multilineCode = match[1];
|
|
10855
|
+
continue;
|
|
10856
|
+
}
|
|
10857
|
+
multilineCode = undefined;
|
|
10858
|
+
multilineLines = 0;
|
|
10859
|
+
const waiter = waiters.shift();
|
|
10860
|
+
if (waiter)
|
|
10861
|
+
waiter(line);
|
|
10862
|
+
else {
|
|
10863
|
+
responses.push(line);
|
|
10864
|
+
if (responses.length > 32) {
|
|
10865
|
+
fail(new Error("Too many buffered SMTP replies."));
|
|
10866
|
+
return;
|
|
10867
|
+
}
|
|
10868
|
+
}
|
|
10869
|
+
}
|
|
10870
|
+
},
|
|
10871
|
+
drain() {
|
|
10872
|
+
drainVersion++;
|
|
10873
|
+
const waiting = drain;
|
|
10874
|
+
drain = undefined;
|
|
10875
|
+
waiting?.();
|
|
10876
|
+
},
|
|
10877
|
+
end() {
|
|
10878
|
+
fail(new Error("SMTP peer closed before the send completed."));
|
|
10879
|
+
},
|
|
10880
|
+
close() {
|
|
10881
|
+
fail(new Error("SMTP connection closed before the send completed."));
|
|
10882
|
+
},
|
|
10883
|
+
error() {
|
|
10884
|
+
fail(new Error("SMTP socket failed."));
|
|
10885
|
+
},
|
|
10886
|
+
connectError() {
|
|
10887
|
+
fail(new Error("SMTP connection failed."));
|
|
10888
|
+
},
|
|
10889
|
+
handshake(_socket, success) {
|
|
10890
|
+
if (!success)
|
|
10891
|
+
fail(new Error("SMTP TLS verification failed."));
|
|
10736
10892
|
}
|
|
10737
10893
|
}
|
|
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
|
|
10894
|
+
});
|
|
10895
|
+
connecting.then((value) => {
|
|
10896
|
+
socket = value;
|
|
10897
|
+
if (failed || finished)
|
|
10898
|
+
value.terminate();
|
|
10899
|
+
}, () => {});
|
|
10900
|
+
socket = await Promise.race([connecting, failurePromise]);
|
|
10901
|
+
await readResponse(["220"]);
|
|
10902
|
+
await write(`EHLO ${smtpEhloHost()}\r
|
|
10753
10903
|
`);
|
|
10754
|
-
await
|
|
10904
|
+
await readResponse(["250"]);
|
|
10755
10905
|
if (config.username && config.password) {
|
|
10756
|
-
await
|
|
10906
|
+
await write(`AUTH LOGIN\r
|
|
10757
10907
|
`);
|
|
10758
|
-
await
|
|
10759
|
-
await
|
|
10908
|
+
await readResponse(["334"]);
|
|
10909
|
+
await write(`${encodeBase64(config.username)}\r
|
|
10760
10910
|
`);
|
|
10761
|
-
await
|
|
10762
|
-
await
|
|
10911
|
+
await readResponse(["334"]);
|
|
10912
|
+
await write(`${encodeBase64(config.password)}\r
|
|
10763
10913
|
`);
|
|
10764
|
-
await
|
|
10914
|
+
await readResponse(["235"]);
|
|
10765
10915
|
}
|
|
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
|
|
10916
|
+
await write(`MAIL FROM:<${fromEnvelope}>\r
|
|
10771
10917
|
`);
|
|
10772
|
-
await
|
|
10773
|
-
await
|
|
10918
|
+
await readResponse(["250"]);
|
|
10919
|
+
await write(`RCPT TO:<${toEnvelope}>\r
|
|
10774
10920
|
`);
|
|
10775
|
-
await
|
|
10776
|
-
await
|
|
10921
|
+
await readResponse(["250", "251"]);
|
|
10922
|
+
await write(`DATA\r
|
|
10777
10923
|
`);
|
|
10778
|
-
await
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
await socket.write(payload);
|
|
10785
|
-
await waitForSmtpResponse(readResponse, ["250"]);
|
|
10786
|
-
await socket.write(`QUIT\r
|
|
10924
|
+
await readResponse(["354"]);
|
|
10925
|
+
await write(buildSmtpPayload(fromHeader, { ...message, to: toHeader }));
|
|
10926
|
+
await readResponse(["250"]);
|
|
10927
|
+
finished = true;
|
|
10928
|
+
try {
|
|
10929
|
+
socket.write(`QUIT\r
|
|
10787
10930
|
`);
|
|
10788
|
-
|
|
10931
|
+
} catch {}
|
|
10789
10932
|
} finally {
|
|
10790
|
-
|
|
10933
|
+
finished = true;
|
|
10934
|
+
clearTimeout(timer);
|
|
10935
|
+
message.signal?.removeEventListener("abort", onAbort);
|
|
10936
|
+
waiters.length = 0;
|
|
10937
|
+
responses.length = 0;
|
|
10938
|
+
drain = undefined;
|
|
10939
|
+
socket?.end();
|
|
10791
10940
|
}
|
|
10792
10941
|
}
|
|
10942
|
+
function encodeSmtpHeader(value) {
|
|
10943
|
+
if (/^[\x20-\x7e]{0,60}$/u.test(value))
|
|
10944
|
+
return value;
|
|
10945
|
+
const words = [];
|
|
10946
|
+
let part = "";
|
|
10947
|
+
for (const character of value) {
|
|
10948
|
+
if (Buffer.byteLength(part + character, "utf8") > 45) {
|
|
10949
|
+
words.push(`=?UTF-8?B?${encodeBase64(part)}?=`);
|
|
10950
|
+
part = "";
|
|
10951
|
+
}
|
|
10952
|
+
part += character;
|
|
10953
|
+
}
|
|
10954
|
+
if (part)
|
|
10955
|
+
words.push(`=?UTF-8?B?${encodeBase64(part)}?=`);
|
|
10956
|
+
return words.join(`\r
|
|
10957
|
+
`);
|
|
10958
|
+
}
|
|
10959
|
+
function smtpAddressHeader(value) {
|
|
10960
|
+
if (!value.includes("<"))
|
|
10961
|
+
return value;
|
|
10962
|
+
const name = value.slice(0, value.lastIndexOf("<")).trim();
|
|
10963
|
+
const address = smtpEnvelopeAddress(value, "address");
|
|
10964
|
+
if (!name)
|
|
10965
|
+
return address;
|
|
10966
|
+
const encoded = encodeSmtpHeader(name);
|
|
10967
|
+
const display = encoded.startsWith("=?UTF-8?B?") ? encoded : `"${encoded.replace(/[\\"]/gu, "\\$&")}"`;
|
|
10968
|
+
return `${display} <${address}>`;
|
|
10969
|
+
}
|
|
10970
|
+
function smtpMimePart(type, value) {
|
|
10971
|
+
const normalized = value.replace(/\r\n|\r|\n/gu, `\r
|
|
10972
|
+
`);
|
|
10973
|
+
const base64 = /[^\t\r\n\x20-\x7e]/u.test(normalized) || normalized.split(`\r
|
|
10974
|
+
`).some((line) => line.length > 998);
|
|
10975
|
+
return [
|
|
10976
|
+
`Content-Type: ${type}; charset=utf-8`,
|
|
10977
|
+
`Content-Transfer-Encoding: ${base64 ? "base64" : "7bit"}`,
|
|
10978
|
+
"",
|
|
10979
|
+
base64 ? (encodeBase64(normalized).match(/.{1,76}/gu) ?? []).join(`\r
|
|
10980
|
+
`) : normalized
|
|
10981
|
+
];
|
|
10982
|
+
}
|
|
10793
10983
|
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
10984
|
const headers = [
|
|
10798
|
-
`From: ${
|
|
10799
|
-
`To: ${
|
|
10800
|
-
`Subject: ${
|
|
10985
|
+
`From: ${smtpAddressHeader(assertSafeSmtpAddress(from, "from"))}`,
|
|
10986
|
+
`To: ${smtpAddressHeader(assertSafeSmtpAddress(message.to, "to"))}`,
|
|
10987
|
+
`Subject: ${encodeSmtpHeader(assertSafeSmtpField(message.subject, "subject"))}`,
|
|
10801
10988
|
"MIME-Version: 1.0"
|
|
10802
10989
|
];
|
|
10990
|
+
let lines;
|
|
10803
10991
|
if (message.html) {
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10992
|
+
let boundary;
|
|
10993
|
+
do {
|
|
10994
|
+
boundary = `strata-${crypto.randomUUID()}`;
|
|
10995
|
+
} while (message.body.includes(boundary) || message.html.includes(boundary));
|
|
10996
|
+
lines = [
|
|
10997
|
+
...headers,
|
|
10998
|
+
`Content-Type: multipart/alternative; boundary="${boundary}"`,
|
|
10809
10999
|
"",
|
|
10810
|
-
message.body,
|
|
10811
11000
|
`--${boundary}`,
|
|
10812
|
-
"
|
|
10813
|
-
|
|
10814
|
-
message.html,
|
|
11001
|
+
...smtpMimePart("text/plain", message.body),
|
|
11002
|
+
`--${boundary}`,
|
|
11003
|
+
...smtpMimePart("text/html", message.html),
|
|
10815
11004
|
`--${boundary}--`,
|
|
10816
11005
|
""
|
|
10817
11006
|
];
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10821
|
-
|
|
10822
|
-
return
|
|
10823
|
-
`)
|
|
11007
|
+
} else
|
|
11008
|
+
lines = [...headers, ...smtpMimePart("text/plain", message.body)];
|
|
11009
|
+
const content = lines.join(`\r
|
|
11010
|
+
`).replace(/(^|\r\n)\./gu, "$1..");
|
|
11011
|
+
return `${content}${content.endsWith(`\r
|
|
11012
|
+
`) ? "" : `\r
|
|
11013
|
+
`}.\r
|
|
11014
|
+
`;
|
|
10824
11015
|
}
|
|
10825
11016
|
|
|
10826
11017
|
class LogMailDriver {
|