@upyo/smtp 0.5.2 → 0.5.4
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 +182 -38
- package/dist/index.d.cts +38 -3
- package/dist/index.d.ts +38 -3
- package/dist/index.js +182 -38
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -592,6 +592,12 @@ var SmtpConnection = class {
|
|
|
592
592
|
}
|
|
593
593
|
async ehlo(signal) {
|
|
594
594
|
const response = await this.sendCommand(`EHLO ${this.config.localName}`, signal);
|
|
595
|
+
if (response.code === 500 || response.code === 502) {
|
|
596
|
+
const heloResponse = await this.sendCommand(`HELO ${this.config.localName}`, signal);
|
|
597
|
+
if (heloResponse.code !== 250) throw new Error(`HELO failed: ${heloResponse.message}`);
|
|
598
|
+
this.capabilities = [];
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
595
601
|
if (response.code !== 250) throw new Error(`EHLO failed: ${response.message}`);
|
|
596
602
|
this.capabilities = response.raw.split("\r\n").filter((line) => line.startsWith("250-") || line.startsWith("250 ")).map((line) => line.substring(4)).filter((line) => line.length > 0);
|
|
597
603
|
}
|
|
@@ -748,10 +754,22 @@ var SmtpConnection = class {
|
|
|
748
754
|
async sendMessage(message, signal) {
|
|
749
755
|
const mailResponse = await this.sendCommand(`MAIL FROM:<${message.envelope.from}>`, signal);
|
|
750
756
|
if (mailResponse.code !== 250) throw new SmtpResponseError(`MAIL FROM failed: ${mailResponse.message}`, mailResponse.code, "MAIL FROM", mailResponse.message);
|
|
757
|
+
const rejectedRecipients = [];
|
|
751
758
|
for (const recipient of message.envelope.to) {
|
|
752
759
|
signal?.throwIfAborted();
|
|
753
760
|
const rcptResponse = await this.sendCommand(`RCPT TO:<${recipient}>`, signal);
|
|
754
|
-
if (rcptResponse.code
|
|
761
|
+
if (rcptResponse.code === 421) throw new SmtpResponseError(`RCPT TO failed for ${recipient}: ${rcptResponse.message}`, rcptResponse.code, "RCPT TO", rcptResponse.message);
|
|
762
|
+
if (rcptResponse.code !== 250 && rcptResponse.code !== 251) rejectedRecipients.push({
|
|
763
|
+
recipient,
|
|
764
|
+
code: rcptResponse.code,
|
|
765
|
+
response: rcptResponse.message,
|
|
766
|
+
retryable: rcptResponse.code >= 400 && rcptResponse.code < 500
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
if (rejectedRecipients.length > 0 && rejectedRecipients.length === message.envelope.to.length) {
|
|
770
|
+
const rejection = rejectedRecipients.find((item) => item.retryable) ?? rejectedRecipients[0];
|
|
771
|
+
const details = rejectedRecipients.map((item) => `${item.recipient}: ${item.code} ${item.response}`).join("; ");
|
|
772
|
+
throw new SmtpResponseError(`RCPT TO failed for every recipient: ${details}`, rejection.code, "RCPT TO", rejection.response, rejectedRecipients);
|
|
755
773
|
}
|
|
756
774
|
const dataResponse = await this.sendCommand("DATA", signal);
|
|
757
775
|
if (dataResponse.code !== 354) throw new SmtpResponseError(`DATA failed: ${dataResponse.message}`, dataResponse.code, "DATA", dataResponse.message);
|
|
@@ -759,7 +777,10 @@ var SmtpConnection = class {
|
|
|
759
777
|
const finalResponse = await this.sendCommand(`${content}\r\n.`, signal);
|
|
760
778
|
if (finalResponse.code !== 250) throw new SmtpResponseError(`Message send failed: ${finalResponse.message}`, finalResponse.code, "DATA_END", finalResponse.message);
|
|
761
779
|
const messageId = this.extractMessageId(finalResponse.message);
|
|
762
|
-
return
|
|
780
|
+
return {
|
|
781
|
+
messageId,
|
|
782
|
+
rejectedRecipients
|
|
783
|
+
};
|
|
763
784
|
}
|
|
764
785
|
extractMessageId(response) {
|
|
765
786
|
const match = response.match(/(?:Message-ID:|id=)[\s<]*([^>\s]+)/i);
|
|
@@ -815,6 +836,8 @@ var SmtpResponseError = class extends Error {
|
|
|
815
836
|
* The textual SMTP reply returned by the server.
|
|
816
837
|
*/
|
|
817
838
|
response;
|
|
839
|
+
/** Recipient-level failures collected for an unsuccessful transaction. */
|
|
840
|
+
rejectedRecipients;
|
|
818
841
|
/**
|
|
819
842
|
* Creates an SMTP response error.
|
|
820
843
|
*
|
|
@@ -822,13 +845,16 @@ var SmtpResponseError = class extends Error {
|
|
|
822
845
|
* @param code The numeric SMTP reply code.
|
|
823
846
|
* @param command The SMTP command that produced the reply.
|
|
824
847
|
* @param response The textual SMTP reply returned by the server.
|
|
848
|
+
* @param rejectedRecipients Recipient-level failures collected for the
|
|
849
|
+
* transaction.
|
|
825
850
|
*/
|
|
826
|
-
constructor(message, code, command, response) {
|
|
851
|
+
constructor(message, code, command, response, rejectedRecipients) {
|
|
827
852
|
super(message);
|
|
828
853
|
this.name = "SmtpResponseError";
|
|
829
854
|
this.code = code;
|
|
830
855
|
this.command = command;
|
|
831
856
|
this.response = response;
|
|
857
|
+
this.rejectedRecipients = rejectedRecipients;
|
|
832
858
|
}
|
|
833
859
|
};
|
|
834
860
|
/**
|
|
@@ -1130,6 +1156,17 @@ function arrayBufferToBase64(buffer) {
|
|
|
1130
1156
|
|
|
1131
1157
|
//#endregion
|
|
1132
1158
|
//#region src/message-converter.ts
|
|
1159
|
+
/**
|
|
1160
|
+
* Converts a message to its SMTP envelope and wire representation.
|
|
1161
|
+
*
|
|
1162
|
+
* @param message The message to convert.
|
|
1163
|
+
* @param dkimConfig Optional DKIM signing configuration.
|
|
1164
|
+
* @returns The converted SMTP message.
|
|
1165
|
+
* @throws {RangeError} If a header contains a token that cannot be folded
|
|
1166
|
+
* within the RFC 5322 hard line-length limit.
|
|
1167
|
+
* @throws {TypeError} If a `Date` or `Message-ID` header supplied by the
|
|
1168
|
+
* message contains a carriage return or line feed.
|
|
1169
|
+
*/
|
|
1133
1170
|
async function convertMessage(message, dkimConfig) {
|
|
1134
1171
|
const envelope = {
|
|
1135
1172
|
from: message.sender.address,
|
|
@@ -1161,19 +1198,34 @@ async function buildRawMessage(message) {
|
|
|
1161
1198
|
const hasHtml = "html" in message.content;
|
|
1162
1199
|
const hasText = "text" in message.content;
|
|
1163
1200
|
const isMultipart = hasAttachments || hasHtml && hasText;
|
|
1164
|
-
lines.push(
|
|
1165
|
-
lines.push(
|
|
1166
|
-
if (message.ccRecipients.length > 0) lines.push(
|
|
1167
|
-
if (message.replyRecipients.length > 0) lines.push(
|
|
1168
|
-
lines.push(
|
|
1169
|
-
lines.push(
|
|
1170
|
-
lines.push(
|
|
1201
|
+
lines.push(foldHeader("From", encodeAddress(message.sender)));
|
|
1202
|
+
lines.push(foldHeader("To", message.recipients.map(encodeAddress).join(", ")));
|
|
1203
|
+
if (message.ccRecipients.length > 0) lines.push(foldHeader("Cc", message.ccRecipients.map(encodeAddress).join(", ")));
|
|
1204
|
+
if (message.replyRecipients.length > 0) lines.push(foldHeader("Reply-To", message.replyRecipients.map(encodeAddress).join(", ")));
|
|
1205
|
+
lines.push(foldHeader("Subject", encodeHeaderValue(message.subject, true)));
|
|
1206
|
+
lines.push(foldHeader("Date", overridden(message, "Date") ?? (/* @__PURE__ */ new Date()).toUTCString()));
|
|
1207
|
+
lines.push(foldHeader("Message-ID", overridden(message, "Message-ID") ?? `<${generateMessageId()}>`));
|
|
1208
|
+
const composed = new Set([
|
|
1209
|
+
"from",
|
|
1210
|
+
"to",
|
|
1211
|
+
"cc",
|
|
1212
|
+
"reply-to",
|
|
1213
|
+
"subject",
|
|
1214
|
+
"date",
|
|
1215
|
+
"message-id"
|
|
1216
|
+
]);
|
|
1171
1217
|
if (message.priority !== "normal") {
|
|
1172
1218
|
const priorityValue = message.priority === "high" ? "1" : "5";
|
|
1173
1219
|
lines.push(`X-Priority: ${priorityValue}`);
|
|
1174
1220
|
lines.push(`X-MSMail-Priority: ${message.priority === "high" ? "High" : "Low"}`);
|
|
1221
|
+
composed.add("x-priority");
|
|
1222
|
+
composed.add("x-msmail-priority");
|
|
1223
|
+
}
|
|
1224
|
+
for (const [key, value] of message.headers) {
|
|
1225
|
+
const name = key.toLowerCase();
|
|
1226
|
+
if (composed.has(name) || reservedHeaders.has(name)) continue;
|
|
1227
|
+
lines.push(foldHeader(key, encodeHeaderValue(value)));
|
|
1175
1228
|
}
|
|
1176
|
-
for (const [key, value] of message.headers) lines.push(`${key}: ${encodeHeaderValue(value)}`);
|
|
1177
1229
|
lines.push("MIME-Version: 1.0");
|
|
1178
1230
|
if (isMultipart) {
|
|
1179
1231
|
lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
@@ -1212,12 +1264,12 @@ async function buildRawMessage(message) {
|
|
|
1212
1264
|
for (const attachment of message.attachments) {
|
|
1213
1265
|
lines.push("");
|
|
1214
1266
|
lines.push(`--${boundary}`);
|
|
1215
|
-
lines.push(
|
|
1267
|
+
lines.push(foldHeader("Content-Type", `${attachment.contentType}; ${encodeMimeParameter("name", attachment.filename)}`));
|
|
1216
1268
|
lines.push("Content-Transfer-Encoding: base64");
|
|
1217
1269
|
if (attachment.inline) {
|
|
1218
|
-
lines.push(
|
|
1270
|
+
lines.push(foldHeader("Content-Disposition", `inline; ${encodeMimeParameter("filename", attachment.filename)}`));
|
|
1219
1271
|
lines.push(`Content-ID: <${attachment.contentId}>`);
|
|
1220
|
-
} else lines.push(
|
|
1272
|
+
} else lines.push(foldHeader("Content-Disposition", `attachment; ${encodeMimeParameter("filename", attachment.filename)}`));
|
|
1221
1273
|
lines.push("");
|
|
1222
1274
|
lines.push(encodeBase64(await attachment.content));
|
|
1223
1275
|
}
|
|
@@ -1236,6 +1288,41 @@ async function buildRawMessage(message) {
|
|
|
1236
1288
|
}
|
|
1237
1289
|
return lines.join("\r\n");
|
|
1238
1290
|
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Reads a header field that overrides a generated default.
|
|
1293
|
+
*
|
|
1294
|
+
* Unlike custom headers, the value is written verbatim rather than RFC 2047
|
|
1295
|
+
* encoded, so it cannot rely on `encodeHeaderValue()` to neutralize control
|
|
1296
|
+
* characters. `ImmutableHeaders` is a structural type, so a `Message` may
|
|
1297
|
+
* carry an adapter that never rejected CR or LF the way a platform `Headers`
|
|
1298
|
+
* does.
|
|
1299
|
+
*
|
|
1300
|
+
* @param message The message whose headers are read.
|
|
1301
|
+
* @param name The header field name.
|
|
1302
|
+
* @returns The supplied value, or `undefined` when the message has none.
|
|
1303
|
+
* @throws {TypeError} If the value contains a carriage return or line feed,
|
|
1304
|
+
* which would inject additional header fields into the message.
|
|
1305
|
+
*/
|
|
1306
|
+
function overridden(message, name) {
|
|
1307
|
+
const value = message.headers.get(name);
|
|
1308
|
+
if (value == null) return void 0;
|
|
1309
|
+
if (/[\r\n]/.test(value)) throw new TypeError(`Header field ${name} must not contain a carriage return or line feed.`);
|
|
1310
|
+
return value;
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Header fields the composer owns but writes after the custom headers, or
|
|
1314
|
+
* deliberately omits. A custom header with one of these names is dropped
|
|
1315
|
+
* rather than appended: the structured `Message` fields are authoritative,
|
|
1316
|
+
* RFC 5322 §3.6 allows at most one of each, and a duplicate placed before the
|
|
1317
|
+
* composer's own field makes parsers that take the first occurrence read the
|
|
1318
|
+
* wrong value.
|
|
1319
|
+
*/
|
|
1320
|
+
const reservedHeaders = new Set([
|
|
1321
|
+
"bcc",
|
|
1322
|
+
"content-transfer-encoding",
|
|
1323
|
+
"content-type",
|
|
1324
|
+
"mime-version"
|
|
1325
|
+
]);
|
|
1239
1326
|
function generateBoundary() {
|
|
1240
1327
|
return `boundary-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1241
1328
|
}
|
|
@@ -1246,32 +1333,85 @@ function generateMessageId() {
|
|
|
1246
1333
|
}
|
|
1247
1334
|
function encodeAddress(address) {
|
|
1248
1335
|
if (address.name == null) return address.address;
|
|
1249
|
-
const encodedDisplayName = encodeHeaderValue(address.name);
|
|
1336
|
+
const encodedDisplayName = encodeHeaderValue(address.name, true);
|
|
1250
1337
|
return `${encodedDisplayName} <${address.address}>`;
|
|
1251
1338
|
}
|
|
1252
|
-
function encodeHeaderValue(value) {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
const
|
|
1339
|
+
function encodeHeaderValue(value, encodeLongAsciiWords = false) {
|
|
1340
|
+
const hasLongWord = value.split(/\s+/).some((word) => word.length > 60);
|
|
1341
|
+
if (!/^[\x20-\x7E]*$/.test(value) || encodeLongAsciiWords && hasLongWord) {
|
|
1342
|
+
const encodeWord = (text) => {
|
|
1343
|
+
const utf8Bytes = new TextEncoder().encode(text);
|
|
1344
|
+
const base64 = node_buffer.Buffer.from(utf8Bytes).toString("base64");
|
|
1345
|
+
return `=?UTF-8?B?${base64}?=`;
|
|
1346
|
+
};
|
|
1256
1347
|
const maxEncodedLength = 75;
|
|
1257
|
-
const encodedWord =
|
|
1348
|
+
const encodedWord = encodeWord(value);
|
|
1258
1349
|
if (encodedWord.length <= maxEncodedLength) return encodedWord;
|
|
1259
1350
|
const words = [];
|
|
1260
|
-
let
|
|
1261
|
-
for (
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
if (testWord.length <= maxEncodedLength) currentBase64 += chunk;
|
|
1351
|
+
let currentText = "";
|
|
1352
|
+
for (const character of value) {
|
|
1353
|
+
const candidate = currentText + character;
|
|
1354
|
+
if (encodeWord(candidate).length <= maxEncodedLength) currentText = candidate;
|
|
1265
1355
|
else {
|
|
1266
|
-
if (
|
|
1267
|
-
|
|
1356
|
+
if (currentText.length > 0) words.push(encodeWord(currentText));
|
|
1357
|
+
currentText = character;
|
|
1268
1358
|
}
|
|
1269
1359
|
}
|
|
1270
|
-
if (
|
|
1360
|
+
if (currentText.length > 0) words.push(encodeWord(currentText));
|
|
1271
1361
|
return words.join(" ");
|
|
1272
1362
|
}
|
|
1273
1363
|
return value;
|
|
1274
1364
|
}
|
|
1365
|
+
function encodeMimeParameter(name, value) {
|
|
1366
|
+
const escapedValue = value.replace(/[\\"]/g, "\\$&");
|
|
1367
|
+
const quotedParameter = `${name}="${escapedValue}"`;
|
|
1368
|
+
if (/^[\x20-\x7E]*$/.test(value) && quotedParameter.length <= 60) return quotedParameter;
|
|
1369
|
+
const encodedBytes = Array.from(new TextEncoder().encode(value), (byte) => {
|
|
1370
|
+
const character = String.fromCharCode(byte);
|
|
1371
|
+
return /^[A-Za-z0-9!#$&+.^_`|~-]$/.test(character) ? character : `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
1372
|
+
});
|
|
1373
|
+
const segments = [];
|
|
1374
|
+
let segment = "";
|
|
1375
|
+
for (const encodedByte of encodedBytes) {
|
|
1376
|
+
if (segment.length + encodedByte.length > 45) {
|
|
1377
|
+
segments.push(segment);
|
|
1378
|
+
segment = "";
|
|
1379
|
+
}
|
|
1380
|
+
segment += encodedByte;
|
|
1381
|
+
}
|
|
1382
|
+
if (segment.length > 0 || segments.length === 0) segments.push(segment);
|
|
1383
|
+
return segments.map((part, index) => `${name}*${index}*=${index === 0 ? "UTF-8''" : ""}${part}`).join("; ");
|
|
1384
|
+
}
|
|
1385
|
+
function foldHeader(name, value) {
|
|
1386
|
+
const recommendedLineLength = 78;
|
|
1387
|
+
const lines = [];
|
|
1388
|
+
let prefix = `${name}: `;
|
|
1389
|
+
let remaining = value;
|
|
1390
|
+
while (prefix.length + remaining.length > recommendedLineLength) {
|
|
1391
|
+
const availableLength = recommendedLineLength - prefix.length;
|
|
1392
|
+
let breakIndex = -1;
|
|
1393
|
+
for (let index = Math.min(availableLength, remaining.length - 1); index >= 0; index--) if (remaining[index] === " " || remaining[index] === " ") {
|
|
1394
|
+
breakIndex = index;
|
|
1395
|
+
break;
|
|
1396
|
+
}
|
|
1397
|
+
if (breakIndex < 0) {
|
|
1398
|
+
for (let index = Math.max(availableLength + 1, 0); index < remaining.length; index++) if (remaining[index] === " " || remaining[index] === " ") {
|
|
1399
|
+
breakIndex = index;
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (breakIndex < 0) break;
|
|
1404
|
+
let whitespaceEnd = breakIndex + 1;
|
|
1405
|
+
while (whitespaceEnd < remaining.length && (remaining[whitespaceEnd] === " " || remaining[whitespaceEnd] === " ")) whitespaceEnd++;
|
|
1406
|
+
if (whitespaceEnd === remaining.length) break;
|
|
1407
|
+
lines.push(prefix + remaining.slice(0, breakIndex));
|
|
1408
|
+
prefix = remaining.slice(breakIndex, whitespaceEnd);
|
|
1409
|
+
remaining = remaining.slice(whitespaceEnd);
|
|
1410
|
+
}
|
|
1411
|
+
lines.push(prefix + remaining);
|
|
1412
|
+
if (lines.some((line) => line.length > 998)) throw new RangeError(`Header field ${name} contains a token too long to fold.`);
|
|
1413
|
+
return lines.join("\r\n");
|
|
1414
|
+
}
|
|
1275
1415
|
function encodeQuotedPrintable(text) {
|
|
1276
1416
|
const utf8Bytes = new TextEncoder().encode(text);
|
|
1277
1417
|
let result = "";
|
|
@@ -1402,12 +1542,13 @@ var SmtpTransport = class {
|
|
|
1402
1542
|
options?.signal?.throwIfAborted();
|
|
1403
1543
|
const smtpMessage = await convertMessage(message, this.config.dkim);
|
|
1404
1544
|
options?.signal?.throwIfAborted();
|
|
1405
|
-
const
|
|
1545
|
+
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1406
1546
|
await this.returnConnection(connection);
|
|
1407
1547
|
return {
|
|
1408
1548
|
successful: true,
|
|
1409
|
-
messageId,
|
|
1410
|
-
provider: "smtp"
|
|
1549
|
+
messageId: result.messageId,
|
|
1550
|
+
provider: "smtp",
|
|
1551
|
+
rejectedRecipients: result.rejectedRecipients
|
|
1411
1552
|
};
|
|
1412
1553
|
} catch (error) {
|
|
1413
1554
|
if (connection != null) await this.discardConnection(connection);
|
|
@@ -1471,11 +1612,12 @@ var SmtpTransport = class {
|
|
|
1471
1612
|
try {
|
|
1472
1613
|
const smtpMessage = await convertMessage(message, this.config.dkim);
|
|
1473
1614
|
options?.signal?.throwIfAborted();
|
|
1474
|
-
const
|
|
1615
|
+
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1475
1616
|
yield {
|
|
1476
1617
|
successful: true,
|
|
1477
|
-
messageId,
|
|
1478
|
-
provider: "smtp"
|
|
1618
|
+
messageId: result.messageId,
|
|
1619
|
+
provider: "smtp",
|
|
1620
|
+
rejectedRecipients: result.rejectedRecipients
|
|
1479
1621
|
};
|
|
1480
1622
|
} catch (error) {
|
|
1481
1623
|
options?.signal?.throwIfAborted();
|
|
@@ -1492,11 +1634,12 @@ var SmtpTransport = class {
|
|
|
1492
1634
|
try {
|
|
1493
1635
|
const smtpMessage = await convertMessage(message, this.config.dkim);
|
|
1494
1636
|
options?.signal?.throwIfAborted();
|
|
1495
|
-
const
|
|
1637
|
+
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1496
1638
|
yield {
|
|
1497
1639
|
successful: true,
|
|
1498
|
-
messageId,
|
|
1499
|
-
provider: "smtp"
|
|
1640
|
+
messageId: result.messageId,
|
|
1641
|
+
provider: "smtp",
|
|
1642
|
+
rejectedRecipients: result.rejectedRecipients
|
|
1500
1643
|
};
|
|
1501
1644
|
} catch (error) {
|
|
1502
1645
|
options?.signal?.throwIfAborted();
|
|
@@ -1605,7 +1748,8 @@ function createSmtpFailure(message, error) {
|
|
|
1605
1748
|
attempts: 1,
|
|
1606
1749
|
providerDetails: {
|
|
1607
1750
|
command: error.command,
|
|
1608
|
-
response: error.response
|
|
1751
|
+
response: error.response,
|
|
1752
|
+
rejectedRecipients: error.rejectedRecipients
|
|
1609
1753
|
}
|
|
1610
1754
|
});
|
|
1611
1755
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -408,6 +408,41 @@ interface SmtpTlsOptions {
|
|
|
408
408
|
* used internally by the SMTP transport implementation.
|
|
409
409
|
*/
|
|
410
410
|
//#endregion
|
|
411
|
+
//#region src/smtp-receipt.d.ts
|
|
412
|
+
/**
|
|
413
|
+
* An SMTP envelope recipient that the server rejected during an otherwise
|
|
414
|
+
* successful delivery.
|
|
415
|
+
*
|
|
416
|
+
* @since 0.5.3
|
|
417
|
+
*/
|
|
418
|
+
interface SmtpRejectedRecipient {
|
|
419
|
+
/** The rejected recipient address. */
|
|
420
|
+
readonly recipient: string;
|
|
421
|
+
/** The three-digit SMTP reply code. */
|
|
422
|
+
readonly code: number;
|
|
423
|
+
/** The SMTP server's reply text. */
|
|
424
|
+
readonly response: string;
|
|
425
|
+
/** Whether retrying delivery to this recipient may succeed. */
|
|
426
|
+
readonly retryable: boolean;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* A receipt returned by {@link SmtpTransport}.
|
|
430
|
+
*
|
|
431
|
+
* Successful receipts list any recipients rejected before the message was
|
|
432
|
+
* delivered to the remaining accepted recipients. Callers can retry delivery
|
|
433
|
+
* to entries marked as retryable without redelivering to accepted recipients.
|
|
434
|
+
*
|
|
435
|
+
* @since 0.5.3
|
|
436
|
+
*/
|
|
437
|
+
type SmtpReceipt = (Extract<Receipt<"smtp">, {
|
|
438
|
+
readonly successful: true;
|
|
439
|
+
}> & {
|
|
440
|
+
/** Recipients excluded from an otherwise successful delivery. */
|
|
441
|
+
readonly rejectedRecipients: readonly SmtpRejectedRecipient[];
|
|
442
|
+
}) | Extract<Receipt<"smtp">, {
|
|
443
|
+
readonly successful: false;
|
|
444
|
+
}>;
|
|
445
|
+
//#endregion
|
|
411
446
|
//#region src/smtp-transport.d.ts
|
|
412
447
|
/**
|
|
413
448
|
* SMTP transport implementation for sending emails via SMTP protocol.
|
|
@@ -495,7 +530,7 @@ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
|
|
|
495
530
|
* @throws {DOMException} If the operation is aborted through
|
|
496
531
|
* `options.signal`.
|
|
497
532
|
*/
|
|
498
|
-
send(message: Message, options?: TransportOptions): Promise<
|
|
533
|
+
send(message: Message, options?: TransportOptions): Promise<SmtpReceipt>;
|
|
499
534
|
/**
|
|
500
535
|
* Sends multiple email messages efficiently using a single SMTP connection.
|
|
501
536
|
*
|
|
@@ -526,7 +561,7 @@ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
|
|
|
526
561
|
* @throws {DOMException} If the operation is aborted through
|
|
527
562
|
* `options.signal`.
|
|
528
563
|
*/
|
|
529
|
-
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<
|
|
564
|
+
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<SmtpReceipt>;
|
|
530
565
|
private getConnection;
|
|
531
566
|
private connectAndSetup;
|
|
532
567
|
private returnConnection;
|
|
@@ -591,4 +626,4 @@ declare class SmtpAuthError extends Error {
|
|
|
591
626
|
*/
|
|
592
627
|
|
|
593
628
|
//#endregion
|
|
594
|
-
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpTlsOptions, SmtpTransport, SmtpUserPassAuth };
|
|
629
|
+
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpReceipt, SmtpRejectedRecipient, SmtpTlsOptions, SmtpTransport, SmtpUserPassAuth };
|
package/dist/index.d.ts
CHANGED
|
@@ -408,6 +408,41 @@ interface SmtpTlsOptions {
|
|
|
408
408
|
* used internally by the SMTP transport implementation.
|
|
409
409
|
*/
|
|
410
410
|
//#endregion
|
|
411
|
+
//#region src/smtp-receipt.d.ts
|
|
412
|
+
/**
|
|
413
|
+
* An SMTP envelope recipient that the server rejected during an otherwise
|
|
414
|
+
* successful delivery.
|
|
415
|
+
*
|
|
416
|
+
* @since 0.5.3
|
|
417
|
+
*/
|
|
418
|
+
interface SmtpRejectedRecipient {
|
|
419
|
+
/** The rejected recipient address. */
|
|
420
|
+
readonly recipient: string;
|
|
421
|
+
/** The three-digit SMTP reply code. */
|
|
422
|
+
readonly code: number;
|
|
423
|
+
/** The SMTP server's reply text. */
|
|
424
|
+
readonly response: string;
|
|
425
|
+
/** Whether retrying delivery to this recipient may succeed. */
|
|
426
|
+
readonly retryable: boolean;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* A receipt returned by {@link SmtpTransport}.
|
|
430
|
+
*
|
|
431
|
+
* Successful receipts list any recipients rejected before the message was
|
|
432
|
+
* delivered to the remaining accepted recipients. Callers can retry delivery
|
|
433
|
+
* to entries marked as retryable without redelivering to accepted recipients.
|
|
434
|
+
*
|
|
435
|
+
* @since 0.5.3
|
|
436
|
+
*/
|
|
437
|
+
type SmtpReceipt = (Extract<Receipt<"smtp">, {
|
|
438
|
+
readonly successful: true;
|
|
439
|
+
}> & {
|
|
440
|
+
/** Recipients excluded from an otherwise successful delivery. */
|
|
441
|
+
readonly rejectedRecipients: readonly SmtpRejectedRecipient[];
|
|
442
|
+
}) | Extract<Receipt<"smtp">, {
|
|
443
|
+
readonly successful: false;
|
|
444
|
+
}>;
|
|
445
|
+
//#endregion
|
|
411
446
|
//#region src/smtp-transport.d.ts
|
|
412
447
|
/**
|
|
413
448
|
* SMTP transport implementation for sending emails via SMTP protocol.
|
|
@@ -495,7 +530,7 @@ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
|
|
|
495
530
|
* @throws {DOMException} If the operation is aborted through
|
|
496
531
|
* `options.signal`.
|
|
497
532
|
*/
|
|
498
|
-
send(message: Message, options?: TransportOptions): Promise<
|
|
533
|
+
send(message: Message, options?: TransportOptions): Promise<SmtpReceipt>;
|
|
499
534
|
/**
|
|
500
535
|
* Sends multiple email messages efficiently using a single SMTP connection.
|
|
501
536
|
*
|
|
@@ -526,7 +561,7 @@ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
|
|
|
526
561
|
* @throws {DOMException} If the operation is aborted through
|
|
527
562
|
* `options.signal`.
|
|
528
563
|
*/
|
|
529
|
-
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<
|
|
564
|
+
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<SmtpReceipt>;
|
|
530
565
|
private getConnection;
|
|
531
566
|
private connectAndSetup;
|
|
532
567
|
private returnConnection;
|
|
@@ -591,4 +626,4 @@ declare class SmtpAuthError extends Error {
|
|
|
591
626
|
*/
|
|
592
627
|
|
|
593
628
|
//#endregion
|
|
594
|
-
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpTlsOptions, SmtpTransport, SmtpUserPassAuth };
|
|
629
|
+
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpReceipt, SmtpRejectedRecipient, SmtpTlsOptions, SmtpTransport, SmtpUserPassAuth };
|
package/dist/index.js
CHANGED
|
@@ -569,6 +569,12 @@ var SmtpConnection = class {
|
|
|
569
569
|
}
|
|
570
570
|
async ehlo(signal) {
|
|
571
571
|
const response = await this.sendCommand(`EHLO ${this.config.localName}`, signal);
|
|
572
|
+
if (response.code === 500 || response.code === 502) {
|
|
573
|
+
const heloResponse = await this.sendCommand(`HELO ${this.config.localName}`, signal);
|
|
574
|
+
if (heloResponse.code !== 250) throw new Error(`HELO failed: ${heloResponse.message}`);
|
|
575
|
+
this.capabilities = [];
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
572
578
|
if (response.code !== 250) throw new Error(`EHLO failed: ${response.message}`);
|
|
573
579
|
this.capabilities = response.raw.split("\r\n").filter((line) => line.startsWith("250-") || line.startsWith("250 ")).map((line) => line.substring(4)).filter((line) => line.length > 0);
|
|
574
580
|
}
|
|
@@ -725,10 +731,22 @@ var SmtpConnection = class {
|
|
|
725
731
|
async sendMessage(message, signal) {
|
|
726
732
|
const mailResponse = await this.sendCommand(`MAIL FROM:<${message.envelope.from}>`, signal);
|
|
727
733
|
if (mailResponse.code !== 250) throw new SmtpResponseError(`MAIL FROM failed: ${mailResponse.message}`, mailResponse.code, "MAIL FROM", mailResponse.message);
|
|
734
|
+
const rejectedRecipients = [];
|
|
728
735
|
for (const recipient of message.envelope.to) {
|
|
729
736
|
signal?.throwIfAborted();
|
|
730
737
|
const rcptResponse = await this.sendCommand(`RCPT TO:<${recipient}>`, signal);
|
|
731
|
-
if (rcptResponse.code
|
|
738
|
+
if (rcptResponse.code === 421) throw new SmtpResponseError(`RCPT TO failed for ${recipient}: ${rcptResponse.message}`, rcptResponse.code, "RCPT TO", rcptResponse.message);
|
|
739
|
+
if (rcptResponse.code !== 250 && rcptResponse.code !== 251) rejectedRecipients.push({
|
|
740
|
+
recipient,
|
|
741
|
+
code: rcptResponse.code,
|
|
742
|
+
response: rcptResponse.message,
|
|
743
|
+
retryable: rcptResponse.code >= 400 && rcptResponse.code < 500
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
if (rejectedRecipients.length > 0 && rejectedRecipients.length === message.envelope.to.length) {
|
|
747
|
+
const rejection = rejectedRecipients.find((item) => item.retryable) ?? rejectedRecipients[0];
|
|
748
|
+
const details = rejectedRecipients.map((item) => `${item.recipient}: ${item.code} ${item.response}`).join("; ");
|
|
749
|
+
throw new SmtpResponseError(`RCPT TO failed for every recipient: ${details}`, rejection.code, "RCPT TO", rejection.response, rejectedRecipients);
|
|
732
750
|
}
|
|
733
751
|
const dataResponse = await this.sendCommand("DATA", signal);
|
|
734
752
|
if (dataResponse.code !== 354) throw new SmtpResponseError(`DATA failed: ${dataResponse.message}`, dataResponse.code, "DATA", dataResponse.message);
|
|
@@ -736,7 +754,10 @@ var SmtpConnection = class {
|
|
|
736
754
|
const finalResponse = await this.sendCommand(`${content}\r\n.`, signal);
|
|
737
755
|
if (finalResponse.code !== 250) throw new SmtpResponseError(`Message send failed: ${finalResponse.message}`, finalResponse.code, "DATA_END", finalResponse.message);
|
|
738
756
|
const messageId = this.extractMessageId(finalResponse.message);
|
|
739
|
-
return
|
|
757
|
+
return {
|
|
758
|
+
messageId,
|
|
759
|
+
rejectedRecipients
|
|
760
|
+
};
|
|
740
761
|
}
|
|
741
762
|
extractMessageId(response) {
|
|
742
763
|
const match = response.match(/(?:Message-ID:|id=)[\s<]*([^>\s]+)/i);
|
|
@@ -792,6 +813,8 @@ var SmtpResponseError = class extends Error {
|
|
|
792
813
|
* The textual SMTP reply returned by the server.
|
|
793
814
|
*/
|
|
794
815
|
response;
|
|
816
|
+
/** Recipient-level failures collected for an unsuccessful transaction. */
|
|
817
|
+
rejectedRecipients;
|
|
795
818
|
/**
|
|
796
819
|
* Creates an SMTP response error.
|
|
797
820
|
*
|
|
@@ -799,13 +822,16 @@ var SmtpResponseError = class extends Error {
|
|
|
799
822
|
* @param code The numeric SMTP reply code.
|
|
800
823
|
* @param command The SMTP command that produced the reply.
|
|
801
824
|
* @param response The textual SMTP reply returned by the server.
|
|
825
|
+
* @param rejectedRecipients Recipient-level failures collected for the
|
|
826
|
+
* transaction.
|
|
802
827
|
*/
|
|
803
|
-
constructor(message, code, command, response) {
|
|
828
|
+
constructor(message, code, command, response, rejectedRecipients) {
|
|
804
829
|
super(message);
|
|
805
830
|
this.name = "SmtpResponseError";
|
|
806
831
|
this.code = code;
|
|
807
832
|
this.command = command;
|
|
808
833
|
this.response = response;
|
|
834
|
+
this.rejectedRecipients = rejectedRecipients;
|
|
809
835
|
}
|
|
810
836
|
};
|
|
811
837
|
/**
|
|
@@ -1107,6 +1133,17 @@ function arrayBufferToBase64(buffer) {
|
|
|
1107
1133
|
|
|
1108
1134
|
//#endregion
|
|
1109
1135
|
//#region src/message-converter.ts
|
|
1136
|
+
/**
|
|
1137
|
+
* Converts a message to its SMTP envelope and wire representation.
|
|
1138
|
+
*
|
|
1139
|
+
* @param message The message to convert.
|
|
1140
|
+
* @param dkimConfig Optional DKIM signing configuration.
|
|
1141
|
+
* @returns The converted SMTP message.
|
|
1142
|
+
* @throws {RangeError} If a header contains a token that cannot be folded
|
|
1143
|
+
* within the RFC 5322 hard line-length limit.
|
|
1144
|
+
* @throws {TypeError} If a `Date` or `Message-ID` header supplied by the
|
|
1145
|
+
* message contains a carriage return or line feed.
|
|
1146
|
+
*/
|
|
1110
1147
|
async function convertMessage(message, dkimConfig) {
|
|
1111
1148
|
const envelope = {
|
|
1112
1149
|
from: message.sender.address,
|
|
@@ -1138,19 +1175,34 @@ async function buildRawMessage(message) {
|
|
|
1138
1175
|
const hasHtml = "html" in message.content;
|
|
1139
1176
|
const hasText = "text" in message.content;
|
|
1140
1177
|
const isMultipart = hasAttachments || hasHtml && hasText;
|
|
1141
|
-
lines.push(
|
|
1142
|
-
lines.push(
|
|
1143
|
-
if (message.ccRecipients.length > 0) lines.push(
|
|
1144
|
-
if (message.replyRecipients.length > 0) lines.push(
|
|
1145
|
-
lines.push(
|
|
1146
|
-
lines.push(
|
|
1147
|
-
lines.push(
|
|
1178
|
+
lines.push(foldHeader("From", encodeAddress(message.sender)));
|
|
1179
|
+
lines.push(foldHeader("To", message.recipients.map(encodeAddress).join(", ")));
|
|
1180
|
+
if (message.ccRecipients.length > 0) lines.push(foldHeader("Cc", message.ccRecipients.map(encodeAddress).join(", ")));
|
|
1181
|
+
if (message.replyRecipients.length > 0) lines.push(foldHeader("Reply-To", message.replyRecipients.map(encodeAddress).join(", ")));
|
|
1182
|
+
lines.push(foldHeader("Subject", encodeHeaderValue(message.subject, true)));
|
|
1183
|
+
lines.push(foldHeader("Date", overridden(message, "Date") ?? (/* @__PURE__ */ new Date()).toUTCString()));
|
|
1184
|
+
lines.push(foldHeader("Message-ID", overridden(message, "Message-ID") ?? `<${generateMessageId()}>`));
|
|
1185
|
+
const composed = new Set([
|
|
1186
|
+
"from",
|
|
1187
|
+
"to",
|
|
1188
|
+
"cc",
|
|
1189
|
+
"reply-to",
|
|
1190
|
+
"subject",
|
|
1191
|
+
"date",
|
|
1192
|
+
"message-id"
|
|
1193
|
+
]);
|
|
1148
1194
|
if (message.priority !== "normal") {
|
|
1149
1195
|
const priorityValue = message.priority === "high" ? "1" : "5";
|
|
1150
1196
|
lines.push(`X-Priority: ${priorityValue}`);
|
|
1151
1197
|
lines.push(`X-MSMail-Priority: ${message.priority === "high" ? "High" : "Low"}`);
|
|
1198
|
+
composed.add("x-priority");
|
|
1199
|
+
composed.add("x-msmail-priority");
|
|
1200
|
+
}
|
|
1201
|
+
for (const [key, value] of message.headers) {
|
|
1202
|
+
const name = key.toLowerCase();
|
|
1203
|
+
if (composed.has(name) || reservedHeaders.has(name)) continue;
|
|
1204
|
+
lines.push(foldHeader(key, encodeHeaderValue(value)));
|
|
1152
1205
|
}
|
|
1153
|
-
for (const [key, value] of message.headers) lines.push(`${key}: ${encodeHeaderValue(value)}`);
|
|
1154
1206
|
lines.push("MIME-Version: 1.0");
|
|
1155
1207
|
if (isMultipart) {
|
|
1156
1208
|
lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
@@ -1189,12 +1241,12 @@ async function buildRawMessage(message) {
|
|
|
1189
1241
|
for (const attachment of message.attachments) {
|
|
1190
1242
|
lines.push("");
|
|
1191
1243
|
lines.push(`--${boundary}`);
|
|
1192
|
-
lines.push(
|
|
1244
|
+
lines.push(foldHeader("Content-Type", `${attachment.contentType}; ${encodeMimeParameter("name", attachment.filename)}`));
|
|
1193
1245
|
lines.push("Content-Transfer-Encoding: base64");
|
|
1194
1246
|
if (attachment.inline) {
|
|
1195
|
-
lines.push(
|
|
1247
|
+
lines.push(foldHeader("Content-Disposition", `inline; ${encodeMimeParameter("filename", attachment.filename)}`));
|
|
1196
1248
|
lines.push(`Content-ID: <${attachment.contentId}>`);
|
|
1197
|
-
} else lines.push(
|
|
1249
|
+
} else lines.push(foldHeader("Content-Disposition", `attachment; ${encodeMimeParameter("filename", attachment.filename)}`));
|
|
1198
1250
|
lines.push("");
|
|
1199
1251
|
lines.push(encodeBase64(await attachment.content));
|
|
1200
1252
|
}
|
|
@@ -1213,6 +1265,41 @@ async function buildRawMessage(message) {
|
|
|
1213
1265
|
}
|
|
1214
1266
|
return lines.join("\r\n");
|
|
1215
1267
|
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Reads a header field that overrides a generated default.
|
|
1270
|
+
*
|
|
1271
|
+
* Unlike custom headers, the value is written verbatim rather than RFC 2047
|
|
1272
|
+
* encoded, so it cannot rely on `encodeHeaderValue()` to neutralize control
|
|
1273
|
+
* characters. `ImmutableHeaders` is a structural type, so a `Message` may
|
|
1274
|
+
* carry an adapter that never rejected CR or LF the way a platform `Headers`
|
|
1275
|
+
* does.
|
|
1276
|
+
*
|
|
1277
|
+
* @param message The message whose headers are read.
|
|
1278
|
+
* @param name The header field name.
|
|
1279
|
+
* @returns The supplied value, or `undefined` when the message has none.
|
|
1280
|
+
* @throws {TypeError} If the value contains a carriage return or line feed,
|
|
1281
|
+
* which would inject additional header fields into the message.
|
|
1282
|
+
*/
|
|
1283
|
+
function overridden(message, name) {
|
|
1284
|
+
const value = message.headers.get(name);
|
|
1285
|
+
if (value == null) return void 0;
|
|
1286
|
+
if (/[\r\n]/.test(value)) throw new TypeError(`Header field ${name} must not contain a carriage return or line feed.`);
|
|
1287
|
+
return value;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Header fields the composer owns but writes after the custom headers, or
|
|
1291
|
+
* deliberately omits. A custom header with one of these names is dropped
|
|
1292
|
+
* rather than appended: the structured `Message` fields are authoritative,
|
|
1293
|
+
* RFC 5322 §3.6 allows at most one of each, and a duplicate placed before the
|
|
1294
|
+
* composer's own field makes parsers that take the first occurrence read the
|
|
1295
|
+
* wrong value.
|
|
1296
|
+
*/
|
|
1297
|
+
const reservedHeaders = new Set([
|
|
1298
|
+
"bcc",
|
|
1299
|
+
"content-transfer-encoding",
|
|
1300
|
+
"content-type",
|
|
1301
|
+
"mime-version"
|
|
1302
|
+
]);
|
|
1216
1303
|
function generateBoundary() {
|
|
1217
1304
|
return `boundary-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1218
1305
|
}
|
|
@@ -1223,32 +1310,85 @@ function generateMessageId() {
|
|
|
1223
1310
|
}
|
|
1224
1311
|
function encodeAddress(address) {
|
|
1225
1312
|
if (address.name == null) return address.address;
|
|
1226
|
-
const encodedDisplayName = encodeHeaderValue(address.name);
|
|
1313
|
+
const encodedDisplayName = encodeHeaderValue(address.name, true);
|
|
1227
1314
|
return `${encodedDisplayName} <${address.address}>`;
|
|
1228
1315
|
}
|
|
1229
|
-
function encodeHeaderValue(value) {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const
|
|
1316
|
+
function encodeHeaderValue(value, encodeLongAsciiWords = false) {
|
|
1317
|
+
const hasLongWord = value.split(/\s+/).some((word) => word.length > 60);
|
|
1318
|
+
if (!/^[\x20-\x7E]*$/.test(value) || encodeLongAsciiWords && hasLongWord) {
|
|
1319
|
+
const encodeWord = (text) => {
|
|
1320
|
+
const utf8Bytes = new TextEncoder().encode(text);
|
|
1321
|
+
const base64 = Buffer.from(utf8Bytes).toString("base64");
|
|
1322
|
+
return `=?UTF-8?B?${base64}?=`;
|
|
1323
|
+
};
|
|
1233
1324
|
const maxEncodedLength = 75;
|
|
1234
|
-
const encodedWord =
|
|
1325
|
+
const encodedWord = encodeWord(value);
|
|
1235
1326
|
if (encodedWord.length <= maxEncodedLength) return encodedWord;
|
|
1236
1327
|
const words = [];
|
|
1237
|
-
let
|
|
1238
|
-
for (
|
|
1239
|
-
const
|
|
1240
|
-
|
|
1241
|
-
if (testWord.length <= maxEncodedLength) currentBase64 += chunk;
|
|
1328
|
+
let currentText = "";
|
|
1329
|
+
for (const character of value) {
|
|
1330
|
+
const candidate = currentText + character;
|
|
1331
|
+
if (encodeWord(candidate).length <= maxEncodedLength) currentText = candidate;
|
|
1242
1332
|
else {
|
|
1243
|
-
if (
|
|
1244
|
-
|
|
1333
|
+
if (currentText.length > 0) words.push(encodeWord(currentText));
|
|
1334
|
+
currentText = character;
|
|
1245
1335
|
}
|
|
1246
1336
|
}
|
|
1247
|
-
if (
|
|
1337
|
+
if (currentText.length > 0) words.push(encodeWord(currentText));
|
|
1248
1338
|
return words.join(" ");
|
|
1249
1339
|
}
|
|
1250
1340
|
return value;
|
|
1251
1341
|
}
|
|
1342
|
+
function encodeMimeParameter(name, value) {
|
|
1343
|
+
const escapedValue = value.replace(/[\\"]/g, "\\$&");
|
|
1344
|
+
const quotedParameter = `${name}="${escapedValue}"`;
|
|
1345
|
+
if (/^[\x20-\x7E]*$/.test(value) && quotedParameter.length <= 60) return quotedParameter;
|
|
1346
|
+
const encodedBytes = Array.from(new TextEncoder().encode(value), (byte) => {
|
|
1347
|
+
const character = String.fromCharCode(byte);
|
|
1348
|
+
return /^[A-Za-z0-9!#$&+.^_`|~-]$/.test(character) ? character : `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
1349
|
+
});
|
|
1350
|
+
const segments = [];
|
|
1351
|
+
let segment = "";
|
|
1352
|
+
for (const encodedByte of encodedBytes) {
|
|
1353
|
+
if (segment.length + encodedByte.length > 45) {
|
|
1354
|
+
segments.push(segment);
|
|
1355
|
+
segment = "";
|
|
1356
|
+
}
|
|
1357
|
+
segment += encodedByte;
|
|
1358
|
+
}
|
|
1359
|
+
if (segment.length > 0 || segments.length === 0) segments.push(segment);
|
|
1360
|
+
return segments.map((part, index) => `${name}*${index}*=${index === 0 ? "UTF-8''" : ""}${part}`).join("; ");
|
|
1361
|
+
}
|
|
1362
|
+
function foldHeader(name, value) {
|
|
1363
|
+
const recommendedLineLength = 78;
|
|
1364
|
+
const lines = [];
|
|
1365
|
+
let prefix = `${name}: `;
|
|
1366
|
+
let remaining = value;
|
|
1367
|
+
while (prefix.length + remaining.length > recommendedLineLength) {
|
|
1368
|
+
const availableLength = recommendedLineLength - prefix.length;
|
|
1369
|
+
let breakIndex = -1;
|
|
1370
|
+
for (let index = Math.min(availableLength, remaining.length - 1); index >= 0; index--) if (remaining[index] === " " || remaining[index] === " ") {
|
|
1371
|
+
breakIndex = index;
|
|
1372
|
+
break;
|
|
1373
|
+
}
|
|
1374
|
+
if (breakIndex < 0) {
|
|
1375
|
+
for (let index = Math.max(availableLength + 1, 0); index < remaining.length; index++) if (remaining[index] === " " || remaining[index] === " ") {
|
|
1376
|
+
breakIndex = index;
|
|
1377
|
+
break;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (breakIndex < 0) break;
|
|
1381
|
+
let whitespaceEnd = breakIndex + 1;
|
|
1382
|
+
while (whitespaceEnd < remaining.length && (remaining[whitespaceEnd] === " " || remaining[whitespaceEnd] === " ")) whitespaceEnd++;
|
|
1383
|
+
if (whitespaceEnd === remaining.length) break;
|
|
1384
|
+
lines.push(prefix + remaining.slice(0, breakIndex));
|
|
1385
|
+
prefix = remaining.slice(breakIndex, whitespaceEnd);
|
|
1386
|
+
remaining = remaining.slice(whitespaceEnd);
|
|
1387
|
+
}
|
|
1388
|
+
lines.push(prefix + remaining);
|
|
1389
|
+
if (lines.some((line) => line.length > 998)) throw new RangeError(`Header field ${name} contains a token too long to fold.`);
|
|
1390
|
+
return lines.join("\r\n");
|
|
1391
|
+
}
|
|
1252
1392
|
function encodeQuotedPrintable(text) {
|
|
1253
1393
|
const utf8Bytes = new TextEncoder().encode(text);
|
|
1254
1394
|
let result = "";
|
|
@@ -1379,12 +1519,13 @@ var SmtpTransport = class {
|
|
|
1379
1519
|
options?.signal?.throwIfAborted();
|
|
1380
1520
|
const smtpMessage = await convertMessage(message, this.config.dkim);
|
|
1381
1521
|
options?.signal?.throwIfAborted();
|
|
1382
|
-
const
|
|
1522
|
+
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1383
1523
|
await this.returnConnection(connection);
|
|
1384
1524
|
return {
|
|
1385
1525
|
successful: true,
|
|
1386
|
-
messageId,
|
|
1387
|
-
provider: "smtp"
|
|
1526
|
+
messageId: result.messageId,
|
|
1527
|
+
provider: "smtp",
|
|
1528
|
+
rejectedRecipients: result.rejectedRecipients
|
|
1388
1529
|
};
|
|
1389
1530
|
} catch (error) {
|
|
1390
1531
|
if (connection != null) await this.discardConnection(connection);
|
|
@@ -1448,11 +1589,12 @@ var SmtpTransport = class {
|
|
|
1448
1589
|
try {
|
|
1449
1590
|
const smtpMessage = await convertMessage(message, this.config.dkim);
|
|
1450
1591
|
options?.signal?.throwIfAborted();
|
|
1451
|
-
const
|
|
1592
|
+
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1452
1593
|
yield {
|
|
1453
1594
|
successful: true,
|
|
1454
|
-
messageId,
|
|
1455
|
-
provider: "smtp"
|
|
1595
|
+
messageId: result.messageId,
|
|
1596
|
+
provider: "smtp",
|
|
1597
|
+
rejectedRecipients: result.rejectedRecipients
|
|
1456
1598
|
};
|
|
1457
1599
|
} catch (error) {
|
|
1458
1600
|
options?.signal?.throwIfAborted();
|
|
@@ -1469,11 +1611,12 @@ var SmtpTransport = class {
|
|
|
1469
1611
|
try {
|
|
1470
1612
|
const smtpMessage = await convertMessage(message, this.config.dkim);
|
|
1471
1613
|
options?.signal?.throwIfAborted();
|
|
1472
|
-
const
|
|
1614
|
+
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1473
1615
|
yield {
|
|
1474
1616
|
successful: true,
|
|
1475
|
-
messageId,
|
|
1476
|
-
provider: "smtp"
|
|
1617
|
+
messageId: result.messageId,
|
|
1618
|
+
provider: "smtp",
|
|
1619
|
+
rejectedRecipients: result.rejectedRecipients
|
|
1477
1620
|
};
|
|
1478
1621
|
} catch (error) {
|
|
1479
1622
|
options?.signal?.throwIfAborted();
|
|
@@ -1582,7 +1725,8 @@ function createSmtpFailure(message, error) {
|
|
|
1582
1725
|
attempts: 1,
|
|
1583
1726
|
providerDetails: {
|
|
1584
1727
|
command: error.command,
|
|
1585
|
-
response: error.response
|
|
1728
|
+
response: error.response,
|
|
1729
|
+
rejectedRecipients: error.rejectedRecipients
|
|
1586
1730
|
}
|
|
1587
1731
|
});
|
|
1588
1732
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upyo/smtp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "SMTP transport for Upyo email library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"sideEffects": false,
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@upyo/core": "0.5.
|
|
56
|
+
"@upyo/core": "0.5.4"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"tsdown": "^0.12.7",
|