@upyo/smtp 0.6.0-dev.305 → 0.6.0-dev.306
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 +45 -7
- package/dist/index.cjs +137 -70
- package/dist/index.d.cts +61 -6
- package/dist/index.d.ts +61 -6
- package/dist/index.js +138 -72
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -219,13 +219,50 @@ structure.
|
|
|
219
219
|
[RFC 3463]: https://www.rfc-editor.org/rfc/rfc3463
|
|
220
220
|
|
|
221
221
|
|
|
222
|
+
Envelope overrides
|
|
223
|
+
------------------
|
|
224
|
+
|
|
225
|
+
Use the `envelope` send option when the SMTP reverse-path or recipients must
|
|
226
|
+
differ from the visible message headers:
|
|
227
|
+
|
|
228
|
+
~~~~ typescript
|
|
229
|
+
const receipt = await transport.send(message, {
|
|
230
|
+
envelope: {
|
|
231
|
+
from: "bounces+customer-42@bounce.example.com",
|
|
232
|
+
to: ["delivery@example.net"],
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
~~~~
|
|
236
|
+
|
|
237
|
+
Omitting `from` or `to` keeps the value derived from the message. Set `from`
|
|
238
|
+
to `null` to send `MAIL FROM:<>`. The override changes only `MAIL FROM` and
|
|
239
|
+
`RCPT TO`; the From, To, Cc, Bcc, and Reply-To message fields remain unchanged.
|
|
240
|
+
|
|
241
|
+
For bulk VERP delivery, pass a resolver that returns an override for each
|
|
242
|
+
message:
|
|
243
|
+
|
|
244
|
+
~~~~ typescript
|
|
245
|
+
for await (const receipt of transport.sendMany(messages, {
|
|
246
|
+
envelope: (_message, index) => ({
|
|
247
|
+
from: `bounces+${index}@bounce.example.com`,
|
|
248
|
+
}),
|
|
249
|
+
})) {
|
|
250
|
+
console.log(receipt);
|
|
251
|
+
}
|
|
252
|
+
~~~~
|
|
253
|
+
|
|
254
|
+
A plain override applies to every message passed to `sendMany()`. Invalid
|
|
255
|
+
addresses and empty recipient lists return a non-retryable failed receipt with
|
|
256
|
+
the code `smtp.envelope-invalid` before `MAIL FROM` is sent.
|
|
257
|
+
|
|
258
|
+
|
|
222
259
|
Internationalized addresses
|
|
223
260
|
---------------------------
|
|
224
261
|
|
|
225
|
-
The transport automatically negotiates [RFC 6531] SMTPUTF8 when
|
|
226
|
-
|
|
227
|
-
server must advertise both `SMTPUTF8` and `8BITMIME`;
|
|
228
|
-
`BODY=8BITMIME SMTPUTF8` on `MAIL FROM`.
|
|
262
|
+
The transport automatically negotiates [RFC 6531] SMTPUTF8 when the effective
|
|
263
|
+
SMTP envelope or a visible message-header mailbox contains a non-ASCII
|
|
264
|
+
character. A supporting server must advertise both `SMTPUTF8` and `8BITMIME`;
|
|
265
|
+
Upyo then sends `BODY=8BITMIME SMTPUTF8` on `MAIL FROM`.
|
|
229
266
|
|
|
230
267
|
If either capability is missing, delivery returns a non-retryable failed
|
|
231
268
|
receipt with the code `smtp.smtputf8-unsupported` before the mail transaction
|
|
@@ -258,9 +295,10 @@ const receipt = await transport.send(message, {
|
|
|
258
295
|
|
|
259
296
|
`envelopeId` and `return` become `ENVID` and `RET` parameters on `MAIL FROM`.
|
|
260
297
|
Each recipient's `notify` and `originalRecipient` values become `NOTIFY` and
|
|
261
|
-
`ORCPT` parameters on its `RCPT TO` command. Upyo validates
|
|
262
|
-
|
|
263
|
-
|
|
298
|
+
`ORCPT` parameters on its `RCPT TO` command. Upyo validates them against the
|
|
299
|
+
effective envelope, including any override, and `xtext`-escapes the values
|
|
300
|
+
without adding them to the message headers. `envelopeId` must not be empty,
|
|
301
|
+
and `originalRecipient` must exactly match its envelope recipient.
|
|
264
302
|
|
|
265
303
|
If the server does not advertise `DSN`, the transport returns a non-retryable
|
|
266
304
|
failed receipt with the code `smtp.dsn-unsupported` before sending `MAIL FROM`.
|
package/dist/index.cjs
CHANGED
|
@@ -65,14 +65,14 @@ const NOTIFICATION_CONDITIONS = new Set([
|
|
|
65
65
|
/**
|
|
66
66
|
* Validates and serializes the RFC 3461 envelope parameters for a message.
|
|
67
67
|
*
|
|
68
|
-
* @param
|
|
68
|
+
* @param envelope The effective SMTP envelope that will carry the parameters.
|
|
69
69
|
* @param dsn The caller-supplied delivery status notification settings.
|
|
70
70
|
* @returns Serialized parameters, or `undefined` when none were requested.
|
|
71
71
|
* @throws {SmtpDsnValidationError} If any setting violates RFC 3461 or does
|
|
72
72
|
* not correspond to the message envelope.
|
|
73
73
|
* @internal
|
|
74
74
|
*/
|
|
75
|
-
function resolveSmtpDsn(
|
|
75
|
+
function resolveSmtpDsn(envelope, dsn) {
|
|
76
76
|
if (dsn == null) return void 0;
|
|
77
77
|
if (typeof dsn !== "object" || Array.isArray(dsn)) throw new SmtpDsnValidationError("DSN options must be an object.");
|
|
78
78
|
const mailParameters = [];
|
|
@@ -87,11 +87,7 @@ function resolveSmtpDsn(message, dsn) {
|
|
|
87
87
|
assertParameterLength(parameter, 100, "ENVID");
|
|
88
88
|
mailParameters.push(parameter);
|
|
89
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
|
-
];
|
|
90
|
+
const envelopeRecipients = envelope.to;
|
|
95
91
|
const envelopeRecipientSet = new Set(envelopeRecipients);
|
|
96
92
|
const configuredRecipients = dsn.recipients;
|
|
97
93
|
if (configuredRecipients != null && (typeof configuredRecipients !== "object" || Array.isArray(configuredRecipients))) throw new SmtpDsnValidationError("DSN recipient options must be an object.");
|
|
@@ -1075,7 +1071,7 @@ var SmtpConnection = class {
|
|
|
1075
1071
|
const dsn = message.envelope.dsn;
|
|
1076
1072
|
if (dsn != null && !this.capabilities.some((capability) => /^DSN[ \t]*$/i.test(capability))) throw new SmtpDsnUnsupportedError();
|
|
1077
1073
|
const mailDsnParameters = dsn == null || dsn.mailParameters.length === 0 ? "" : ` ${dsn.mailParameters.join(" ")}`;
|
|
1078
|
-
const mailCommand = `MAIL FROM:<${message.envelope.from}>${sizeParameter}${smtpUtf8Parameters}${mailDsnParameters}`;
|
|
1074
|
+
const mailCommand = `MAIL FROM:<${message.envelope.from ?? ""}>${sizeParameter}${smtpUtf8Parameters}${mailDsnParameters}`;
|
|
1079
1075
|
const recipientCommands = message.envelope.to.map((recipient, index) => {
|
|
1080
1076
|
const parameters = dsn?.recipientParameters[index] ?? [];
|
|
1081
1077
|
const suffix = parameters.length === 0 ? "" : ` ${parameters.join(" ")}`;
|
|
@@ -1227,6 +1223,52 @@ function decodeOAuth2Challenge(message) {
|
|
|
1227
1223
|
}
|
|
1228
1224
|
}
|
|
1229
1225
|
|
|
1226
|
+
//#endregion
|
|
1227
|
+
//#region src/envelope.ts
|
|
1228
|
+
/**
|
|
1229
|
+
* Error produced when an effective SMTP envelope is invalid.
|
|
1230
|
+
*
|
|
1231
|
+
* @since 0.6.0
|
|
1232
|
+
*/
|
|
1233
|
+
var SmtpEnvelopeValidationError = class extends TypeError {
|
|
1234
|
+
/**
|
|
1235
|
+
* Creates an SMTP envelope validation error.
|
|
1236
|
+
*
|
|
1237
|
+
* @param message A description of the invalid envelope.
|
|
1238
|
+
*/
|
|
1239
|
+
constructor(message) {
|
|
1240
|
+
super(message);
|
|
1241
|
+
this.name = "SmtpEnvelopeValidationError";
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
/**
|
|
1245
|
+
* Resolves and validates the SMTP envelope for a message.
|
|
1246
|
+
*
|
|
1247
|
+
* @param message The message that supplies fields without an override.
|
|
1248
|
+
* @param override Optional sender and recipient overrides.
|
|
1249
|
+
* @returns The validated effective SMTP envelope.
|
|
1250
|
+
* @throws {SmtpEnvelopeValidationError} If the override or any effective
|
|
1251
|
+
* address is invalid, or if the effective recipient list is empty.
|
|
1252
|
+
* @internal
|
|
1253
|
+
*/
|
|
1254
|
+
function resolveSmtpEnvelope(message, override) {
|
|
1255
|
+
if (override !== void 0 && (override === null || typeof override !== "object" || Array.isArray(override))) throw new SmtpEnvelopeValidationError("SMTP envelope options must be an object.");
|
|
1256
|
+
const from = override?.from === void 0 ? message.sender.address : override.from;
|
|
1257
|
+
if (from !== null && !(0, __upyo_core.isEmailAddress)(from)) throw new SmtpEnvelopeValidationError("SMTP envelope sender must be a valid email address or null.");
|
|
1258
|
+
const recipients = override?.to === void 0 ? [
|
|
1259
|
+
...message.recipients.map((recipient) => recipient.address),
|
|
1260
|
+
...message.ccRecipients.map((recipient) => recipient.address),
|
|
1261
|
+
...message.bccRecipients.map((recipient) => recipient.address)
|
|
1262
|
+
] : override.to;
|
|
1263
|
+
if (!Array.isArray(recipients)) throw new SmtpEnvelopeValidationError("SMTP envelope recipients must be an array.");
|
|
1264
|
+
if (recipients.length === 0) throw new SmtpEnvelopeValidationError("SMTP envelope must contain at least one recipient.");
|
|
1265
|
+
for (const [index, recipient] of recipients.entries()) if (!(0, __upyo_core.isEmailAddress)(recipient)) throw new SmtpEnvelopeValidationError(`SMTP envelope recipient at index ${index} must be a valid email address.`);
|
|
1266
|
+
return {
|
|
1267
|
+
from,
|
|
1268
|
+
to: [...recipients]
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1230
1272
|
//#endregion
|
|
1231
1273
|
//#region src/dkim/canonicalize.ts
|
|
1232
1274
|
/**
|
|
@@ -1512,27 +1554,24 @@ function arrayBufferToBase64(buffer) {
|
|
|
1512
1554
|
* @param message The message to convert.
|
|
1513
1555
|
* @param dkimConfig Optional DKIM signing configuration.
|
|
1514
1556
|
* @param dsn Optional validated SMTP delivery status notification parameters.
|
|
1557
|
+
* @param resolvedEnvelope The validated effective SMTP envelope.
|
|
1515
1558
|
* @returns The converted SMTP message.
|
|
1516
1559
|
* @throws {RangeError} If a header contains a token that cannot be folded
|
|
1517
1560
|
* within the RFC 5322 hard line-length limit.
|
|
1518
1561
|
*/
|
|
1519
|
-
async function convertMessage(message, dkimConfig, dsn) {
|
|
1562
|
+
async function convertMessage(message, dkimConfig, dsn, resolvedEnvelope = resolveSmtpEnvelope(message)) {
|
|
1520
1563
|
const envelope = {
|
|
1521
|
-
|
|
1522
|
-
to: [
|
|
1523
|
-
...message.recipients.map((r) => r.address),
|
|
1524
|
-
...message.ccRecipients.map((r) => r.address),
|
|
1525
|
-
...message.bccRecipients.map((r) => r.address)
|
|
1526
|
-
],
|
|
1564
|
+
...resolvedEnvelope,
|
|
1527
1565
|
dsn
|
|
1528
1566
|
};
|
|
1529
|
-
const
|
|
1530
|
-
message.sender,
|
|
1531
|
-
...message.recipients,
|
|
1532
|
-
...message.ccRecipients,
|
|
1533
|
-
...message.
|
|
1534
|
-
|
|
1535
|
-
|
|
1567
|
+
const headerAddresses = [
|
|
1568
|
+
message.sender.address,
|
|
1569
|
+
...message.recipients.map((address) => address.address),
|
|
1570
|
+
...message.ccRecipients.map((address) => address.address),
|
|
1571
|
+
...message.replyRecipients.map((address) => address.address)
|
|
1572
|
+
];
|
|
1573
|
+
const envelopeAddresses = [...envelope.from == null ? [] : [envelope.from], ...envelope.to];
|
|
1574
|
+
const requiresSmtpUtf8 = [...headerAddresses, ...envelopeAddresses].some((address) => Array.from(address).some((character) => (character.codePointAt(0) ?? 0) > 127));
|
|
1536
1575
|
let raw = await buildRawMessage(message);
|
|
1537
1576
|
if (dkimConfig) try {
|
|
1538
1577
|
for (const sig of dkimConfig.signatures) {
|
|
@@ -1835,8 +1874,8 @@ var SmtpTransport = class {
|
|
|
1835
1874
|
* ```
|
|
1836
1875
|
*
|
|
1837
1876
|
* @param message The email message to send.
|
|
1838
|
-
* @param options Optional
|
|
1839
|
-
*
|
|
1877
|
+
* @param options Optional SMTP envelope, delivery status, and cancellation
|
|
1878
|
+
* settings.
|
|
1840
1879
|
* @returns A promise that resolves to a receipt indicating success or
|
|
1841
1880
|
* failure.
|
|
1842
1881
|
* @throws {DOMException} If the operation is aborted through
|
|
@@ -1846,10 +1885,11 @@ var SmtpTransport = class {
|
|
|
1846
1885
|
options?.signal?.throwIfAborted();
|
|
1847
1886
|
let connection;
|
|
1848
1887
|
try {
|
|
1849
|
-
const
|
|
1888
|
+
const envelope = resolveEnvelopeOption(message, options?.envelope, 0);
|
|
1889
|
+
const dsn = resolveSmtpDsn(envelope, options?.dsn);
|
|
1850
1890
|
connection = await this.getConnection(options?.signal);
|
|
1851
1891
|
options?.signal?.throwIfAborted();
|
|
1852
|
-
const smtpMessage = await convertMessage(message, this.config.dkim, dsn);
|
|
1892
|
+
const smtpMessage = await convertMessage(message, this.config.dkim, dsn, envelope);
|
|
1853
1893
|
options?.signal?.throwIfAborted();
|
|
1854
1894
|
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1855
1895
|
await this.returnConnection(connection);
|
|
@@ -1890,8 +1930,8 @@ var SmtpTransport = class {
|
|
|
1890
1930
|
* ```
|
|
1891
1931
|
*
|
|
1892
1932
|
* @param messages An iterable or async iterable of messages to send.
|
|
1893
|
-
* @param options Optional
|
|
1894
|
-
*
|
|
1933
|
+
* @param options Optional SMTP envelope, delivery status, and cancellation
|
|
1934
|
+
* settings.
|
|
1895
1935
|
* @returns An async iterable of receipts, one for each message.
|
|
1896
1936
|
* @throws {DOMException} If the operation is aborted through
|
|
1897
1937
|
* `options.signal`.
|
|
@@ -1913,50 +1953,57 @@ var SmtpTransport = class {
|
|
|
1913
1953
|
let connectionValid = true;
|
|
1914
1954
|
try {
|
|
1915
1955
|
const isAsyncIterable = Symbol.asyncIterator in messages;
|
|
1916
|
-
if (isAsyncIterable)
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
yield createSmtpFailure("Connection is no longer valid");
|
|
1920
|
-
continue;
|
|
1921
|
-
}
|
|
1922
|
-
try {
|
|
1923
|
-
const dsn = resolveSmtpDsn(message, options?.dsn);
|
|
1924
|
-
const smtpMessage = await convertMessage(message, this.config.dkim, dsn);
|
|
1956
|
+
if (isAsyncIterable) {
|
|
1957
|
+
let index = 0;
|
|
1958
|
+
for await (const message of messages) {
|
|
1925
1959
|
options?.signal?.throwIfAborted();
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1960
|
+
if (!connectionValid) {
|
|
1961
|
+
yield createSmtpFailure("Connection is no longer valid");
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
try {
|
|
1965
|
+
const envelope = resolveEnvelopeOption(message, options?.envelope, index++);
|
|
1966
|
+
const dsn = resolveSmtpDsn(envelope, options?.dsn);
|
|
1967
|
+
const smtpMessage = await convertMessage(message, this.config.dkim, dsn, envelope);
|
|
1968
|
+
options?.signal?.throwIfAborted();
|
|
1969
|
+
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1970
|
+
yield {
|
|
1971
|
+
successful: true,
|
|
1972
|
+
messageId: result.messageId,
|
|
1973
|
+
provider: "smtp",
|
|
1974
|
+
rejectedRecipients: result.rejectedRecipients
|
|
1975
|
+
};
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
options?.signal?.throwIfAborted();
|
|
1978
|
+
if (!isReusableLocalFailure(error)) connectionValid = false;
|
|
1979
|
+
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1980
|
+
}
|
|
1944
1981
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1982
|
+
} else {
|
|
1983
|
+
let index = 0;
|
|
1984
|
+
for (const message of messages) {
|
|
1948
1985
|
options?.signal?.throwIfAborted();
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1986
|
+
if (!connectionValid) {
|
|
1987
|
+
yield createSmtpFailure("Connection is no longer valid");
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
try {
|
|
1991
|
+
const envelope = resolveEnvelopeOption(message, options?.envelope, index++);
|
|
1992
|
+
const dsn = resolveSmtpDsn(envelope, options?.dsn);
|
|
1993
|
+
const smtpMessage = await convertMessage(message, this.config.dkim, dsn, envelope);
|
|
1994
|
+
options?.signal?.throwIfAborted();
|
|
1995
|
+
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1996
|
+
yield {
|
|
1997
|
+
successful: true,
|
|
1998
|
+
messageId: result.messageId,
|
|
1999
|
+
provider: "smtp",
|
|
2000
|
+
rejectedRecipients: result.rejectedRecipients
|
|
2001
|
+
};
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
options?.signal?.throwIfAborted();
|
|
2004
|
+
if (!isReusableLocalFailure(error)) connectionValid = false;
|
|
2005
|
+
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
2006
|
+
}
|
|
1960
2007
|
}
|
|
1961
2008
|
}
|
|
1962
2009
|
if (connectionValid) await this.returnConnection(connection);
|
|
@@ -2050,6 +2097,13 @@ var SmtpTransport = class {
|
|
|
2050
2097
|
}
|
|
2051
2098
|
};
|
|
2052
2099
|
function createSmtpFailure(message, error) {
|
|
2100
|
+
if (error instanceof SmtpEnvelopeValidationError) return (0, __upyo_core.createFailedReceipt)(message, {
|
|
2101
|
+
provider: "smtp",
|
|
2102
|
+
code: "smtp.envelope-invalid",
|
|
2103
|
+
category: "validation",
|
|
2104
|
+
retryable: false,
|
|
2105
|
+
attempts: 1
|
|
2106
|
+
});
|
|
2053
2107
|
if (error instanceof SmtpDsnValidationError) return (0, __upyo_core.createFailedReceipt)(message, {
|
|
2054
2108
|
provider: "smtp",
|
|
2055
2109
|
code: "smtp.dsn-invalid",
|
|
@@ -2106,7 +2160,19 @@ function createSmtpFailure(message, error) {
|
|
|
2106
2160
|
});
|
|
2107
2161
|
}
|
|
2108
2162
|
function isReusableLocalFailure(error) {
|
|
2109
|
-
return error instanceof SmtpMessageSizeError || error instanceof SmtpUtf8UnsupportedError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
|
|
2163
|
+
return error instanceof SmtpMessageSizeError || error instanceof SmtpUtf8UnsupportedError || error instanceof SmtpEnvelopeValidationError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
|
|
2164
|
+
}
|
|
2165
|
+
function resolveEnvelopeOption(message, option, index) {
|
|
2166
|
+
let override;
|
|
2167
|
+
if (typeof option === "function") try {
|
|
2168
|
+
override = option(message, index);
|
|
2169
|
+
} catch (error) {
|
|
2170
|
+
const failure = new SmtpEnvelopeValidationError(`SMTP envelope resolver failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2171
|
+
failure.cause = error;
|
|
2172
|
+
throw failure;
|
|
2173
|
+
}
|
|
2174
|
+
else override = option;
|
|
2175
|
+
return resolveSmtpEnvelope(message, override);
|
|
2110
2176
|
}
|
|
2111
2177
|
function classifySmtpReply(code, enhancedStatusCode) {
|
|
2112
2178
|
const retryable = enhancedStatusCode == null ? code >= 400 && code < 500 : enhancedStatusCode.class === 4;
|
|
@@ -2149,5 +2215,6 @@ function isSmtpResponseProviderDetails(value) {
|
|
|
2149
2215
|
exports.SmtpAuthError = SmtpAuthError;
|
|
2150
2216
|
exports.SmtpDsnUnsupportedError = SmtpDsnUnsupportedError;
|
|
2151
2217
|
exports.SmtpDsnValidationError = SmtpDsnValidationError;
|
|
2218
|
+
exports.SmtpEnvelopeValidationError = SmtpEnvelopeValidationError;
|
|
2152
2219
|
exports.SmtpTransport = SmtpTransport;
|
|
2153
2220
|
exports.isSmtpResponseProviderDetails = isSmtpResponseProviderDetails;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Message, Receipt, Transport, TransportOptions } from "@upyo/core";
|
|
1
|
+
import { EmailAddress, Message, Receipt, Transport, TransportOptions } from "@upyo/core";
|
|
2
2
|
|
|
3
3
|
//#region src/dkim/types.d.ts
|
|
4
4
|
|
|
@@ -417,6 +417,54 @@ interface SmtpTlsOptions {
|
|
|
417
417
|
* used internally by the SMTP transport implementation.
|
|
418
418
|
*/
|
|
419
419
|
//#endregion
|
|
420
|
+
//#region src/envelope.d.ts
|
|
421
|
+
/**
|
|
422
|
+
* Overrides for the SMTP envelope of one message.
|
|
423
|
+
*
|
|
424
|
+
* Omitted fields are derived from the corresponding message fields. A `null`
|
|
425
|
+
* sender produces the null reverse-path used for delivery notifications.
|
|
426
|
+
*
|
|
427
|
+
* @since 0.6.0
|
|
428
|
+
*/
|
|
429
|
+
interface SmtpEnvelopeOptions {
|
|
430
|
+
/**
|
|
431
|
+
* The address for `MAIL FROM`, or `null` for `MAIL FROM:<>`.
|
|
432
|
+
*
|
|
433
|
+
* When omitted, the message sender is used.
|
|
434
|
+
*/
|
|
435
|
+
readonly from?: EmailAddress | null;
|
|
436
|
+
/**
|
|
437
|
+
* The addresses for `RCPT TO`.
|
|
438
|
+
*
|
|
439
|
+
* When omitted, the message's To, Cc, and Bcc addresses are used.
|
|
440
|
+
*/
|
|
441
|
+
readonly to?: readonly EmailAddress[];
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Resolves an SMTP envelope override for one message in a batch.
|
|
445
|
+
*
|
|
446
|
+
* @param message The message whose envelope is being resolved.
|
|
447
|
+
* @param index The zero-based position of the message in the send operation.
|
|
448
|
+
* @returns The envelope override, or `undefined` to derive the envelope from
|
|
449
|
+
* the message.
|
|
450
|
+
* @since 0.6.0
|
|
451
|
+
*/
|
|
452
|
+
type SmtpEnvelopeResolver = (message: Message, index: number) => SmtpEnvelopeOptions | undefined;
|
|
453
|
+
/**
|
|
454
|
+
* Error produced when an effective SMTP envelope is invalid.
|
|
455
|
+
*
|
|
456
|
+
* @since 0.6.0
|
|
457
|
+
*/
|
|
458
|
+
declare class SmtpEnvelopeValidationError extends TypeError {
|
|
459
|
+
/**
|
|
460
|
+
* Creates an SMTP envelope validation error.
|
|
461
|
+
*
|
|
462
|
+
* @param message A description of the invalid envelope.
|
|
463
|
+
*/
|
|
464
|
+
constructor(message: string);
|
|
465
|
+
}
|
|
466
|
+
/** @internal */
|
|
467
|
+
//#endregion
|
|
420
468
|
//#region src/delivery-status.d.ts
|
|
421
469
|
/**
|
|
422
470
|
* A condition under which an SMTP server should issue a delivery status
|
|
@@ -481,6 +529,13 @@ interface SmtpDsnOptions {
|
|
|
481
529
|
* @since 0.6.0
|
|
482
530
|
*/
|
|
483
531
|
interface SmtpTransportOptions extends TransportOptions {
|
|
532
|
+
/**
|
|
533
|
+
* SMTP envelope overrides for this send operation.
|
|
534
|
+
*
|
|
535
|
+
* A resolver can return a different envelope for each message passed to
|
|
536
|
+
* {@link SmtpTransport.sendMany}.
|
|
537
|
+
*/
|
|
538
|
+
readonly envelope?: SmtpEnvelopeOptions | SmtpEnvelopeResolver;
|
|
484
539
|
/** Delivery status notification settings for this SMTP transaction. */
|
|
485
540
|
readonly dsn?: SmtpDsnOptions;
|
|
486
541
|
}
|
|
@@ -694,8 +749,8 @@ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
|
|
|
694
749
|
* ```
|
|
695
750
|
*
|
|
696
751
|
* @param message The email message to send.
|
|
697
|
-
* @param options Optional
|
|
698
|
-
*
|
|
752
|
+
* @param options Optional SMTP envelope, delivery status, and cancellation
|
|
753
|
+
* settings.
|
|
699
754
|
* @returns A promise that resolves to a receipt indicating success or
|
|
700
755
|
* failure.
|
|
701
756
|
* @throws {DOMException} If the operation is aborted through
|
|
@@ -726,8 +781,8 @@ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
|
|
|
726
781
|
* ```
|
|
727
782
|
*
|
|
728
783
|
* @param messages An iterable or async iterable of messages to send.
|
|
729
|
-
* @param options Optional
|
|
730
|
-
*
|
|
784
|
+
* @param options Optional SMTP envelope, delivery status, and cancellation
|
|
785
|
+
* settings.
|
|
731
786
|
* @returns An async iterable of receipts, one for each message.
|
|
732
787
|
* @throws {DOMException} If the operation is aborted through
|
|
733
788
|
* `options.signal`.
|
|
@@ -797,4 +852,4 @@ declare class SmtpAuthError extends Error {
|
|
|
797
852
|
*/
|
|
798
853
|
|
|
799
854
|
//#endregion
|
|
800
|
-
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpDsnNotification, SmtpDsnOptions, SmtpDsnRecipientOptions, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpEnhancedStatusCode, SmtpMessageSizeProviderDetails, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpProviderDetails, SmtpReceipt, SmtpRejectedRecipient, SmtpResponseProviderDetails, SmtpTlsOptions, SmtpTransport, SmtpTransportOptions, SmtpUserPassAuth, SmtpUtf8ProviderDetails, isSmtpResponseProviderDetails };
|
|
855
|
+
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpDsnNotification, SmtpDsnOptions, SmtpDsnRecipientOptions, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpEnhancedStatusCode, SmtpEnvelopeOptions, SmtpEnvelopeResolver, SmtpEnvelopeValidationError, SmtpMessageSizeProviderDetails, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpProviderDetails, SmtpReceipt, SmtpRejectedRecipient, SmtpResponseProviderDetails, SmtpTlsOptions, SmtpTransport, SmtpTransportOptions, SmtpUserPassAuth, SmtpUtf8ProviderDetails, isSmtpResponseProviderDetails };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Message, Receipt, Transport, TransportOptions } from "@upyo/core";
|
|
1
|
+
import { EmailAddress, Message, Receipt, Transport, TransportOptions } from "@upyo/core";
|
|
2
2
|
|
|
3
3
|
//#region src/dkim/types.d.ts
|
|
4
4
|
|
|
@@ -417,6 +417,54 @@ interface SmtpTlsOptions {
|
|
|
417
417
|
* used internally by the SMTP transport implementation.
|
|
418
418
|
*/
|
|
419
419
|
//#endregion
|
|
420
|
+
//#region src/envelope.d.ts
|
|
421
|
+
/**
|
|
422
|
+
* Overrides for the SMTP envelope of one message.
|
|
423
|
+
*
|
|
424
|
+
* Omitted fields are derived from the corresponding message fields. A `null`
|
|
425
|
+
* sender produces the null reverse-path used for delivery notifications.
|
|
426
|
+
*
|
|
427
|
+
* @since 0.6.0
|
|
428
|
+
*/
|
|
429
|
+
interface SmtpEnvelopeOptions {
|
|
430
|
+
/**
|
|
431
|
+
* The address for `MAIL FROM`, or `null` for `MAIL FROM:<>`.
|
|
432
|
+
*
|
|
433
|
+
* When omitted, the message sender is used.
|
|
434
|
+
*/
|
|
435
|
+
readonly from?: EmailAddress | null;
|
|
436
|
+
/**
|
|
437
|
+
* The addresses for `RCPT TO`.
|
|
438
|
+
*
|
|
439
|
+
* When omitted, the message's To, Cc, and Bcc addresses are used.
|
|
440
|
+
*/
|
|
441
|
+
readonly to?: readonly EmailAddress[];
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Resolves an SMTP envelope override for one message in a batch.
|
|
445
|
+
*
|
|
446
|
+
* @param message The message whose envelope is being resolved.
|
|
447
|
+
* @param index The zero-based position of the message in the send operation.
|
|
448
|
+
* @returns The envelope override, or `undefined` to derive the envelope from
|
|
449
|
+
* the message.
|
|
450
|
+
* @since 0.6.0
|
|
451
|
+
*/
|
|
452
|
+
type SmtpEnvelopeResolver = (message: Message, index: number) => SmtpEnvelopeOptions | undefined;
|
|
453
|
+
/**
|
|
454
|
+
* Error produced when an effective SMTP envelope is invalid.
|
|
455
|
+
*
|
|
456
|
+
* @since 0.6.0
|
|
457
|
+
*/
|
|
458
|
+
declare class SmtpEnvelopeValidationError extends TypeError {
|
|
459
|
+
/**
|
|
460
|
+
* Creates an SMTP envelope validation error.
|
|
461
|
+
*
|
|
462
|
+
* @param message A description of the invalid envelope.
|
|
463
|
+
*/
|
|
464
|
+
constructor(message: string);
|
|
465
|
+
}
|
|
466
|
+
/** @internal */
|
|
467
|
+
//#endregion
|
|
420
468
|
//#region src/delivery-status.d.ts
|
|
421
469
|
/**
|
|
422
470
|
* A condition under which an SMTP server should issue a delivery status
|
|
@@ -481,6 +529,13 @@ interface SmtpDsnOptions {
|
|
|
481
529
|
* @since 0.6.0
|
|
482
530
|
*/
|
|
483
531
|
interface SmtpTransportOptions extends TransportOptions {
|
|
532
|
+
/**
|
|
533
|
+
* SMTP envelope overrides for this send operation.
|
|
534
|
+
*
|
|
535
|
+
* A resolver can return a different envelope for each message passed to
|
|
536
|
+
* {@link SmtpTransport.sendMany}.
|
|
537
|
+
*/
|
|
538
|
+
readonly envelope?: SmtpEnvelopeOptions | SmtpEnvelopeResolver;
|
|
484
539
|
/** Delivery status notification settings for this SMTP transaction. */
|
|
485
540
|
readonly dsn?: SmtpDsnOptions;
|
|
486
541
|
}
|
|
@@ -694,8 +749,8 @@ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
|
|
|
694
749
|
* ```
|
|
695
750
|
*
|
|
696
751
|
* @param message The email message to send.
|
|
697
|
-
* @param options Optional
|
|
698
|
-
*
|
|
752
|
+
* @param options Optional SMTP envelope, delivery status, and cancellation
|
|
753
|
+
* settings.
|
|
699
754
|
* @returns A promise that resolves to a receipt indicating success or
|
|
700
755
|
* failure.
|
|
701
756
|
* @throws {DOMException} If the operation is aborted through
|
|
@@ -726,8 +781,8 @@ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
|
|
|
726
781
|
* ```
|
|
727
782
|
*
|
|
728
783
|
* @param messages An iterable or async iterable of messages to send.
|
|
729
|
-
* @param options Optional
|
|
730
|
-
*
|
|
784
|
+
* @param options Optional SMTP envelope, delivery status, and cancellation
|
|
785
|
+
* settings.
|
|
731
786
|
* @returns An async iterable of receipts, one for each message.
|
|
732
787
|
* @throws {DOMException} If the operation is aborted through
|
|
733
788
|
* `options.signal`.
|
|
@@ -797,4 +852,4 @@ declare class SmtpAuthError extends Error {
|
|
|
797
852
|
*/
|
|
798
853
|
|
|
799
854
|
//#endregion
|
|
800
|
-
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpDsnNotification, SmtpDsnOptions, SmtpDsnRecipientOptions, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpEnhancedStatusCode, SmtpMessageSizeProviderDetails, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpProviderDetails, SmtpReceipt, SmtpRejectedRecipient, SmtpResponseProviderDetails, SmtpTlsOptions, SmtpTransport, SmtpTransportOptions, SmtpUserPassAuth, SmtpUtf8ProviderDetails, isSmtpResponseProviderDetails };
|
|
855
|
+
export { DkimAlgorithm, DkimCanonicalization, DkimConfig, DkimSignature, DkimSigningFailureAction, OAuth2TokenProvider, SmtpAuth, SmtpAuthError, SmtpConfig, SmtpDsnNotification, SmtpDsnOptions, SmtpDsnRecipientOptions, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpEnhancedStatusCode, SmtpEnvelopeOptions, SmtpEnvelopeResolver, SmtpEnvelopeValidationError, SmtpMessageSizeProviderDetails, SmtpOAuth2Auth, SmtpOAuth2RefreshAuth, SmtpOAuth2TokenAuth, SmtpProviderDetails, SmtpReceipt, SmtpRejectedRecipient, SmtpResponseProviderDetails, SmtpTlsOptions, SmtpTransport, SmtpTransportOptions, SmtpUserPassAuth, SmtpUtf8ProviderDetails, isSmtpResponseProviderDetails };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createFailedReceipt } from "@upyo/core";
|
|
1
|
+
import { createFailedReceipt, isEmailAddress } from "@upyo/core";
|
|
2
2
|
import { Buffer } from "node:buffer";
|
|
3
3
|
import { Socket } from "node:net";
|
|
4
4
|
import { TLSSocket, connect } from "node:tls";
|
|
@@ -42,14 +42,14 @@ const NOTIFICATION_CONDITIONS = new Set([
|
|
|
42
42
|
/**
|
|
43
43
|
* Validates and serializes the RFC 3461 envelope parameters for a message.
|
|
44
44
|
*
|
|
45
|
-
* @param
|
|
45
|
+
* @param envelope The effective SMTP envelope that will carry the parameters.
|
|
46
46
|
* @param dsn The caller-supplied delivery status notification settings.
|
|
47
47
|
* @returns Serialized parameters, or `undefined` when none were requested.
|
|
48
48
|
* @throws {SmtpDsnValidationError} If any setting violates RFC 3461 or does
|
|
49
49
|
* not correspond to the message envelope.
|
|
50
50
|
* @internal
|
|
51
51
|
*/
|
|
52
|
-
function resolveSmtpDsn(
|
|
52
|
+
function resolveSmtpDsn(envelope, dsn) {
|
|
53
53
|
if (dsn == null) return void 0;
|
|
54
54
|
if (typeof dsn !== "object" || Array.isArray(dsn)) throw new SmtpDsnValidationError("DSN options must be an object.");
|
|
55
55
|
const mailParameters = [];
|
|
@@ -64,11 +64,7 @@ function resolveSmtpDsn(message, dsn) {
|
|
|
64
64
|
assertParameterLength(parameter, 100, "ENVID");
|
|
65
65
|
mailParameters.push(parameter);
|
|
66
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
|
-
];
|
|
67
|
+
const envelopeRecipients = envelope.to;
|
|
72
68
|
const envelopeRecipientSet = new Set(envelopeRecipients);
|
|
73
69
|
const configuredRecipients = dsn.recipients;
|
|
74
70
|
if (configuredRecipients != null && (typeof configuredRecipients !== "object" || Array.isArray(configuredRecipients))) throw new SmtpDsnValidationError("DSN recipient options must be an object.");
|
|
@@ -1052,7 +1048,7 @@ var SmtpConnection = class {
|
|
|
1052
1048
|
const dsn = message.envelope.dsn;
|
|
1053
1049
|
if (dsn != null && !this.capabilities.some((capability) => /^DSN[ \t]*$/i.test(capability))) throw new SmtpDsnUnsupportedError();
|
|
1054
1050
|
const mailDsnParameters = dsn == null || dsn.mailParameters.length === 0 ? "" : ` ${dsn.mailParameters.join(" ")}`;
|
|
1055
|
-
const mailCommand = `MAIL FROM:<${message.envelope.from}>${sizeParameter}${smtpUtf8Parameters}${mailDsnParameters}`;
|
|
1051
|
+
const mailCommand = `MAIL FROM:<${message.envelope.from ?? ""}>${sizeParameter}${smtpUtf8Parameters}${mailDsnParameters}`;
|
|
1056
1052
|
const recipientCommands = message.envelope.to.map((recipient, index) => {
|
|
1057
1053
|
const parameters = dsn?.recipientParameters[index] ?? [];
|
|
1058
1054
|
const suffix = parameters.length === 0 ? "" : ` ${parameters.join(" ")}`;
|
|
@@ -1204,6 +1200,52 @@ function decodeOAuth2Challenge(message) {
|
|
|
1204
1200
|
}
|
|
1205
1201
|
}
|
|
1206
1202
|
|
|
1203
|
+
//#endregion
|
|
1204
|
+
//#region src/envelope.ts
|
|
1205
|
+
/**
|
|
1206
|
+
* Error produced when an effective SMTP envelope is invalid.
|
|
1207
|
+
*
|
|
1208
|
+
* @since 0.6.0
|
|
1209
|
+
*/
|
|
1210
|
+
var SmtpEnvelopeValidationError = class extends TypeError {
|
|
1211
|
+
/**
|
|
1212
|
+
* Creates an SMTP envelope validation error.
|
|
1213
|
+
*
|
|
1214
|
+
* @param message A description of the invalid envelope.
|
|
1215
|
+
*/
|
|
1216
|
+
constructor(message) {
|
|
1217
|
+
super(message);
|
|
1218
|
+
this.name = "SmtpEnvelopeValidationError";
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
/**
|
|
1222
|
+
* Resolves and validates the SMTP envelope for a message.
|
|
1223
|
+
*
|
|
1224
|
+
* @param message The message that supplies fields without an override.
|
|
1225
|
+
* @param override Optional sender and recipient overrides.
|
|
1226
|
+
* @returns The validated effective SMTP envelope.
|
|
1227
|
+
* @throws {SmtpEnvelopeValidationError} If the override or any effective
|
|
1228
|
+
* address is invalid, or if the effective recipient list is empty.
|
|
1229
|
+
* @internal
|
|
1230
|
+
*/
|
|
1231
|
+
function resolveSmtpEnvelope(message, override) {
|
|
1232
|
+
if (override !== void 0 && (override === null || typeof override !== "object" || Array.isArray(override))) throw new SmtpEnvelopeValidationError("SMTP envelope options must be an object.");
|
|
1233
|
+
const from = override?.from === void 0 ? message.sender.address : override.from;
|
|
1234
|
+
if (from !== null && !isEmailAddress(from)) throw new SmtpEnvelopeValidationError("SMTP envelope sender must be a valid email address or null.");
|
|
1235
|
+
const recipients = override?.to === void 0 ? [
|
|
1236
|
+
...message.recipients.map((recipient) => recipient.address),
|
|
1237
|
+
...message.ccRecipients.map((recipient) => recipient.address),
|
|
1238
|
+
...message.bccRecipients.map((recipient) => recipient.address)
|
|
1239
|
+
] : override.to;
|
|
1240
|
+
if (!Array.isArray(recipients)) throw new SmtpEnvelopeValidationError("SMTP envelope recipients must be an array.");
|
|
1241
|
+
if (recipients.length === 0) throw new SmtpEnvelopeValidationError("SMTP envelope must contain at least one recipient.");
|
|
1242
|
+
for (const [index, recipient] of recipients.entries()) if (!isEmailAddress(recipient)) throw new SmtpEnvelopeValidationError(`SMTP envelope recipient at index ${index} must be a valid email address.`);
|
|
1243
|
+
return {
|
|
1244
|
+
from,
|
|
1245
|
+
to: [...recipients]
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1207
1249
|
//#endregion
|
|
1208
1250
|
//#region src/dkim/canonicalize.ts
|
|
1209
1251
|
/**
|
|
@@ -1489,27 +1531,24 @@ function arrayBufferToBase64(buffer) {
|
|
|
1489
1531
|
* @param message The message to convert.
|
|
1490
1532
|
* @param dkimConfig Optional DKIM signing configuration.
|
|
1491
1533
|
* @param dsn Optional validated SMTP delivery status notification parameters.
|
|
1534
|
+
* @param resolvedEnvelope The validated effective SMTP envelope.
|
|
1492
1535
|
* @returns The converted SMTP message.
|
|
1493
1536
|
* @throws {RangeError} If a header contains a token that cannot be folded
|
|
1494
1537
|
* within the RFC 5322 hard line-length limit.
|
|
1495
1538
|
*/
|
|
1496
|
-
async function convertMessage(message, dkimConfig, dsn) {
|
|
1539
|
+
async function convertMessage(message, dkimConfig, dsn, resolvedEnvelope = resolveSmtpEnvelope(message)) {
|
|
1497
1540
|
const envelope = {
|
|
1498
|
-
|
|
1499
|
-
to: [
|
|
1500
|
-
...message.recipients.map((r) => r.address),
|
|
1501
|
-
...message.ccRecipients.map((r) => r.address),
|
|
1502
|
-
...message.bccRecipients.map((r) => r.address)
|
|
1503
|
-
],
|
|
1541
|
+
...resolvedEnvelope,
|
|
1504
1542
|
dsn
|
|
1505
1543
|
};
|
|
1506
|
-
const
|
|
1507
|
-
message.sender,
|
|
1508
|
-
...message.recipients,
|
|
1509
|
-
...message.ccRecipients,
|
|
1510
|
-
...message.
|
|
1511
|
-
|
|
1512
|
-
|
|
1544
|
+
const headerAddresses = [
|
|
1545
|
+
message.sender.address,
|
|
1546
|
+
...message.recipients.map((address) => address.address),
|
|
1547
|
+
...message.ccRecipients.map((address) => address.address),
|
|
1548
|
+
...message.replyRecipients.map((address) => address.address)
|
|
1549
|
+
];
|
|
1550
|
+
const envelopeAddresses = [...envelope.from == null ? [] : [envelope.from], ...envelope.to];
|
|
1551
|
+
const requiresSmtpUtf8 = [...headerAddresses, ...envelopeAddresses].some((address) => Array.from(address).some((character) => (character.codePointAt(0) ?? 0) > 127));
|
|
1513
1552
|
let raw = await buildRawMessage(message);
|
|
1514
1553
|
if (dkimConfig) try {
|
|
1515
1554
|
for (const sig of dkimConfig.signatures) {
|
|
@@ -1812,8 +1851,8 @@ var SmtpTransport = class {
|
|
|
1812
1851
|
* ```
|
|
1813
1852
|
*
|
|
1814
1853
|
* @param message The email message to send.
|
|
1815
|
-
* @param options Optional
|
|
1816
|
-
*
|
|
1854
|
+
* @param options Optional SMTP envelope, delivery status, and cancellation
|
|
1855
|
+
* settings.
|
|
1817
1856
|
* @returns A promise that resolves to a receipt indicating success or
|
|
1818
1857
|
* failure.
|
|
1819
1858
|
* @throws {DOMException} If the operation is aborted through
|
|
@@ -1823,10 +1862,11 @@ var SmtpTransport = class {
|
|
|
1823
1862
|
options?.signal?.throwIfAborted();
|
|
1824
1863
|
let connection;
|
|
1825
1864
|
try {
|
|
1826
|
-
const
|
|
1865
|
+
const envelope = resolveEnvelopeOption(message, options?.envelope, 0);
|
|
1866
|
+
const dsn = resolveSmtpDsn(envelope, options?.dsn);
|
|
1827
1867
|
connection = await this.getConnection(options?.signal);
|
|
1828
1868
|
options?.signal?.throwIfAborted();
|
|
1829
|
-
const smtpMessage = await convertMessage(message, this.config.dkim, dsn);
|
|
1869
|
+
const smtpMessage = await convertMessage(message, this.config.dkim, dsn, envelope);
|
|
1830
1870
|
options?.signal?.throwIfAborted();
|
|
1831
1871
|
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1832
1872
|
await this.returnConnection(connection);
|
|
@@ -1867,8 +1907,8 @@ var SmtpTransport = class {
|
|
|
1867
1907
|
* ```
|
|
1868
1908
|
*
|
|
1869
1909
|
* @param messages An iterable or async iterable of messages to send.
|
|
1870
|
-
* @param options Optional
|
|
1871
|
-
*
|
|
1910
|
+
* @param options Optional SMTP envelope, delivery status, and cancellation
|
|
1911
|
+
* settings.
|
|
1872
1912
|
* @returns An async iterable of receipts, one for each message.
|
|
1873
1913
|
* @throws {DOMException} If the operation is aborted through
|
|
1874
1914
|
* `options.signal`.
|
|
@@ -1890,50 +1930,57 @@ var SmtpTransport = class {
|
|
|
1890
1930
|
let connectionValid = true;
|
|
1891
1931
|
try {
|
|
1892
1932
|
const isAsyncIterable = Symbol.asyncIterator in messages;
|
|
1893
|
-
if (isAsyncIterable)
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
yield createSmtpFailure("Connection is no longer valid");
|
|
1897
|
-
continue;
|
|
1898
|
-
}
|
|
1899
|
-
try {
|
|
1900
|
-
const dsn = resolveSmtpDsn(message, options?.dsn);
|
|
1901
|
-
const smtpMessage = await convertMessage(message, this.config.dkim, dsn);
|
|
1933
|
+
if (isAsyncIterable) {
|
|
1934
|
+
let index = 0;
|
|
1935
|
+
for await (const message of messages) {
|
|
1902
1936
|
options?.signal?.throwIfAborted();
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1937
|
+
if (!connectionValid) {
|
|
1938
|
+
yield createSmtpFailure("Connection is no longer valid");
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
try {
|
|
1942
|
+
const envelope = resolveEnvelopeOption(message, options?.envelope, index++);
|
|
1943
|
+
const dsn = resolveSmtpDsn(envelope, options?.dsn);
|
|
1944
|
+
const smtpMessage = await convertMessage(message, this.config.dkim, dsn, envelope);
|
|
1945
|
+
options?.signal?.throwIfAborted();
|
|
1946
|
+
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1947
|
+
yield {
|
|
1948
|
+
successful: true,
|
|
1949
|
+
messageId: result.messageId,
|
|
1950
|
+
provider: "smtp",
|
|
1951
|
+
rejectedRecipients: result.rejectedRecipients
|
|
1952
|
+
};
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
options?.signal?.throwIfAborted();
|
|
1955
|
+
if (!isReusableLocalFailure(error)) connectionValid = false;
|
|
1956
|
+
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1957
|
+
}
|
|
1921
1958
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1959
|
+
} else {
|
|
1960
|
+
let index = 0;
|
|
1961
|
+
for (const message of messages) {
|
|
1925
1962
|
options?.signal?.throwIfAborted();
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1963
|
+
if (!connectionValid) {
|
|
1964
|
+
yield createSmtpFailure("Connection is no longer valid");
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
try {
|
|
1968
|
+
const envelope = resolveEnvelopeOption(message, options?.envelope, index++);
|
|
1969
|
+
const dsn = resolveSmtpDsn(envelope, options?.dsn);
|
|
1970
|
+
const smtpMessage = await convertMessage(message, this.config.dkim, dsn, envelope);
|
|
1971
|
+
options?.signal?.throwIfAborted();
|
|
1972
|
+
const result = await connection.sendMessage(smtpMessage, options?.signal);
|
|
1973
|
+
yield {
|
|
1974
|
+
successful: true,
|
|
1975
|
+
messageId: result.messageId,
|
|
1976
|
+
provider: "smtp",
|
|
1977
|
+
rejectedRecipients: result.rejectedRecipients
|
|
1978
|
+
};
|
|
1979
|
+
} catch (error) {
|
|
1980
|
+
options?.signal?.throwIfAborted();
|
|
1981
|
+
if (!isReusableLocalFailure(error)) connectionValid = false;
|
|
1982
|
+
yield createSmtpFailure(error instanceof Error ? error.message : String(error), error);
|
|
1983
|
+
}
|
|
1937
1984
|
}
|
|
1938
1985
|
}
|
|
1939
1986
|
if (connectionValid) await this.returnConnection(connection);
|
|
@@ -2027,6 +2074,13 @@ var SmtpTransport = class {
|
|
|
2027
2074
|
}
|
|
2028
2075
|
};
|
|
2029
2076
|
function createSmtpFailure(message, error) {
|
|
2077
|
+
if (error instanceof SmtpEnvelopeValidationError) return createFailedReceipt(message, {
|
|
2078
|
+
provider: "smtp",
|
|
2079
|
+
code: "smtp.envelope-invalid",
|
|
2080
|
+
category: "validation",
|
|
2081
|
+
retryable: false,
|
|
2082
|
+
attempts: 1
|
|
2083
|
+
});
|
|
2030
2084
|
if (error instanceof SmtpDsnValidationError) return createFailedReceipt(message, {
|
|
2031
2085
|
provider: "smtp",
|
|
2032
2086
|
code: "smtp.dsn-invalid",
|
|
@@ -2083,7 +2137,19 @@ function createSmtpFailure(message, error) {
|
|
|
2083
2137
|
});
|
|
2084
2138
|
}
|
|
2085
2139
|
function isReusableLocalFailure(error) {
|
|
2086
|
-
return error instanceof SmtpMessageSizeError || error instanceof SmtpUtf8UnsupportedError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
|
|
2140
|
+
return error instanceof SmtpMessageSizeError || error instanceof SmtpUtf8UnsupportedError || error instanceof SmtpEnvelopeValidationError || error instanceof SmtpDsnValidationError || error instanceof SmtpDsnUnsupportedError;
|
|
2141
|
+
}
|
|
2142
|
+
function resolveEnvelopeOption(message, option, index) {
|
|
2143
|
+
let override;
|
|
2144
|
+
if (typeof option === "function") try {
|
|
2145
|
+
override = option(message, index);
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
const failure = new SmtpEnvelopeValidationError(`SMTP envelope resolver failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2148
|
+
failure.cause = error;
|
|
2149
|
+
throw failure;
|
|
2150
|
+
}
|
|
2151
|
+
else override = option;
|
|
2152
|
+
return resolveSmtpEnvelope(message, override);
|
|
2087
2153
|
}
|
|
2088
2154
|
function classifySmtpReply(code, enhancedStatusCode) {
|
|
2089
2155
|
const retryable = enhancedStatusCode == null ? code >= 400 && code < 500 : enhancedStatusCode.class === 4;
|
|
@@ -2123,4 +2189,4 @@ function isSmtpResponseProviderDetails(value) {
|
|
|
2123
2189
|
}
|
|
2124
2190
|
|
|
2125
2191
|
//#endregion
|
|
2126
|
-
export { SmtpAuthError, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpTransport, isSmtpResponseProviderDetails };
|
|
2192
|
+
export { SmtpAuthError, SmtpDsnUnsupportedError, SmtpDsnValidationError, SmtpEnvelopeValidationError, SmtpTransport, isSmtpResponseProviderDetails };
|
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.306",
|
|
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.306+6bd78f36"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"tsdown": "^0.12.7",
|