@upyo/smtp 0.5.0-dev.184 → 0.5.0-dev.186

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
@@ -669,17 +669,17 @@ var SmtpConnection = class {
669
669
  }
670
670
  async sendMessage(message, signal) {
671
671
  const mailResponse = await this.sendCommand(`MAIL FROM:<${message.envelope.from}>`, signal);
672
- if (mailResponse.code !== 250) throw new Error(`MAIL FROM failed: ${mailResponse.message}`);
672
+ if (mailResponse.code !== 250) throw new SmtpResponseError(`MAIL FROM failed: ${mailResponse.message}`, mailResponse.code, "MAIL FROM", mailResponse.message);
673
673
  for (const recipient of message.envelope.to) {
674
674
  signal?.throwIfAborted();
675
675
  const rcptResponse = await this.sendCommand(`RCPT TO:<${recipient}>`, signal);
676
- if (rcptResponse.code !== 250) throw new Error(`RCPT TO failed for ${recipient}: ${rcptResponse.message}`);
676
+ if (rcptResponse.code !== 250) throw new SmtpResponseError(`RCPT TO failed for ${recipient}: ${rcptResponse.message}`, rcptResponse.code, "RCPT TO", rcptResponse.message);
677
677
  }
678
678
  const dataResponse = await this.sendCommand("DATA", signal);
679
- if (dataResponse.code !== 354) throw new Error(`DATA failed: ${dataResponse.message}`);
679
+ if (dataResponse.code !== 354) throw new SmtpResponseError(`DATA failed: ${dataResponse.message}`, dataResponse.code, "DATA", dataResponse.message);
680
680
  const content = message.raw.replace(/\n\./g, "\n..");
681
681
  const finalResponse = await this.sendCommand(`${content}\r\n.`, signal);
682
- if (finalResponse.code !== 250) throw new Error(`Message send failed: ${finalResponse.message}`);
682
+ if (finalResponse.code !== 250) throw new SmtpResponseError(`Message send failed: ${finalResponse.message}`, finalResponse.code, "DATA_END", finalResponse.message);
683
683
  const messageId = this.extractMessageId(finalResponse.message);
684
684
  return messageId;
685
685
  }
@@ -720,6 +720,40 @@ var SmtpConnection = class {
720
720
  }
721
721
  };
