@upyo/smtp 0.6.0-dev.291 → 0.6.0-dev.294
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 +35 -0
- package/dist/index.cjs +154 -10
- package/dist/index.d.cts +95 -3
- package/dist/index.d.ts +95 -3
- package/dist/index.js +153 -11
- package/package.json +2 -2
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
|
+
- SMTP delivery status notification requests
|
|
31
32
|
- Custom headers
|
|
32
33
|
- Priority levels
|
|
33
34
|
- Comprehensive testing utilities
|
|
@@ -182,6 +183,40 @@ See [RFC 1870] for the SMTP Message Size Declaration extension.
|
|
|
182
183
|
[RFC 1870]: https://www.rfc-editor.org/rfc/rfc1870
|
|
183
184
|
|
|
184
185
|
|
|
186
|
+
Delivery status notifications
|
|
187
|
+
-----------------------------
|
|
188
|
+
|
|
189
|
+
Pass SMTP-specific DSN settings to `send()` to request delivery status
|
|
190
|
+
notifications under [RFC 3461]:
|
|
191
|
+
|
|
192
|
+
~~~~ typescript
|
|
193
|
+
const receipt = await transport.send(message, {
|
|
194
|
+
dsn: {
|
|
195
|
+
envelopeId: "campaign+42",
|
|
196
|
+
return: "headers",
|
|
197
|
+
recipients: {
|
|
198
|
+
"recipient@example.net": {
|
|
199
|
+
notify: ["success", "failure", "delay"],
|
|
200
|
+
originalRecipient: "recipient@example.net",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
~~~~
|
|
206
|
+
|
|
207
|
+
`envelopeId` and `return` become `ENVID` and `RET` parameters on `MAIL FROM`.
|
|
208
|
+
Each recipient's `notify` and `originalRecipient` values become `NOTIFY` and
|
|
209
|
+
`ORCPT` parameters on its `RCPT TO` command. Upyo validates and `xtext`-escapes
|
|
210
|
+
the values without adding them to the message headers. `envelopeId` must not
|
|
211
|
+
be empty, and `originalRecipient` must exactly match its envelope recipient.
|
|
212
|
+
|
|
213
|
+
If the server does not advertise `DSN`, the transport returns a non-retryable
|
|
214
|
+
failed receipt with the code `smtp.dsn-unsupported` before sending `MAIL FROM`.
|
|
215
|
+
Invalid settings use `smtp.dsn-invalid`.
|
|
216
|
+
|
|
217
|
+
[RFC 3461]: https://www.rfc-editor.org/rfc/rfc3461
|
|
218
|
+
|
|
219
|
+
|
|
185
220
|
DKIM signing
|
|
186
221
|
------------
|
|
187
222
|
|
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,119 @@ const node_buffer = __toESM(require("node:buffer"));
|
|
|
26
26
|
const node_net = __toESM(require("node:net"));
|
|
27
27
|
const node_tls = __toESM(require("node:tls"));
|
|
28
28
|
|
|
29
|
+
//#region src/delivery-status.ts
|
|
30
|
+
/**
|
|
31
|
+
* Error produced when SMTP delivery status notification settings are invalid.
|
|
32
|
+
*
|
|
33
|
+
* @since 0.6.0
|
|
34
|
+
*/
|
|
35
|
+
var SmtpDsnValidationError = class extends TypeError {
|
|
36
|
+
/**
|
|
37
|
+
* Creates a delivery status notification validation error.
|
|
38
|
+
*
|
|
39
|
+
* @param message A description of the invalid setting.
|
|
40
|
+
*/
|
|
41
|
+
constructor(message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "SmtpDsnValidationError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Error produced when delivery status notifications were requested from an
|
|
48
|
+
* SMTP server that did not advertise the RFC 3461 `DSN` extension.
|
|
49
|
+
*
|
|
50
|
+
* @since 0.6.0
|
|
51
|
+
*/
|
|
52
|
+
var SmtpDsnUnsupportedError = class extends Error {
|
|
53
|
+
/** Creates an error for a server without the `DSN` extension. */
|
|
54
|
+
constructor() {
|
|
55
|
+
super("Delivery status notifications were requested, but the server does not advertise the DSN extension.");
|
|
56
|
+
this.name = "SmtpDsnUnsupportedError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const NOTIFICATION_CONDITIONS = new Set([
|
|
60
|
+
"never",
|
|
61
|
+
"success",
|
|
62
|
+
"failure",
|
|
63
|
+
"delay"
|
|
64
|
+
]);
|
|
65
|
+
/**
|
|
66
|
+
* Validates and serializes the RFC 3461 envelope parameters for a message.
|
|
67
|
+
*
|
|
68
|
+
* @param message The message whose SMTP envelope will carry the parameters.
|
|
69
|
+
* @param dsn The caller-supplied delivery status notification settings.
|
|
70
|
+
* @returns Serialized parameters, or `undefined` when none were requested.
|
|
71
|
+
* @throws {SmtpDsnValidationError} If any setting violates RFC 3461 or does
|
|
72
|
+
* not correspond to the message envelope.
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
function resolveSmtpDsn(message, dsn) {
|
|
76
|
+
if (dsn == null) return void 0;
|
|
77
|
+
if (typeof dsn !== "object" || Array.isArray(dsn)) throw new SmtpDsnValidationError("DSN options must be an object.");
|
|
78
|
+
const mailParameters = [];
|
|
79
|
+
if (dsn.return != null) {
|
|
80
|
+
if (dsn.return !== "full" && dsn.return !== "headers") throw new SmtpDsnValidationError(`Unsupported DSN return value: ${String(dsn.return)}`);
|
|
81
|
+
mailParameters.push(`RET=${dsn.return === "full" ? "FULL" : "HDRS"}`);
|
|
82
|
+
}
|
|
83
|
+
if (dsn.envelopeId != null) {
|
|
84
|
+
if (dsn.envelopeId === "") throw new SmtpDsnValidationError("DSN envelope ID must not be empty.");
|
|
85
|
+
const encoded = encodeXtext(dsn.envelopeId, "DSN envelope ID");
|
|
86
|
+
const parameter = `ENVID=${encoded}`;
|
|
87
|
+
assertParameterLength(parameter, 100, "ENVID");
|
|
88
|
+
mailParameters.push(parameter);
|
|
89
|
+
}
|
|
90
|
+
const envelopeRecipients = [
|
|
91
|
+
...message.recipients.map((recipient) => recipient.address),
|
|
92
|
+
...message.ccRecipients.map((recipient) => recipient.address),
|
|
93
|
+
...message.bccRecipients.map((recipient) => recipient.address)
|
|
94
|
+
];
|
|
95
|
+
const envelopeRecipientSet = new Set(envelopeRecipients);
|
|
96
|
+
const configuredRecipients = dsn.recipients;
|
|
97
|
+
if (configuredRecipients != null && (typeof configuredRecipients !== "object" || Array.isArray(configuredRecipients))) throw new SmtpDsnValidationError("DSN recipient options must be an object.");
|
|
98
|
+
const serializedByAddress = /* @__PURE__ */ new Map();
|
|
99
|
+
for (const [address, options] of Object.entries(configuredRecipients ?? {})) {
|
|
100
|
+
if (!envelopeRecipientSet.has(address)) throw new SmtpDsnValidationError(`DSN recipient ${address} is not an envelope recipient.`);
|
|
101
|
+
if (options == null || typeof options !== "object" || Array.isArray(options)) throw new SmtpDsnValidationError(`DSN options for ${address} must be an object.`);
|
|
102
|
+
const parameters = [];
|
|
103
|
+
if (options.notify != null) {
|
|
104
|
+
const notify = options.notify;
|
|
105
|
+
if (!Array.isArray(notify) || notify.length === 0) throw new SmtpDsnValidationError(`DSN notification conditions for ${address} must be a non-empty array.`);
|
|
106
|
+
for (const condition of notify) if (typeof condition !== "string" || !NOTIFICATION_CONDITIONS.has(condition)) throw new SmtpDsnValidationError(`Unsupported DSN notification condition: ${String(condition)}`);
|
|
107
|
+
if (notify.includes("never") && notify.length !== 1) throw new SmtpDsnValidationError("The DSN notification condition NEVER must appear by itself.");
|
|
108
|
+
parameters.push(`NOTIFY=${notify.map((condition) => condition.toUpperCase()).join(",")}`);
|
|
109
|
+
}
|
|
110
|
+
if (options.originalRecipient != null) {
|
|
111
|
+
const encoded = encodeXtext(options.originalRecipient, `DSN original recipient for ${address}`);
|
|
112
|
+
if (options.originalRecipient !== address) throw new SmtpDsnValidationError(`DSN original recipient for ${address} must match the envelope recipient address.`);
|
|
113
|
+
const parameter = `ORCPT=rfc822;${encoded}`;
|
|
114
|
+
assertParameterLength(parameter, 500, "ORCPT");
|
|
115
|
+
parameters.push(parameter);
|
|
116
|
+
}
|
|
117
|
+
serializedByAddress.set(address, parameters);
|
|
118
|
+
}
|
|
119
|
+
const recipientParameters = envelopeRecipients.map((address) => serializedByAddress.get(address) ?? []);
|
|
120
|
+
const requested = mailParameters.length > 0 || recipientParameters.some((parameters) => parameters.length > 0);
|
|
121
|
+
return requested ? {
|
|
122
|
+
mailParameters,
|
|
123
|
+
recipientParameters
|
|
124
|
+
} : void 0;
|
|
125
|
+
}
|
|
126
|
+
function encodeXtext(value, name) {
|
|
127
|
+
if (typeof value !== "string") throw new SmtpDsnValidationError(`${name} must be a string.`);
|
|
128
|
+
let encoded = "";
|
|
129
|
+
for (let index = 0; index < value.length; index++) {
|
|
130
|
+
const code = value.charCodeAt(index);
|
|
131
|
+
if (code < 32 || code > 126) throw new SmtpDsnValidationError(`${name} must contain only printable US-ASCII characters.`);
|
|
132
|
+
if (code >= 33 && code <= 126 && code !== 43 && code !== 61) encoded += value[index];
|
|
133
|
+
else encoded += `+${code.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
134
|
+
}
|
|
135
|
+
return encoded;
|
|
136
|
+
}
|
|
137
|
+
function assertParameterLength(parameter, maximum, name) {
|
|
138
|
+
if (parameter.length > maximum) throw new SmtpDsnValidationError(`${name} parameter exceeds the RFC 3461 limit of ${maximum} characters.`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
29
142
|
//#region src/config.ts
|
|
30
143
|
/**
|
|
31
144
|
* Creates a resolved SMTP configuration by applying default values to optional fields.
|
|
@@ -868,8 +981,15 @@ var SmtpConnection = class {
|
|
|
868
981
|
if (sizeCapability.maximum != null && BigInt(messageSize) > sizeCapability.maximum) throw new SmtpMessageSizeError(messageSize, sizeCapability.maximum);
|
|
869
982
|
sizeParameter = ` SIZE=${messageSize}`;
|
|
870
983
|
}
|
|
871
|
-
const
|
|
872
|
-
|
|
984
|
+
const dsn = message.envelope.dsn;
|
|
985
|
+
if (dsn != null && !this.capabilities.some((capability) => /^DSN[ \t]*$/i.test(capability))) throw new SmtpDsnUnsupportedError();
|
|
986
|
+
const mailDsnParameters = dsn == null || dsn.mailParameters.length === 0 ? "" : ` ${dsn.mailParameters.join(" ")}`;
|
|
987
|
+
const mailCommand = `MAIL FROM:<${message.envelope.from}>${sizeParameter}${mailDsnParameters}`;
|
|
988
|
+
const recipientCommands = message.envelope.to.map((recipient, index) => {
|
|
989
|
+
const parameters = dsn?.recipientParameters[index] ?? [];
|
|
990
|
+
const suffix = parameters.length === 0 ? "" : ` ${parameters.join(" ")}`;
|
|
991
|
+
return `RCPT TO:<${recipient}>${suffix}`;
|
|
992
|
+
});
|
|
873
993
|
const pipelining = this.capabilities.some((capability) => /^PIPELINING(?:\s|$)/i.test(capability));
|
|
874
994
|
let mailResponse;
|
|
875
995
|
let recipientResponses;
|
|
@@ -1296,18 +1416,20 @@ function arrayBufferToBase64(buffer) {
|
|
|
1296
1416
|
*
|
|
1297
1417
|
* @param message The message to convert.
|
|
1298
1418
|
* @param dkimConfig Optional DKIM signing configuration.
|
|
1419
|
+
* @param dsn Optional validated SMTP delivery status notification parameters.
|
|
1299
1420
|
* @returns The converted SMTP message.
|
|
1300
1421
|
* @throws {RangeError} If a header contains a token that cannot be folded
|
|
1301
1422
|
* within the RFC 5322 hard line-length limit.
|
|
1302
1423
|
*/
|
|
1303
|
-
async function convertMessage(message, dkimConfig) {
|
|
1424
|
+
async function convertMessage(message, dkimConfig, dsn) {
|
|
1304
1425
|
const envelope = {
|
|
1305
1426
|
from: message.sender.address,
|
|
1306
1427
|
to: [
|
|
1307
1428
|
...message.recipients.map((r) => r.address),
|
|
1308
1429
|
...message.ccRecipients.map((r) => r.address),
|
|
1309
1430
|
...message.bccRecipients.map((r) => r.address)
|
|
1310
|
-
]
|
|
1431
|
+
],
|
|
1432
|
+
dsn
|
|
1311
1433
|
};
|
|
1312
1434
|
let raw = await buildRawMessage(message);
|
|
1313
1435
|
if (dkimConfig) try {
|
|
@@ -1621,9 +1743,10 @@ var SmtpTransport = class {
|
|
|
1621
1743
|
options?.signal?.throwIfAborted();
|
|
1622
1744
|
let connection;
|
|
1623
1745
|
try {
|
|
1746
|
+
const dsn = resolveSmtpDsn(message, options?.dsn);
|
|
1624
1747
|
connection = await this.getConnection(options?.signal);
|
|
1625
1748
|
options?.signal?.throwIfAborted();
|
|
1626
|
-
const smtpMessage = await convertMessage(message, this.config.dkim);
|
|
1749
|
+
const smtpMessage = await convertMessage(message, this.config.dkim, dsn);
|
|
1627
1750
|
options?.signal?.throwIfAborted();
|
|
1628
1751
|
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1629
1752
|
await this.returnConnection(connection);
|
|
@@ -1634,7 +1757,7 @@ var SmtpTransport = class {
|
|
|
1634
1757
|
rejectedRecipients: result.rejectedRecipients
|
|
1635
1758
|
};
|
|
1636
1759
|
} catch (error) {
|
|
1637
|
-
if (connection != null) if (error
|
|
1760
|
+
if (connection != null) if (isReusableLocalFailure(error)) await this.returnConnection(connection);
|
|
1638
1761
|
else await this.discardConnection(connection);
|
|
1639
1762
|
options?.signal?.throwIfAborted();
|
|
1640
1763
|
return createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
@@ -1694,7 +1817,8 @@ var SmtpTransport = class {
|
|
|
1694
1817
|
continue;
|
|
1695
1818
|
}
|
|
1696
1819
|
try {
|
|
1697
|
-
const
|
|
1820
|
+
const dsn = resolveSmtpDsn(message, options?.dsn);
|
|
1821
|
+
const smtpMessage = await convertMessage(message, this.config.dkim, dsn);
|
|
1698
1822
|
options?.signal?.throwIfAborted();
|
|
1699
1823
|
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1700
1824
|
yield {
|
|
@@ -1705,7 +1829,7 @@ var SmtpTransport = class {
|
|
|
1705
1829
|
};
|
|
1706
1830
|
} catch (error) {
|
|
1707
1831
|
options?.signal?.throwIfAborted();
|
|
1708
|
-
if (!(error
|
|
1832
|
+
if (!isReusableLocalFailure(error)) connectionValid = false;
|
|
1709
1833
|
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1710
1834
|
}
|
|
1711
1835
|
}
|
|
@@ -1716,7 +1840,8 @@ var SmtpTransport = class {
|
|
|
1716
1840
|
continue;
|
|
1717
1841
|
}
|
|
1718
1842
|
try {
|
|
1719
|
-
const
|
|
1843
|
+
const dsn = resolveSmtpDsn(message, options?.dsn);
|
|
1844
|
+
const smtpMessage = await convertMessage(message, this.config.dkim, dsn);
|
|
1720
1845
|
options?.signal?.throwIfAborted();
|
|
1721
1846
|
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1722
1847
|
yield {
|
|
@@ -1727,7 +1852,7 @@ var SmtpTransport = class {
|
|
|
1727
1852
|
};
|
|
1728
1853
|
} catch (error) {
|
|
1729
1854
|
options?.signal?.throwIfAborted();
|
|
1730
|
-
if (!(error
|
|
1855
|
+
if (!isReusableLocalFailure(error)) connectionValid = false;
|
|
1731
1856
|
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1732
1857
|
}
|
|
1733
1858
|
}
|
|
@@ -1822,6 +1947,20 @@ var SmtpTransport = class {
|
|
|
1822
1947
|
}
|
|
1823
1948
|
};
|
|
1824
1949
|
function createSmtpFailure(message, error) {
|
|
1950
|
+
if (error instanceof SmtpDsnValidationError) return (0, __upyo_core.createFailedReceipt)(message, {
|
|
1951
|
+
provider: "smtp",
|
|
1952
|
+
code: "smtp.dsn-invalid",
|
|
1953
|
+
category: "validation",
|
|
1954
|
+
retryable: false,
|
|
1955
|
+
attempts: 1
|
|
1956
|
+
});
|
|
1957
|
+
if (error instanceof SmtpDsnUnsupportedError) return (0, __upyo_core.createFailedReceipt)(message, {
|
|
1958
|
+
provider: "smtp",
|
|
1959
|
+
code: "smtp.dsn-unsupported",
|
|
1960
|
+
category: "configuration",
|
|
1961
|
+
retryable: false,
|
|
1962
|
+
attempts: 1
|
|
1963
|
+
});
|
|
1825
1964
|
if (error instanceof SmtpMessageSizeError) return (0, __upyo_core.createFailedReceipt)(message, {
|
|
1826
1965
|
provider: "smtp",
|
|
1827
1966
|
code: "smtp.message-size-exceeded",
|
|
@@ -1853,6 +1992,9 @@ function createSmtpFailure(message, error) {
|
|
|
1853
1992
|
attempts: 1
|
|
1854
1993
|
});
|
|
1855
1994
|
}
|
|
1995
|
+
function isReusableLocalFailure(error) {
|
|
1996
|
+
return error instanceof SmtpMessageSizeError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
|
|
1997
|
+
}
|
|
1856
1998
|
function classifySmtpReply(code) {
|
|
1857
1999
|
if (code >= 400 && code < 500) return {
|
|
1858
2000
|
category: "service-unavailable",
|
|
@@ -1870,4 +2012,6 @@ function classifySmtpReply(code) {
|
|
|
1870
2012
|
|
|
1871
2013
|
//#endregion
|
|
1872
2014
|
exports.SmtpAuthError = SmtpAuthError;
|
|
2015
|
+
exports.SmtpDsnUnsupportedError = SmtpDsnUnsupportedError;
|
|
2016
|
+
exports.SmtpDsnValidationError = SmtpDsnValidationError;
|
|
1873
2017
|
exports.SmtpTransport = SmtpTransport;
|
package/dist/index.d.cts
CHANGED
|
@@ -417,6 +417,98 @@ interface SmtpTlsOptions {
|
|
|
417
417
|
* used internally by the SMTP transport implementation.
|
|
418
418
|
*/
|
|
419
419
|
//#endregion
|
|
420
|
+
//#region src/delivery-status.d.ts
|
|
421
|
+
/**
|
|
422
|
+
* A condition under which an SMTP server should issue a delivery status
|
|
423
|
+
* notification for a recipient.
|
|
424
|
+
*
|
|
425
|
+
* `"never"` must be the only condition when it is used.
|
|
426
|
+
*
|
|
427
|
+
* @since 0.6.0
|
|
428
|
+
*/
|
|
429
|
+
type SmtpDsnNotification = "never" | "success" | "failure" | "delay";
|
|
430
|
+
/**
|
|
431
|
+
* Delivery status notification settings for one SMTP envelope recipient.
|
|
432
|
+
*
|
|
433
|
+
* @since 0.6.0
|
|
434
|
+
*/
|
|
435
|
+
interface SmtpDsnRecipientOptions {
|
|
436
|
+
/**
|
|
437
|
+
* Conditions under which the server should issue a notification.
|
|
438
|
+
*
|
|
439
|
+
* `"never"` must appear by itself. When omitted, the server applies its
|
|
440
|
+
* default behavior, which is normally equivalent to `"failure"` or to
|
|
441
|
+
* `"failure"` together with `"delay"`.
|
|
442
|
+
*/
|
|
443
|
+
readonly notify?: readonly SmtpDsnNotification[];
|
|
444
|
+
/**
|
|
445
|
+
* The original Internet mail address to identify in notifications.
|
|
446
|
+
*
|
|
447
|
+
* Upyo serializes this as an RFC 3461 `ORCPT` parameter with the `rfc822`
|
|
448
|
+
* address type. For an initial submission, RFC 3461 requires this value to
|
|
449
|
+
* equal the corresponding envelope recipient address.
|
|
450
|
+
*/
|
|
451
|
+
readonly originalRecipient?: string;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* SMTP delivery status notification settings for one message.
|
|
455
|
+
*
|
|
456
|
+
* These settings are serialized as SMTP envelope parameters and are not added
|
|
457
|
+
* to the message headers.
|
|
458
|
+
*
|
|
459
|
+
* @since 0.6.0
|
|
460
|
+
*/
|
|
461
|
+
interface SmtpDsnOptions {
|
|
462
|
+
/**
|
|
463
|
+
* An identifier that will be returned in delivery status notifications.
|
|
464
|
+
* Serialized as the RFC 3461 `ENVID` parameter. The identifier must not be
|
|
465
|
+
* empty.
|
|
466
|
+
*/
|
|
467
|
+
readonly envelopeId?: string;
|
|
468
|
+
/**
|
|
469
|
+
* How much of the original message a failure notification should return.
|
|
470
|
+
* Serialized as `RET=FULL` or `RET=HDRS`.
|
|
471
|
+
*/
|
|
472
|
+
readonly return?: "full" | "headers";
|
|
473
|
+
/**
|
|
474
|
+
* Per-recipient notification settings, keyed by exact envelope address.
|
|
475
|
+
*/
|
|
476
|
+
readonly recipients?: Readonly<Record<string, SmtpDsnRecipientOptions>>;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* SMTP-specific options for sending messages.
|
|
480
|
+
*
|
|
481
|
+
* @since 0.6.0
|
|
482
|
+
*/
|
|
483
|
+
interface SmtpTransportOptions extends TransportOptions {
|
|
484
|
+
/** Delivery status notification settings for this SMTP transaction. */
|
|
485
|
+
readonly dsn?: SmtpDsnOptions;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Error produced when SMTP delivery status notification settings are invalid.
|
|
489
|
+
*
|
|
490
|
+
* @since 0.6.0
|
|
491
|
+
*/
|
|
492
|
+
declare class SmtpDsnValidationError extends TypeError {
|
|
493
|
+
/**
|
|
494
|
+
* Creates a delivery status notification validation error.
|
|
495
|
+
*
|
|
496
|
+
* @param message A description of the invalid setting.
|
|
497
|
+
*/
|
|
498
|
+
constructor(message: string);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Error produced when delivery status notifications were requested from an
|
|
502
|
+
* SMTP server that did not advertise the RFC 3461 `DSN` extension.
|
|
503
|
+
*
|
|
504
|
+
* @since 0.6.0
|
|
505
|
+
*/
|
|
506
|
+
declare class SmtpDsnUnsupportedError extends Error {
|
|
507
|
+
/** Creates an error for a server without the `DSN` extension. */
|
|
508
|
+
constructor();
|
|
509
|
+
}
|
|
510
|
+
/** @internal */
|
|
511
|
+
//#endregion
|
|
420
512
|
//#region src/smtp-receipt.d.ts
|
|
421
513
|
/**
|
|
422
514
|
* An SMTP envelope recipient that the server rejected during an otherwise
|
|
@@ -539,7 +631,7 @@ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
|
|
|
539
631
|
* @throws {DOMException} If the operation is aborted through
|
|
540
632
|
* `options.signal`.
|
|
541
633
|
*/
|
|
542
|
-
send(message: Message, options?:
|
|
634
|
+
send(message: Message, options?: SmtpTransportOptions): Promise<SmtpReceipt>;
|
|
543
635
|
/**
|
|
544
636
|
* Sends multiple email messages efficiently using a single SMTP connection.
|
|
545
637
|
*
|
|
@@ -570,7 +662,7 @@ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
|
|
|
570
662
|
* @throws {DOMException} If the operation is aborted through
|
|
571
663
|
* `options.signal`.
|
|
572
664
|
*/
|
|
573
|
-
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?:
|
|
665
|
+
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: SmtpTransportOptions): AsyncIterable<SmtpReceipt>;
|
|
574
666
|
private getConnection;
|
|
575
667
|
private connectAndSetup;
|
|
576
668
|
private returnConnection;
|
|
@@ -635,4 +727,4 @@ declare class SmtpAuthError extends Error {
|
|
|
635
727
|
*/
|
|
636
728
|
|
|
637
729
|
//#endregion
|
|
638
|
-
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpReceipt, SmtpRejectedRecipient, SmtpTlsOptions, SmtpTransport, SmtpUserPassAuth };
|
|
730
|
+
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpDsnNotification, SmtpDsnOptions, SmtpDsnRecipientOptions, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpReceipt, SmtpRejectedRecipient, SmtpTlsOptions, SmtpTransport, SmtpTransportOptions, SmtpUserPassAuth };
|
package/dist/index.d.ts
CHANGED
|
@@ -417,6 +417,98 @@ interface SmtpTlsOptions {
|
|
|
417
417
|
* used internally by the SMTP transport implementation.
|
|
418
418
|
*/
|
|
419
419
|
//#endregion
|
|
420
|
+
//#region src/delivery-status.d.ts
|
|
421
|
+
/**
|
|
422
|
+
* A condition under which an SMTP server should issue a delivery status
|
|
423
|
+
* notification for a recipient.
|
|
424
|
+
*
|
|
425
|
+
* `"never"` must be the only condition when it is used.
|
|
426
|
+
*
|
|
427
|
+
* @since 0.6.0
|
|
428
|
+
*/
|
|
429
|
+
type SmtpDsnNotification = "never" | "success" | "failure" | "delay";
|
|
430
|
+
/**
|
|
431
|
+
* Delivery status notification settings for one SMTP envelope recipient.
|
|
432
|
+
*
|
|
433
|
+
* @since 0.6.0
|
|
434
|
+
*/
|
|
435
|
+
interface SmtpDsnRecipientOptions {
|
|
436
|
+
/**
|
|
437
|
+
* Conditions under which the server should issue a notification.
|
|
438
|
+
*
|
|
439
|
+
* `"never"` must appear by itself. When omitted, the server applies its
|
|
440
|
+
* default behavior, which is normally equivalent to `"failure"` or to
|
|
441
|
+
* `"failure"` together with `"delay"`.
|
|
442
|
+
*/
|
|
443
|
+
readonly notify?: readonly SmtpDsnNotification[];
|
|
444
|
+
/**
|
|
445
|
+
* The original Internet mail address to identify in notifications.
|
|
446
|
+
*
|
|
447
|
+
* Upyo serializes this as an RFC 3461 `ORCPT` parameter with the `rfc822`
|
|
448
|
+
* address type. For an initial submission, RFC 3461 requires this value to
|
|
449
|
+
* equal the corresponding envelope recipient address.
|
|
450
|
+
*/
|
|
451
|
+
readonly originalRecipient?: string;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* SMTP delivery status notification settings for one message.
|
|
455
|
+
*
|
|
456
|
+
* These settings are serialized as SMTP envelope parameters and are not added
|
|
457
|
+
* to the message headers.
|
|
458
|
+
*
|
|
459
|
+
* @since 0.6.0
|
|
460
|
+
*/
|
|
461
|
+
interface SmtpDsnOptions {
|
|
462
|
+
/**
|
|
463
|
+
* An identifier that will be returned in delivery status notifications.
|
|
464
|
+
* Serialized as the RFC 3461 `ENVID` parameter. The identifier must not be
|
|
465
|
+
* empty.
|
|
466
|
+
*/
|
|
467
|
+
readonly envelopeId?: string;
|
|
468
|
+
/**
|
|
469
|
+
* How much of the original message a failure notification should return.
|
|
470
|
+
* Serialized as `RET=FULL` or `RET=HDRS`.
|
|
471
|
+
*/
|
|
472
|
+
readonly return?: "full" | "headers";
|
|
473
|
+
/**
|
|
474
|
+
* Per-recipient notification settings, keyed by exact envelope address.
|
|
475
|
+
*/
|
|
476
|
+
readonly recipients?: Readonly<Record<string, SmtpDsnRecipientOptions>>;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* SMTP-specific options for sending messages.
|
|
480
|
+
*
|
|
481
|
+
* @since 0.6.0
|
|
482
|
+
*/
|
|
483
|
+
interface SmtpTransportOptions extends TransportOptions {
|
|
484
|
+
/** Delivery status notification settings for this SMTP transaction. */
|
|
485
|
+
readonly dsn?: SmtpDsnOptions;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Error produced when SMTP delivery status notification settings are invalid.
|
|
489
|
+
*
|
|
490
|
+
* @since 0.6.0
|
|
491
|
+
*/
|
|
492
|
+
declare class SmtpDsnValidationError extends TypeError {
|
|
493
|
+
/**
|
|
494
|
+
* Creates a delivery status notification validation error.
|
|
495
|
+
*
|
|
496
|
+
* @param message A description of the invalid setting.
|
|
497
|
+
*/
|
|
498
|
+
constructor(message: string);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Error produced when delivery status notifications were requested from an
|
|
502
|
+
* SMTP server that did not advertise the RFC 3461 `DSN` extension.
|
|
503
|
+
*
|
|
504
|
+
* @since 0.6.0
|
|
505
|
+
*/
|
|
506
|
+
declare class SmtpDsnUnsupportedError extends Error {
|
|
507
|
+
/** Creates an error for a server without the `DSN` extension. */
|
|
508
|
+
constructor();
|
|
509
|
+
}
|
|
510
|
+
/** @internal */
|
|
511
|
+
//#endregion
|
|
420
512
|
//#region src/smtp-receipt.d.ts
|
|
421
513
|
/**
|
|
422
514
|
* An SMTP envelope recipient that the server rejected during an otherwise
|
|
@@ -539,7 +631,7 @@ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
|
|
|
539
631
|
* @throws {DOMException} If the operation is aborted through
|
|
540
632
|
* `options.signal`.
|
|
541
633
|
*/
|
|
542
|
-
send(message: Message, options?:
|
|
634
|
+
send(message: Message, options?: SmtpTransportOptions): Promise<SmtpReceipt>;
|
|
543
635
|
/**
|
|
544
636
|
* Sends multiple email messages efficiently using a single SMTP connection.
|
|
545
637
|
*
|
|
@@ -570,7 +662,7 @@ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
|
|
|
570
662
|
* @throws {DOMException} If the operation is aborted through
|
|
571
663
|
* `options.signal`.
|
|
572
664
|
*/
|
|
573
|
-
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?:
|
|
665
|
+
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: SmtpTransportOptions): AsyncIterable<SmtpReceipt>;
|
|
574
666
|
private getConnection;
|
|
575
667
|
private connectAndSetup;
|
|
576
668
|
private returnConnection;
|
|
@@ -635,4 +727,4 @@ declare class SmtpAuthError extends Error {
|
|
|
635
727
|
*/
|
|
636
728
|
|
|
637
729
|
//#endregion
|
|
638
|
-
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpReceipt, SmtpRejectedRecipient, SmtpTlsOptions, SmtpTransport, SmtpUserPassAuth };
|
|
730
|
+
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpDsnNotification, SmtpDsnOptions, SmtpDsnRecipientOptions, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpReceipt, SmtpRejectedRecipient, SmtpTlsOptions, SmtpTransport, SmtpTransportOptions, SmtpUserPassAuth };
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,119 @@ import { Buffer } from "node:buffer";
|
|
|
3
3
|
import { Socket } from "node:net";
|
|
4
4
|
import { TLSSocket, connect } from "node:tls";
|
|
5
5
|
|
|
6
|
+
//#region src/delivery-status.ts
|
|
7
|
+
/**
|
|
8
|
+
* Error produced when SMTP delivery status notification settings are invalid.
|
|
9
|
+
*
|
|
10
|
+
* @since 0.6.0
|
|
11
|
+
*/
|
|
12
|
+
var SmtpDsnValidationError = class extends TypeError {
|
|
13
|
+
/**
|
|
14
|
+
* Creates a delivery status notification validation error.
|
|
15
|
+
*
|
|
16
|
+
* @param message A description of the invalid setting.
|
|
17
|
+
*/
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "SmtpDsnValidationError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Error produced when delivery status notifications were requested from an
|
|
25
|
+
* SMTP server that did not advertise the RFC 3461 `DSN` extension.
|
|
26
|
+
*
|
|
27
|
+
* @since 0.6.0
|
|
28
|
+
*/
|
|
29
|
+
var SmtpDsnUnsupportedError = class extends Error {
|
|
30
|
+
/** Creates an error for a server without the `DSN` extension. */
|
|
31
|
+
constructor() {
|
|
32
|
+
super("Delivery status notifications were requested, but the server does not advertise the DSN extension.");
|
|
33
|
+
this.name = "SmtpDsnUnsupportedError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const NOTIFICATION_CONDITIONS = new Set([
|
|
37
|
+
"never",
|
|
38
|
+
"success",
|
|
39
|
+
"failure",
|
|
40
|
+
"delay"
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* Validates and serializes the RFC 3461 envelope parameters for a message.
|
|
44
|
+
*
|
|
45
|
+
* @param message The message whose SMTP envelope will carry the parameters.
|
|
46
|
+
* @param dsn The caller-supplied delivery status notification settings.
|
|
47
|
+
* @returns Serialized parameters, or `undefined` when none were requested.
|
|
48
|
+
* @throws {SmtpDsnValidationError} If any setting violates RFC 3461 or does
|
|
49
|
+
* not correspond to the message envelope.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
function resolveSmtpDsn(message, dsn) {
|
|
53
|
+
if (dsn == null) return void 0;
|
|
54
|
+
if (typeof dsn !== "object" || Array.isArray(dsn)) throw new SmtpDsnValidationError("DSN options must be an object.");
|
|
55
|
+
const mailParameters = [];
|
|
56
|
+
if (dsn.return != null) {
|
|
57
|
+
if (dsn.return !== "full" && dsn.return !== "headers") throw new SmtpDsnValidationError(`Unsupported DSN return value: ${String(dsn.return)}`);
|
|
58
|
+
mailParameters.push(`RET=${dsn.return === "full" ? "FULL" : "HDRS"}`);
|
|
59
|
+
}
|
|
60
|
+
if (dsn.envelopeId != null) {
|
|
61
|
+
if (dsn.envelopeId === "") throw new SmtpDsnValidationError("DSN envelope ID must not be empty.");
|
|
62
|
+
const encoded = encodeXtext(dsn.envelopeId, "DSN envelope ID");
|
|
63
|
+
const parameter = `ENVID=${encoded}`;
|
|
64
|
+
assertParameterLength(parameter, 100, "ENVID");
|
|
65
|
+
mailParameters.push(parameter);
|
|
66
|
+
}
|
|
67
|
+
const envelopeRecipients = [
|
|
68
|
+
...message.recipients.map((recipient) => recipient.address),
|
|
69
|
+
...message.ccRecipients.map((recipient) => recipient.address),
|
|
70
|
+
...message.bccRecipients.map((recipient) => recipient.address)
|
|
71
|
+
];
|
|
72
|
+
const envelopeRecipientSet = new Set(envelopeRecipients);
|
|
73
|
+
const configuredRecipients = dsn.recipients;
|
|
74
|
+
if (configuredRecipients != null && (typeof configuredRecipients !== "object" || Array.isArray(configuredRecipients))) throw new SmtpDsnValidationError("DSN recipient options must be an object.");
|
|
75
|
+
const serializedByAddress = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const [address, options] of Object.entries(configuredRecipients ?? {})) {
|
|
77
|
+
if (!envelopeRecipientSet.has(address)) throw new SmtpDsnValidationError(`DSN recipient ${address} is not an envelope recipient.`);
|
|
78
|
+
if (options == null || typeof options !== "object" || Array.isArray(options)) throw new SmtpDsnValidationError(`DSN options for ${address} must be an object.`);
|
|
79
|
+
const parameters = [];
|
|
80
|
+
if (options.notify != null) {
|
|
81
|
+
const notify = options.notify;
|
|
82
|
+
if (!Array.isArray(notify) || notify.length === 0) throw new SmtpDsnValidationError(`DSN notification conditions for ${address} must be a non-empty array.`);
|
|
83
|
+
for (const condition of notify) if (typeof condition !== "string" || !NOTIFICATION_CONDITIONS.has(condition)) throw new SmtpDsnValidationError(`Unsupported DSN notification condition: ${String(condition)}`);
|
|
84
|
+
if (notify.includes("never") && notify.length !== 1) throw new SmtpDsnValidationError("The DSN notification condition NEVER must appear by itself.");
|
|
85
|
+
parameters.push(`NOTIFY=${notify.map((condition) => condition.toUpperCase()).join(",")}`);
|
|
86
|
+
}
|
|
87
|
+
if (options.originalRecipient != null) {
|
|
88
|
+
const encoded = encodeXtext(options.originalRecipient, `DSN original recipient for ${address}`);
|
|
89
|
+
if (options.originalRecipient !== address) throw new SmtpDsnValidationError(`DSN original recipient for ${address} must match the envelope recipient address.`);
|
|
90
|
+
const parameter = `ORCPT=rfc822;${encoded}`;
|
|
91
|
+
assertParameterLength(parameter, 500, "ORCPT");
|
|
92
|
+
parameters.push(parameter);
|
|
93
|
+
}
|
|
94
|
+
serializedByAddress.set(address, parameters);
|
|
95
|
+
}
|
|
96
|
+
const recipientParameters = envelopeRecipients.map((address) => serializedByAddress.get(address) ?? []);
|
|
97
|
+
const requested = mailParameters.length > 0 || recipientParameters.some((parameters) => parameters.length > 0);
|
|
98
|
+
return requested ? {
|
|
99
|
+
mailParameters,
|
|
100
|
+
recipientParameters
|
|
101
|
+
} : void 0;
|
|
102
|
+
}
|
|
103
|
+
function encodeXtext(value, name) {
|
|
104
|
+
if (typeof value !== "string") throw new SmtpDsnValidationError(`${name} must be a string.`);
|
|
105
|
+
let encoded = "";
|
|
106
|
+
for (let index = 0; index < value.length; index++) {
|
|
107
|
+
const code = value.charCodeAt(index);
|
|
108
|
+
if (code < 32 || code > 126) throw new SmtpDsnValidationError(`${name} must contain only printable US-ASCII characters.`);
|
|
109
|
+
if (code >= 33 && code <= 126 && code !== 43 && code !== 61) encoded += value[index];
|
|
110
|
+
else encoded += `+${code.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
111
|
+
}
|
|
112
|
+
return encoded;
|
|
113
|
+
}
|
|
114
|
+
function assertParameterLength(parameter, maximum, name) {
|
|
115
|
+
if (parameter.length > maximum) throw new SmtpDsnValidationError(`${name} parameter exceeds the RFC 3461 limit of ${maximum} characters.`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
6
119
|
//#region src/config.ts
|
|
7
120
|
/**
|
|
8
121
|
* Creates a resolved SMTP configuration by applying default values to optional fields.
|
|
@@ -845,8 +958,15 @@ var SmtpConnection = class {
|
|
|
845
958
|
if (sizeCapability.maximum != null && BigInt(messageSize) > sizeCapability.maximum) throw new SmtpMessageSizeError(messageSize, sizeCapability.maximum);
|
|
846
959
|
sizeParameter = ` SIZE=${messageSize}`;
|
|
847
960
|
}
|
|
848
|
-
const
|
|
849
|
-
|
|
961
|
+
const dsn = message.envelope.dsn;
|
|
962
|
+
if (dsn != null && !this.capabilities.some((capability) => /^DSN[ \t]*$/i.test(capability))) throw new SmtpDsnUnsupportedError();
|
|
963
|
+
const mailDsnParameters = dsn == null || dsn.mailParameters.length === 0 ? "" : ` ${dsn.mailParameters.join(" ")}`;
|
|
964
|
+
const mailCommand = `MAIL FROM:<${message.envelope.from}>${sizeParameter}${mailDsnParameters}`;
|
|
965
|
+
const recipientCommands = message.envelope.to.map((recipient, index) => {
|
|
966
|
+
const parameters = dsn?.recipientParameters[index] ?? [];
|
|
967
|
+
const suffix = parameters.length === 0 ? "" : ` ${parameters.join(" ")}`;
|
|
968
|
+
return `RCPT TO:<${recipient}>${suffix}`;
|
|
969
|
+
});
|
|
850
970
|
const pipelining = this.capabilities.some((capability) => /^PIPELINING(?:\s|$)/i.test(capability));
|
|
851
971
|
let mailResponse;
|
|
852
972
|
let recipientResponses;
|
|
@@ -1273,18 +1393,20 @@ function arrayBufferToBase64(buffer) {
|
|
|
1273
1393
|
*
|
|
1274
1394
|
* @param message The message to convert.
|
|
1275
1395
|
* @param dkimConfig Optional DKIM signing configuration.
|
|
1396
|
+
* @param dsn Optional validated SMTP delivery status notification parameters.
|
|
1276
1397
|
* @returns The converted SMTP message.
|
|
1277
1398
|
* @throws {RangeError} If a header contains a token that cannot be folded
|
|
1278
1399
|
* within the RFC 5322 hard line-length limit.
|
|
1279
1400
|
*/
|
|
1280
|
-
async function convertMessage(message, dkimConfig) {
|
|
1401
|
+
async function convertMessage(message, dkimConfig, dsn) {
|
|
1281
1402
|
const envelope = {
|
|
1282
1403
|
from: message.sender.address,
|
|
1283
1404
|
to: [
|
|
1284
1405
|
...message.recipients.map((r) => r.address),
|
|
1285
1406
|
...message.ccRecipients.map((r) => r.address),
|
|
1286
1407
|
...message.bccRecipients.map((r) => r.address)
|
|
1287
|
-
]
|
|
1408
|
+
],
|
|
1409
|
+
dsn
|
|
1288
1410
|
};
|
|
1289
1411
|
let raw = await buildRawMessage(message);
|
|
1290
1412
|
if (dkimConfig) try {
|
|
@@ -1598,9 +1720,10 @@ var SmtpTransport = class {
|
|
|
1598
1720
|
options?.signal?.throwIfAborted();
|
|
1599
1721
|
let connection;
|
|
1600
1722
|
try {
|
|
1723
|
+
const dsn = resolveSmtpDsn(message, options?.dsn);
|
|
1601
1724
|
connection = await this.getConnection(options?.signal);
|
|
1602
1725
|
options?.signal?.throwIfAborted();
|
|
1603
|
-
const smtpMessage = await convertMessage(message, this.config.dkim);
|
|
1726
|
+
const smtpMessage = await convertMessage(message, this.config.dkim, dsn);
|
|
1604
1727
|
options?.signal?.throwIfAborted();
|
|
1605
1728
|
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1606
1729
|
await this.returnConnection(connection);
|
|
@@ -1611,7 +1734,7 @@ var SmtpTransport = class {
|
|
|
1611
1734
|
rejectedRecipients: result.rejectedRecipients
|
|
1612
1735
|
};
|
|
1613
1736
|
} catch (error) {
|
|
1614
|
-
if (connection != null) if (error
|
|
1737
|
+
if (connection != null) if (isReusableLocalFailure(error)) await this.returnConnection(connection);
|
|
1615
1738
|
else await this.discardConnection(connection);
|
|
1616
1739
|
options?.signal?.throwIfAborted();
|
|
1617
1740
|
return createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
@@ -1671,7 +1794,8 @@ var SmtpTransport = class {
|
|
|
1671
1794
|
continue;
|
|
1672
1795
|
}
|
|
1673
1796
|
try {
|
|
1674
|
-
const
|
|
1797
|
+
const dsn = resolveSmtpDsn(message, options?.dsn);
|
|
1798
|
+
const smtpMessage = await convertMessage(message, this.config.dkim, dsn);
|
|
1675
1799
|
options?.signal?.throwIfAborted();
|
|
1676
1800
|
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1677
1801
|
yield {
|
|
@@ -1682,7 +1806,7 @@ var SmtpTransport = class {
|
|
|
1682
1806
|
};
|
|
1683
1807
|
} catch (error) {
|
|
1684
1808
|
options?.signal?.throwIfAborted();
|
|
1685
|
-
if (!(error
|
|
1809
|
+
if (!isReusableLocalFailure(error)) connectionValid = false;
|
|
1686
1810
|
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1687
1811
|
}
|
|
1688
1812
|
}
|
|
@@ -1693,7 +1817,8 @@ var SmtpTransport = class {
|
|
|
1693
1817
|
continue;
|
|
1694
1818
|
}
|
|
1695
1819
|
try {
|
|
1696
|
-
const
|
|
1820
|
+
const dsn = resolveSmtpDsn(message, options?.dsn);
|
|
1821
|
+
const smtpMessage = await convertMessage(message, this.config.dkim, dsn);
|
|
1697
1822
|
options?.signal?.throwIfAborted();
|
|
1698
1823
|
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1699
1824
|
yield {
|
|
@@ -1704,7 +1829,7 @@ var SmtpTransport = class {
|
|
|
1704
1829
|
};
|
|
1705
1830
|
} catch (error) {
|
|
1706
1831
|
options?.signal?.throwIfAborted();
|
|
1707
|
-
if (!(error
|
|
1832
|
+
if (!isReusableLocalFailure(error)) connectionValid = false;
|
|
1708
1833
|
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1709
1834
|
}
|
|
1710
1835
|
}
|
|
@@ -1799,6 +1924,20 @@ var SmtpTransport = class {
|
|
|
1799
1924
|
}
|
|
1800
1925
|
};
|
|
1801
1926
|
function createSmtpFailure(message, error) {
|
|
1927
|
+
if (error instanceof SmtpDsnValidationError) return createFailedReceipt(message, {
|
|
1928
|
+
provider: "smtp",
|
|
1929
|
+
code: "smtp.dsn-invalid",
|
|
1930
|
+
category: "validation",
|
|
1931
|
+
retryable: false,
|
|
1932
|
+
attempts: 1
|
|
1933
|
+
});
|
|
1934
|
+
if (error instanceof SmtpDsnUnsupportedError) return createFailedReceipt(message, {
|
|
1935
|
+
provider: "smtp",
|
|
1936
|
+
code: "smtp.dsn-unsupported",
|
|
1937
|
+
category: "configuration",
|
|
1938
|
+
retryable: false,
|
|
1939
|
+
attempts: 1
|
|
1940
|
+
});
|
|
1802
1941
|
if (error instanceof SmtpMessageSizeError) return createFailedReceipt(message, {
|
|
1803
1942
|
provider: "smtp",
|
|
1804
1943
|
code: "smtp.message-size-exceeded",
|
|
@@ -1830,6 +1969,9 @@ function createSmtpFailure(message, error) {
|
|
|
1830
1969
|
attempts: 1
|
|
1831
1970
|
});
|
|
1832
1971
|
}
|
|
1972
|
+
function isReusableLocalFailure(error) {
|
|
1973
|
+
return error instanceof SmtpMessageSizeError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
|
|
1974
|
+
}
|
|
1833
1975
|
function classifySmtpReply(code) {
|
|
1834
1976
|
if (code >= 400 && code < 500) return {
|
|
1835
1977
|
category: "service-unavailable",
|
|
@@ -1846,4 +1988,4 @@ function classifySmtpReply(code) {
|
|
|
1846
1988
|
}
|
|
1847
1989
|
|
|
1848
1990
|
//#endregion
|
|
1849
|
-
export { SmtpAuthError, SmtpTransport };
|
|
1991
|
+
export { SmtpAuthError, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpTransport };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upyo/smtp",
|
|
3
|
-
"version": "0.6.0-dev.
|
|
3
|
+
"version": "0.6.0-dev.294",
|
|
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.
|
|
56
|
+
"@upyo/core": "0.6.0-dev.294+88e02bad"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"tsdown": "^0.12.7",
|