@upyo/smtp 0.5.3 → 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 CHANGED
@@ -1164,6 +1164,8 @@ function arrayBufferToBase64(buffer) {
1164
1164
  * @returns The converted SMTP message.
1165
1165
  * @throws {RangeError} If a header contains a token that cannot be folded
1166
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.
1167
1169
  */
1168
1170
  async function convertMessage(message, dkimConfig) {
1169
1171
  const envelope = {
@@ -1201,14 +1203,29 @@ async function buildRawMessage(message) {
1201
1203
  if (message.ccRecipients.length > 0) lines.push(foldHeader("Cc", message.ccRecipients.map(encodeAddress).join(", ")));
1202
1204
  if (message.replyRecipients.length > 0) lines.push(foldHeader("Reply-To", message.replyRecipients.map(encodeAddress).join(", ")));
1203
1205
  lines.push(foldHeader("Subject", encodeHeaderValue(message.subject, true)));
1204
- lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
1205
- lines.push(`Message-ID: <${generateMessageId()}>`);
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
+ ]);
1206
1217
  if (message.priority !== "normal") {
1207
1218
  const priorityValue = message.priority === "high" ? "1" : "5";
1208
1219
  lines.push(`X-Priority: ${priorityValue}`);
1209
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)));
1210
1228
  }
1211
- for (const [key, value] of message.headers) lines.push(foldHeader(key, encodeHeaderValue(value)));
1212
1229
  lines.push("MIME-Version: 1.0");
1213
1230
  if (isMultipart) {
1214
1231
  lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
@@ -1271,6 +1288,41 @@ async function buildRawMessage(message) {
1271
1288
  }
1272
1289
  return lines.join("\r\n");
1273
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
+ ]);
1274
1326
  function generateBoundary() {
1275
1327
  return `boundary-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1276
1328
  }
package/dist/index.js CHANGED
@@ -1141,6 +1141,8 @@ function arrayBufferToBase64(buffer) {
1141
1141
  * @returns The converted SMTP message.
1142
1142
  * @throws {RangeError} If a header contains a token that cannot be folded
1143
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.
1144
1146
  */
1145
1147
  async function convertMessage(message, dkimConfig) {
1146
1148
  const envelope = {
@@ -1178,14 +1180,29 @@ async function buildRawMessage(message) {
1178
1180
  if (message.ccRecipients.length > 0) lines.push(foldHeader("Cc", message.ccRecipients.map(encodeAddress).join(", ")));
1179
1181
  if (message.replyRecipients.length > 0) lines.push(foldHeader("Reply-To", message.replyRecipients.map(encodeAddress).join(", ")));
1180
1182
  lines.push(foldHeader("Subject", encodeHeaderValue(message.subject, true)));
1181
- lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
1182
- lines.push(`Message-ID: <${generateMessageId()}>`);
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
+ ]);
1183
1194
  if (message.priority !== "normal") {
1184
1195
  const priorityValue = message.priority === "high" ? "1" : "5";
1185
1196
  lines.push(`X-Priority: ${priorityValue}`);
1186
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)));
1187
1205
  }
1188
- for (const [key, value] of message.headers) lines.push(foldHeader(key, encodeHeaderValue(value)));
1189
1206
  lines.push("MIME-Version: 1.0");
1190
1207
  if (isMultipart) {
1191
1208
  lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
@@ -1248,6 +1265,41 @@ async function buildRawMessage(message) {
1248
1265
  }
1249
1266
  return lines.join("\r\n");
1250
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
+ ]);
1251
1303
  function generateBoundary() {
1252
1304
  return `boundary-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1253
1305
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/smtp",
3
- "version": "0.5.3",
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.3"
56
+ "@upyo/core": "0.5.4"
57
57
  },
58
58
  "devDependencies": {
59
59
  "tsdown": "^0.12.7",