722
722
  /**
723
+ * Error thrown when an SMTP command receives an unsuccessful server reply.
724
+ *
725
+ * @since 0.5.0
726
+ */
727
+ var SmtpResponseError = class extends Error {
728
+ /**
729
+ * The numeric SMTP reply code returned by the server.
730
+ */
731
+ code;
732
+ /**
733
+ * The SMTP command that produced the reply.
734
+ */
735
+ command;
736
+ /**
737
+ * The textual SMTP reply returned by the server.
738
+ */
739
+ response;
740
+ /**
741
+ * Creates an SMTP response error.
742
+ *
743
+ * @param message Human-readable error message.
744
+ * @param code The numeric SMTP reply code.
745
+ * @param command The SMTP command that produced the reply.
746
+ * @param response The textual SMTP reply returned by the server.
747
+ */
748
+ constructor(message, code, command, response) {
749
+ super(message);
750
+ this.name = "SmtpResponseError";
751
+ this.code = code;
752
+ this.command = command;
753
+ this.response = response;
754
+ }
755
+ };
756
+ /**
723
757
  * Decodes the Base64 JSON error challenge a server sends after a failed OAuth
724
758
  * SASL exchange, falling back to the raw message when it is not valid Base64.
725
759
  *
@@ -1300,7 +1334,7 @@ var SmtpTransport = class {
1300
1334
  } catch (error) {
1301
1335
  if (connection != null) await this.discardConnection(connection);
1302
1336
  options?.signal?.throwIfAborted();
1303
- return createSmtpFailure(error instanceof Error ? error.message : String(error));
1337
+ return createSmtpFailure(error instanceof Error ? error.message : String(error), error);
1304
1338
  }
1305
1339
  }
1306
1340
  /**
@@ -1343,7 +1377,7 @@ var SmtpTransport = class {
1343
1377
  const errorMessage = error instanceof Error ? error.message : String(error);
1344
1378
  for await (const _ of messages) {
1345
1379
  options?.signal?.throwIfAborted();
1346
- yield createSmtpFailure(errorMessage);
1380
+ yield createSmtpFailure(errorMessage, error);
1347
1381
  }
1348
1382
  return;
1349
1383
  }
@@ -1368,7 +1402,7 @@ var SmtpTransport = class {
1368
1402
  } catch (error) {
1369
1403
  options?.signal?.throwIfAborted();
1370
1404
  connectionValid = false;
1371
- yield createSmtpFailure(error instanceof Error ? error.message : String(error));
1405
+ yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
1372
1406
  }
1373
1407
  }
1374
1408
  else for (const message of messages) {
@@ -1389,7 +1423,7 @@ var SmtpTransport = class {
1389
1423
  } catch (error) {
1390
1424
  options?.signal?.throwIfAborted();
1391
1425
  connectionValid = false;
1392
- yield createSmtpFailure(error instanceof Error ? error.message : String(error));
1426
+ yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
1393
1427
  }
1394
1428
  }
1395
1429
  if (connectionValid) await this.returnConnection(connection);
@@ -1482,12 +1516,40 @@ var SmtpTransport = class {
1482
1516
  await this.closeAllConnections();
1483
1517
  }
1484
1518
  };
1485
- function createSmtpFailure(message) {
1519
+ function createSmtpFailure(message, error) {
1520
+ if (error instanceof SmtpResponseError) {
1521
+ const classification = classifySmtpReply(error.code);
1522
+ return (0, __upyo_core.createFailedReceipt)(message, {
1523
+ provider: "smtp",
1524
+ code: `smtp.${error.code}`,
1525
+ category: classification.category,
1526
+ retryable: classification.retryable,
1527
+ attempts: 1,
1528
+ providerDetails: {
1529
+ command: error.command,
1530
+ response: error.response
1531
+ }
1532
+ });
1533
+ }
1486
1534
  return (0, __upyo_core.createFailedReceipt)(message, {
1487
1535
  provider: "smtp",
1488
1536
  attempts: 1
1489
1537
  });
1490
1538
  }
1539
+ function classifySmtpReply(code) {
1540
+ if (code >= 400 && code < 500) return {
1541
+ category: "service-unavailable",
1542
+ retryable: true
1543
+ };
1544
+ if (code >= 500 && code < 600) return {
1545
+ category: "rejected",
1546
+ retryable: false
1547
+ };
1548
+ return {
1549
+ category: "unknown",
1550
+ retryable: false
1551
+ };
1552
+ }
1491
1553
 
1492
1554
  //#endregion
1493
1555
  exports.SmtpAuthError = SmtpAuthError;
package/dist/index.js CHANGED
@@ -646,17 +646,17 @@ var SmtpConnection = class {
646
646
  }
647
647
  async sendMessage(message, signal) {
648
648
  const mailResponse = await this.sendCommand(`MAIL FROM:<${message.envelope.from}>`, signal);
649
- if (mailResponse.code !== 250) throw new Error(`MAIL FROM failed: ${mailResponse.message}`);
649
+ if (mailResponse.code !== 250) throw new SmtpResponseError(`MAIL FROM failed: ${mailResponse.message}`, mailResponse.code, "MAIL FROM", mailResponse.message);
650
650
  for (const recipient of message.envelope.to) {
651
651
  signal?.throwIfAborted();
652
652
  const rcptResponse = await this.sendCommand(`RCPT TO:<${recipient}>`, signal);
653
- if (rcptResponse.code !== 250) throw new Error(`RCPT TO failed for ${recipient}: ${rcptResponse.message}`);
653
+ if (rcptResponse.code !== 250) throw new SmtpResponseError(`RCPT TO failed for ${recipient}: ${rcptResponse.message}`, rcptResponse.code, "RCPT TO", rcptResponse.message);
654
654
  }
655
655
  const dataResponse = await this.sendCommand("DATA", signal);
656
- if (dataResponse.code !== 354) throw new Error(`DATA failed: ${dataResponse.message}`);
656
+ if (dataResponse.code !== 354) throw new SmtpResponseError(`DATA failed: ${dataResponse.message}`, dataResponse.code, "DATA", dataResponse.message);
657
657
  const content = message.raw.replace(/\n\./g, "\n..");
658
658
  const finalResponse = await this.sendCommand(`${content}\r\n.`, signal);
659
- if (finalResponse.code !== 250) throw new Error(`Message send failed: ${finalResponse.message}`);
659
+ if (finalResponse.code !== 250) throw new SmtpResponseError(`Message send failed: ${finalResponse.message}`, finalResponse.code, "DATA_END", finalResponse.message);
660
660
  const messageId = this.extractMessageId(finalResponse.message);
661
661
  return messageId;
662
662
  }
@@ -697,6 +697,40 @@ var SmtpConnection = class {
697
697
  }
698
698
  };
699
699
  /**
700
+ * Error thrown when an SMTP command receives an unsuccessful server reply.
701
+ *
702
+ * @since 0.5.0
703
+ */
704
+ var SmtpResponseError = class extends Error {
705
+ /**
706
+ * The numeric SMTP reply code returned by the server.
707
+ */
708
+ code;
709
+ /**
710
+ * The SMTP command that produced the reply.
711
+ */
712
+ command;
713
+ /**
714
+ * The textual SMTP reply returned by the server.
715
+ */
716
+ response;
717
+ /**
718
+ * Creates an SMTP response error.
719
+ *
720
+ * @param message Human-readable error message.
721
+ * @param code The numeric SMTP reply code.
722
+ * @param command The SMTP command that produced the reply.
723
+ * @param response The textual SMTP reply returned by the server.
724
+ */
725
+ constructor(message, code, command, response) {
726
+ super(message);
727
+ this.name = "SmtpResponseError";
728
+ this.code = code;
729
+ this.command = command;
730
+ this.response = response;
731
+ }
732
+ };
733
+ /**
700
734
  * Decodes the Base64 JSON error challenge a server sends after a failed OAuth
701
735
  * SASL exchange, falling back to the raw message when it is not valid Base64.
702
736
  *
@@ -1277,7 +1311,7 @@ var SmtpTransport = class {
1277
1311
  } catch (error) {
1278
1312
  if (connection != null) await this.discardConnection(connection);
1279
1313
  options?.signal?.throwIfAborted();
1280
- return createSmtpFailure(error instanceof Error ? error.message : String(error));
1314
+ return createSmtpFailure(error instanceof Error ? error.message : String(error), error);
1281
1315
  }
1282
1316
  }
1283
1317
  /**
@@ -1320,7 +1354,7 @@ var SmtpTransport = class {
1320
1354
  const errorMessage = error instanceof Error ? error.message : String(error);
1321
1355
  for await (const _ of messages) {
1322
1356
  options?.signal?.throwIfAborted();
1323
- yield createSmtpFailure(errorMessage);
1357
+ yield createSmtpFailure(errorMessage, error);
1324
1358
  }
1325
1359
  return;
1326
1360
  }
@@ -1345,7 +1379,7 @@ var SmtpTransport = class {
1345
1379
  } catch (error) {
1346
1380
  options?.signal?.throwIfAborted();
1347
1381
  connectionValid = false;
1348
- yield createSmtpFailure(error instanceof Error ? error.message : String(error));
1382
+ yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
1349
1383
  }
1350
1384
  }
1351
1385
  else for (const message of messages) {
@@ -1366,7 +1400,7 @@ var SmtpTransport = class {
1366
1400
  } catch (error) {
1367
1401
  options?.signal?.throwIfAborted();
1368
1402
  connectionValid = false;
1369
- yield createSmtpFailure(error instanceof Error ? error.message : String(error));
1403
+ yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
1370
1404
  }
1371
1405
  }
1372
1406
  if (connectionValid) await this.returnConnection(connection);
@@ -1459,12 +1493,40 @@ var SmtpTransport = class {
1459
1493
  await this.closeAllConnections();
1460
1494
  }
1461
1495
  };
1462
- function createSmtpFailure(message) {
1496
+ function createSmtpFailure(message, error) {
1497
+ if (error instanceof SmtpResponseError) {
1498
+ const classification = classifySmtpReply(error.code);
1499
+ return createFailedReceipt(message, {
1500
+ provider: "smtp",
1501
+ code: `smtp.${error.code}`,
1502
+ category: classification.category,
1503
+ retryable: classification.retryable,
1504
+ attempts: 1,
1505
+ providerDetails: {
1506
+ command: error.command,
1507
+ response: error.response
1508
+ }
1509
+ });
1510
+ }
1463
1511
  return createFailedReceipt(message, {
1464
1512
  provider: "smtp",
1465
1513
  attempts: 1
1466
1514
  });
1467
1515
  }
1516
+ function classifySmtpReply(code) {
1517
+ if (code >= 400 && code < 500) return {
1518
+ category: "service-unavailable",
1519
+ retryable: true
1520
+ };
1521
+ if (code >= 500 && code < 600) return {
1522
+ category: "rejected",
1523
+ retryable: false
1524
+ };
1525
+ return {
1526
+ category: "unknown",
1527
+ retryable: false
1528
+ };
1529
+ }
1468
1530
 
1469
1531
  //#endregion
1470
1532
  export { SmtpAuthError, SmtpTransport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/smtp",
3
- "version": "0.5.0-dev.184",
3
+ "version": "0.5.0-dev.186",
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.0-dev.184+f9b152b6"
56
+ "@upyo/core": "0.5.0-dev.186+096ec68f"
57
57
  },
58
58
  "devDependencies": {
59
59
  "tsdown": "^0.12.7",