@upyo/smtp 0.6.0-dev.295 → 0.6.0-dev.296

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/README.md CHANGED
@@ -28,6 +28,7 @@ Features
28
28
  - Multiple recipients (To, CC, BCC)
29
29
  - SMTP PIPELINING for faster multi-recipient delivery
30
30
  - SMTP SIZE declaration and advertised-limit checks
31
+ - SMTPUTF8 internationalized address delivery
31
32
  - SMTP delivery status notification requests
32
33
  - Custom headers
33
34
  - Priority levels
@@ -183,6 +184,22 @@ See [RFC 1870] for the SMTP Message Size Declaration extension.
183
184
  [RFC 1870]: https://www.rfc-editor.org/rfc/rfc1870
184
185
 
185
186
 
187
+ Internationalized addresses
188
+ ---------------------------
189
+
190
+ The transport automatically negotiates [RFC 6531] SMTPUTF8 when any sender,
191
+ recipient, or reply-to mailbox contains a non-ASCII character. A supporting
192
+ server must advertise both `SMTPUTF8` and `8BITMIME`; Upyo then sends
193
+ `BODY=8BITMIME SMTPUTF8` on `MAIL FROM`.
194
+
195
+ If either capability is missing, delivery returns a non-retryable failed
196
+ receipt with the code `smtp.smtputf8-unsupported` before the mail transaction
197
+ starts. Unicode display names and subjects continue to use RFC 2047 encoding
198
+ and do not require SMTPUTF8 when the mailbox addresses remain ASCII-only.
199
+
200
+ [RFC 6531]: https://www.rfc-editor.org/rfc/rfc6531
201
+
202
+
186
203
  Delivery status notifications
187
204
  -----------------------------
188
205
 
package/dist/index.cjs CHANGED
@@ -501,6 +501,29 @@ var SmtpMessageSizeError = class extends RangeError {
501
501
  this.maximumSize = maximumSize;
502
502
  }
503
503
  };
504
+ /**
505
+ * Error thrown when an internationalized message requires an SMTP extension
506
+ * that the server did not advertise.
507
+ *
508
+ * The check happens before `MAIL FROM`, so the SMTP connection remains usable
509
+ * for another message.
510
+ *
511
+ * @since 0.6.0
512
+ */
513
+ var SmtpUtf8UnsupportedError = class extends Error {
514
+ /** The required SMTP extension that the server did not advertise. */
515
+ missingCapability;
516
+ /**
517
+ * Creates an SMTPUTF8 support error.
518
+ *
519
+ * @param missingCapability The required extension that was not advertised.
520
+ */
521
+ constructor(missingCapability) {
522
+ super(missingCapability === "SMTPUTF8" ? "The SMTP server does not advertise SMTPUTF8." : "The SMTP server advertises SMTPUTF8 without the required 8BITMIME capability.");
523
+ this.name = "SmtpUtf8UnsupportedError";
524
+ this.missingCapability = missingCapability;
525
+ }
526
+ };
504
527
  var SmtpPipelineTerminatedError = class extends Error {
505
528
  responseIndex;
506
529
  response;
@@ -974,6 +997,12 @@ var SmtpConnection = class {
974
997
  }
975
998
  async sendMessage(message, signal) {
976
999
  signal?.throwIfAborted();
1000
+ let smtpUtf8Parameters = "";
1001
+ if (message.requiresSmtpUtf8 === true) {
1002
+ if (!this.capabilities.some((capability) => /^SMTPUTF8(?:[ \t]|$)/i.test(capability))) throw new SmtpUtf8UnsupportedError("SMTPUTF8");
1003
+ if (!this.capabilities.some((capability) => /^8BITMIME(?:[ \t]|$)/i.test(capability))) throw new SmtpUtf8UnsupportedError("8BITMIME");
1004
+ smtpUtf8Parameters = " BODY=8BITMIME SMTPUTF8";
1005
+ }
977
1006
  const sizeCapability = parseSizeCapability(this.capabilities);
978
1007
  let sizeParameter = "";
979
1008
  if (sizeCapability != null) {
@@ -984,7 +1013,7 @@ var SmtpConnection = class {
984
1013
  const dsn = message.envelope.dsn;
985
1014
  if (dsn != null && !this.capabilities.some((capability) => /^DSN[ \t]*$/i.test(capability))) throw new SmtpDsnUnsupportedError();
986
1015
  const mailDsnParameters = dsn == null || dsn.mailParameters.length === 0 ? "" : ` ${dsn.mailParameters.join(" ")}`;
987
- const mailCommand = `MAIL FROM:<${message.envelope.from}>${sizeParameter}${mailDsnParameters}`;
1016
+ const mailCommand = `MAIL FROM:<${message.envelope.from}>${sizeParameter}${smtpUtf8Parameters}${mailDsnParameters}`;
988
1017
  const recipientCommands = message.envelope.to.map((recipient, index) => {
989
1018
  const parameters = dsn?.recipientParameters[index] ?? [];
990
1019
  const suffix = parameters.length === 0 ? "" : ` ${parameters.join(" ")}`;
@@ -1431,6 +1460,13 @@ async function convertMessage(message, dkimConfig, dsn) {
1431
1460
  ],
1432
1461
  dsn
1433
1462
  };
1463
+ const requiresSmtpUtf8 = [
1464
+ message.sender,
1465
+ ...message.recipients,
1466
+ ...message.ccRecipients,
1467
+ ...message.bccRecipients,
1468
+ ...message.replyRecipients
1469
+ ].some((address) => Array.from(address.address).some((character) => (character.codePointAt(0) ?? 0) > 127));
1434
1470
  let raw = await buildRawMessage(message);
1435
1471
  if (dkimConfig) try {
1436
1472
  for (const sig of dkimConfig.signatures) {
@@ -1443,7 +1479,8 @@ async function convertMessage(message, dkimConfig, dsn) {
1443
1479
  }
1444
1480
  return {
1445
1481
  envelope,
1446
- raw
1482
+ raw,
1483
+ requiresSmtpUtf8
1447
1484
  };
1448
1485
  }
1449
1486
  async function buildRawMessage(message) {
@@ -1614,7 +1651,7 @@ function foldHeader(name, value) {
1614
1651
  remaining = remaining.slice(whitespaceEnd);
1615
1652
  }
1616
1653
  lines.push(prefix + remaining);
1617
- if (lines.some((line) => line.length > 998)) throw new RangeError(`Header field ${name} contains a token too long to fold.`);
1654
+ if (lines.some((line) => node_buffer.Buffer.byteLength(line, "utf8") > 998)) throw new RangeError(`Header field ${name} contains a token too long to fold.`);
1618
1655
  return lines.join("\r\n");
1619
1656
  }
1620
1657
  function encodeQuotedPrintable(text) {
@@ -1972,6 +2009,14 @@ function createSmtpFailure(message, error) {
1972
2009
  maximumSize: error.maximumSize.toString()
1973
2010
  }
1974
2011
  });
2012
+ if (error instanceof SmtpUtf8UnsupportedError) return (0, __upyo_core.createFailedReceipt)(message, {
2013
+ provider: "smtp",
2014
+ code: "smtp.smtputf8-unsupported",
2015
+ category: "configuration",
2016
+ retryable: false,
2017
+ attempts: 1,
2018
+ providerDetails: { missingCapability: error.missingCapability }
2019
+ });
1975
2020
  if (error instanceof SmtpResponseError) {
1976
2021
  const classification = classifySmtpReply(error.code);
1977
2022
  return (0, __upyo_core.createFailedReceipt)(message, {
@@ -1993,7 +2038,7 @@ function createSmtpFailure(message, error) {
1993
2038
  });
1994
2039
  }
1995
2040
  function isReusableLocalFailure(error) {
1996
- return error instanceof SmtpMessageSizeError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
2041
+ return error instanceof SmtpMessageSizeError || error instanceof SmtpUtf8UnsupportedError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
1997
2042
  }
1998
2043
  function classifySmtpReply(code) {
1999
2044
  if (code >= 400 && code < 500) return {
package/dist/index.js CHANGED
@@ -478,6 +478,29 @@ var SmtpMessageSizeError = class extends RangeError {
478
478
  this.maximumSize = maximumSize;
479
479
  }
480
480
  };
481
+ /**
482
+ * Error thrown when an internationalized message requires an SMTP extension
483
+ * that the server did not advertise.
484
+ *
485
+ * The check happens before `MAIL FROM`, so the SMTP connection remains usable
486
+ * for another message.
487
+ *
488
+ * @since 0.6.0
489
+ */
490
+ var SmtpUtf8UnsupportedError = class extends Error {
491
+ /** The required SMTP extension that the server did not advertise. */
492
+ missingCapability;
493
+ /**
494
+ * Creates an SMTPUTF8 support error.
495
+ *
496
+ * @param missingCapability The required extension that was not advertised.
497
+ */
498
+ constructor(missingCapability) {
499
+ super(missingCapability === "SMTPUTF8" ? "The SMTP server does not advertise SMTPUTF8." : "The SMTP server advertises SMTPUTF8 without the required 8BITMIME capability.");
500
+ this.name = "SmtpUtf8UnsupportedError";
501
+ this.missingCapability = missingCapability;
502
+ }
503
+ };
481
504
  var SmtpPipelineTerminatedError = class extends Error {
482
505
  responseIndex;
483
506
  response;
@@ -951,6 +974,12 @@ var SmtpConnection = class {
951
974
  }
952
975
  async sendMessage(message, signal) {
953
976
  signal?.throwIfAborted();
977
+ let smtpUtf8Parameters = "";
978
+ if (message.requiresSmtpUtf8 === true) {
979
+ if (!this.capabilities.some((capability) => /^SMTPUTF8(?:[ \t]|$)/i.test(capability))) throw new SmtpUtf8UnsupportedError("SMTPUTF8");
980
+ if (!this.capabilities.some((capability) => /^8BITMIME(?:[ \t]|$)/i.test(capability))) throw new SmtpUtf8UnsupportedError("8BITMIME");
981
+ smtpUtf8Parameters = " BODY=8BITMIME SMTPUTF8";
982
+ }
954
983
  const sizeCapability = parseSizeCapability(this.capabilities);
955
984
  let sizeParameter = "";
956
985
  if (sizeCapability != null) {
@@ -961,7 +990,7 @@ var SmtpConnection = class {
961
990
  const dsn = message.envelope.dsn;
962
991
  if (dsn != null && !this.capabilities.some((capability) => /^DSN[ \t]*$/i.test(capability))) throw new SmtpDsnUnsupportedError();
963
992
  const mailDsnParameters = dsn == null || dsn.mailParameters.length === 0 ? "" : ` ${dsn.mailParameters.join(" ")}`;
964
- const mailCommand = `MAIL FROM:<${message.envelope.from}>${sizeParameter}${mailDsnParameters}`;
993
+ const mailCommand = `MAIL FROM:<${message.envelope.from}>${sizeParameter}${smtpUtf8Parameters}${mailDsnParameters}`;
965
994
  const recipientCommands = message.envelope.to.map((recipient, index) => {
966
995
  const parameters = dsn?.recipientParameters[index] ?? [];
967
996
  const suffix = parameters.length === 0 ? "" : ` ${parameters.join(" ")}`;
@@ -1408,6 +1437,13 @@ async function convertMessage(message, dkimConfig, dsn) {
1408
1437
  ],
1409
1438
  dsn
1410
1439
  };
1440
+ const requiresSmtpUtf8 = [
1441
+ message.sender,
1442
+ ...message.recipients,
1443
+ ...message.ccRecipients,
1444
+ ...message.bccRecipients,
1445
+ ...message.replyRecipients
1446
+ ].some((address) => Array.from(address.address).some((character) => (character.codePointAt(0) ?? 0) > 127));
1411
1447
  let raw = await buildRawMessage(message);
1412
1448
  if (dkimConfig) try {
1413
1449
  for (const sig of dkimConfig.signatures) {
@@ -1420,7 +1456,8 @@ async function convertMessage(message, dkimConfig, dsn) {
1420
1456
  }
1421
1457
  return {
1422
1458
  envelope,
1423
- raw
1459
+ raw,
1460
+ requiresSmtpUtf8
1424
1461
  };
1425
1462
  }
1426
1463
  async function buildRawMessage(message) {
@@ -1591,7 +1628,7 @@ function foldHeader(name, value) {
1591
1628
  remaining = remaining.slice(whitespaceEnd);
1592
1629
  }
1593
1630
  lines.push(prefix + remaining);
1594
- if (lines.some((line) => line.length > 998)) throw new RangeError(`Header field ${name} contains a token too long to fold.`);
1631
+ if (lines.some((line) => Buffer.byteLength(line, "utf8") > 998)) throw new RangeError(`Header field ${name} contains a token too long to fold.`);
1595
1632
  return lines.join("\r\n");
1596
1633
  }
1597
1634
  function encodeQuotedPrintable(text) {
@@ -1949,6 +1986,14 @@ function createSmtpFailure(message, error) {
1949
1986
  maximumSize: error.maximumSize.toString()
1950
1987
  }
1951
1988
  });
1989
+ if (error instanceof SmtpUtf8UnsupportedError) return createFailedReceipt(message, {
1990
+ provider: "smtp",
1991
+ code: "smtp.smtputf8-unsupported",
1992
+ category: "configuration",
1993
+ retryable: false,
1994
+ attempts: 1,
1995
+ providerDetails: { missingCapability: error.missingCapability }
1996
+ });
1952
1997
  if (error instanceof SmtpResponseError) {
1953
1998
  const classification = classifySmtpReply(error.code);
1954
1999
  return createFailedReceipt(message, {
@@ -1970,7 +2015,7 @@ function createSmtpFailure(message, error) {
1970
2015
  });
1971
2016
  }
1972
2017
  function isReusableLocalFailure(error) {
1973
- return error instanceof SmtpMessageSizeError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
2018
+ return error instanceof SmtpMessageSizeError || error instanceof SmtpUtf8UnsupportedError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
1974
2019
  }
1975
2020
  function classifySmtpReply(code) {
1976
2021
  if (code >= 400 && code < 500) return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/smtp",
3
- "version": "0.6.0-dev.295",
3
+ "version": "0.6.0-dev.296",
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.6.0-dev.295+d64d0d8f"
56
+ "@upyo/core": "0.6.0-dev.296+edf6b8c1"
57
57
  },
58
58
  "devDependencies": {
59
59
  "tsdown": "^0.12.7",