@upyo/smtp 0.5.0-dev.128 → 0.5.0-dev.154

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
@@ -21,6 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  }) : target, mod));
22
22
 
23
23
  //#endregion
24
+ const __upyo_core = __toESM(require("@upyo/core"));
24
25
  const node_net = __toESM(require("node:net"));
25
26
  const node_tls = __toESM(require("node:tls"));
26
27
  const node_buffer = __toESM(require("node:buffer"));
@@ -154,7 +155,7 @@ function escapeSaslName(name) {
154
155
  */
155
156
  function selectOAuth2Mechanism(capabilities) {
156
157
  const authLine = capabilities.find((cap) => cap.toUpperCase().startsWith("AUTH"));
157
- const mechanisms = authLine == null ? [] : authLine.toUpperCase().split(/\s+/).slice(1);
158
+ const mechanisms = authLine == null ? [] : authLine.toUpperCase().replace(/=/g, " ").split(/\s+/).slice(1);
158
159
  if (mechanisms.includes("XOAUTH2")) return "xoauth2";
159
160
  if (mechanisms.includes("OAUTHBEARER")) return "oauthbearer";
160
161
  return "xoauth2";
@@ -218,8 +219,9 @@ function truncateErrorBody(text) {
218
219
  function abortable(promise, signal) {
219
220
  if (signal == null) return promise;
220
221
  return new Promise((resolve, reject) => {
221
- const onAbort = () => reject(signal.reason);
222
- if (signal.aborted) reject(signal.reason);
222
+ const abortReason = () => signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
223
+ const onAbort = () => reject(abortReason());
224
+ if (signal.aborted) reject(abortReason());
223
225
  else signal.addEventListener("abort", onAbort, { once: true });
224
226
  promise.then((value) => {
225
227
  signal.removeEventListener("abort", onAbort);
@@ -276,11 +278,13 @@ var OAuth2TokenManager = class {
276
278
  const auth = this.auth;
277
279
  if ("accessToken" in auth) return typeof auth.accessToken === "function" ? await auth.accessToken(signal) : auth.accessToken;
278
280
  const cached = this.cached;
279
- if (cached != null && cached.expiresAt - REFRESH_SAFETY_MARGIN_MS > Date.now()) return cached.accessToken;
281
+ if (cached != null && cached.expiresAt > Date.now()) return cached.accessToken;
280
282
  this.pending ??= this.refresh(auth).then((result) => {
283
+ const lifetimeMs = result.expiresIn * 1e3;
284
+ const safetyMargin = Math.min(REFRESH_SAFETY_MARGIN_MS, lifetimeMs / 2);
281
285
  this.cached = {
282
286
  accessToken: result.accessToken,
283
- expiresAt: Date.now() + result.expiresIn * 1e3
287
+ expiresAt: Date.now() + lifetimeMs - safetyMargin
284
288
  };
285
289
  return result;
286
290
  }).finally(() => {
@@ -314,8 +318,9 @@ var OAuth2TokenManager = class {
314
318
  }, TOKEN_REQUEST_TIMEOUT_MS);
315
319
  let response;
316
320
  let text;
321
+ const fetchFn = this.fetchFn;
317
322
  try {
318
- response = await this.fetchFn(auth.tokenEndpoint, {
323
+ response = await fetchFn(auth.tokenEndpoint, {
319
324
  method: "POST",
320
325
  headers: {
321
326
  "content-type": "application/x-www-form-urlencoded",
@@ -340,12 +345,12 @@ var OAuth2TokenManager = class {
340
345
  }
341
346
  if (typeof json !== "object" || json == null || typeof json.access_token !== "string") throw new SmtpAuthError(`OAuth 2.0 token endpoint response did not include an access_token: ${safeText}`);
342
347
  const record = json;
343
- let expiresIn = DEFAULT_EXPIRES_IN;
344
- if (typeof record.expires_in === "number") expiresIn = record.expires_in;
345
- else if (typeof record.expires_in === "string") {
346
- const parsed = Number.parseInt(record.expires_in, 10);
347
- if (Number.isFinite(parsed)) expiresIn = parsed;
348
- }
348
+ const rawExpiresIn = record.expires_in;
349
+ let parsedExpiresIn;
350
+ if (typeof rawExpiresIn === "number") parsedExpiresIn = rawExpiresIn;
351
+ else if (typeof rawExpiresIn === "string" && rawExpiresIn.trim() !== "") parsedExpiresIn = Number(rawExpiresIn);
352
+ else parsedExpiresIn = NaN;
353
+ const expiresIn = Number.isFinite(parsedExpiresIn) && parsedExpiresIn >= 0 ? parsedExpiresIn : DEFAULT_EXPIRES_IN;
349
354
  return {
350
355
  accessToken: record.access_token,
351
356
  expiresIn
@@ -363,6 +368,12 @@ const MAX_COMMAND_LINE_LENGTH = 512;
363
368
  /** The length of the CRLF terminator appended to every command. */
364
369
  const CRLF_LENGTH = 2;
365
370
  /**
371
+ * How long, in milliseconds, to wait for the graceful `QUIT` to flush during
372
+ * teardown before giving up, so an unresponsive server cannot block shutdown
373
+ * for the full socket timeout.
374
+ */
375
+ const QUIT_TIMEOUT_MS = 5e3;
376
+ /**
366
377
  * Whether a host refers to the local loopback interface, for which cleartext
367
378
  * OAuth 2.0 authentication is permitted (e.g. local testing and development).
368
379
  *
@@ -679,9 +690,22 @@ var SmtpConnection = class {
679
690
  async quit() {
680
691
  const socket = this.socket;
681
692
  if (!socket) return;
682
- if (socket.writable) try {
683
- await this.sendCommand("QUIT");
684
- } catch {}
693
+ if (socket.writable) await new Promise((resolve) => {
694
+ const done = () => {
695
+ clearTimeout(timer);
696
+ socket.off("error", done);
697
+ socket.off("close", done);
698
+ resolve();
699
+ };
700
+ const timer = setTimeout(done, QUIT_TIMEOUT_MS);
701
+ socket.once("error", done);
702
+ socket.once("close", done);
703
+ try {
704
+ socket.write("QUIT\r\n", done);
705
+ } catch {
706
+ done();
707
+ }
708
+ });
685
709
  try {
686
710
  socket.destroy();
687
711
  } catch {}
@@ -699,12 +723,17 @@ var SmtpConnection = class {
699
723
  * Decodes the Base64 JSON error challenge a server sends after a failed OAuth
700
724
  * SASL exchange, falling back to the raw message when it is not valid Base64.
701
725
  *
726
+ * The bytes are decoded as UTF-8 (via `TextDecoder`) so non-ASCII challenge
727
+ * messages are not corrupted, unlike decoding `atob`'s Latin-1 output directly.
728
+ *
702
729
  * @param message The challenge text from the server's 334 response.
703
730
  * @returns A human-readable description of the failure.
704
731
  */
705
732
  function decodeOAuth2Challenge(message) {
706
733
  try {
707
- return atob(message.trim());
734
+ const binary = atob(message.trim());
735
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
736
+ return new TextDecoder().decode(bytes);
708
737
  } catch {
709
738
  return message;
710
739
  }
@@ -1196,6 +1225,7 @@ function encodeBase64(data) {
1196
1225
  * ```
1197
1226
  */
1198
1227
  var SmtpTransport = class {
1228
+ id = "smtp";
1199
1229
  /**
1200
1230
  * The SMTP configuration used by this transport.
1201
1231
  */
@@ -1249,6 +1279,8 @@ var SmtpTransport = class {
1249
1279
  * cancellation.
1250
1280
  * @returns A promise that resolves to a receipt indicating success or
1251
1281
  * failure.
1282
+ * @throws {DOMException} If the operation is aborted through
1283
+ * `options.signal`.
1252
1284
  */
1253
1285
  async send(message, options) {
1254
1286
  options?.signal?.throwIfAborted();
@@ -1262,15 +1294,13 @@ var SmtpTransport = class {
1262
1294
  await this.returnConnection(connection);
1263
1295
  return {
1264
1296
  successful: true,
1265
- messageId
1297
+ messageId,
1298
+ provider: "smtp"
1266
1299
  };
1267
1300
  } catch (error) {
1268
1301
  if (connection != null) await this.discardConnection(connection);
1269
1302
  options?.signal?.throwIfAborted();
1270
- return {
1271
- successful: false,
1272
- errorMessages: [error instanceof Error ? error.message : String(error)]
1273
- };
1303
+ return createSmtpFailure(error instanceof Error ? error.message : String(error));
1274
1304
  }
1275
1305
  }
1276
1306
  /**
@@ -1300,6 +1330,8 @@ var SmtpTransport = class {
1300
1330
  * @param options Optional transport options including `AbortSignal` for
1301
1331
  * cancellation.
1302
1332
  * @returns An async iterable of receipts, one for each message.
1333
+ * @throws {DOMException} If the operation is aborted through
1334
+ * `options.signal`.
1303
1335
  */
1304
1336
  async *sendMany(messages, options) {
1305
1337
  options?.signal?.throwIfAborted();
@@ -1311,10 +1343,7 @@ var SmtpTransport = class {
1311
1343
  const errorMessage = error instanceof Error ? error.message : String(error);
1312
1344
  for await (const _ of messages) {
1313
1345
  options?.signal?.throwIfAborted();
1314
- yield {
1315
- successful: false,
1316
- errorMessages: [errorMessage]
1317
- };
1346
+ yield createSmtpFailure(errorMessage);
1318
1347
  }
1319
1348
  return;
1320
1349
  }
@@ -1324,10 +1353,7 @@ var SmtpTransport = class {
1324
1353
  if (isAsyncIterable) for await (const message of messages) {
1325
1354
  options?.signal?.throwIfAborted();
1326
1355
  if (!connectionValid) {
1327
- yield {
1328
- successful: false,
1329
- errorMessages: ["Connection is no longer valid"]
1330
- };
1356
+ yield createSmtpFailure("Connection is no longer valid");
1331
1357
  continue;
1332
1358
  }
1333
1359
  try {
@@ -1336,24 +1362,19 @@ var SmtpTransport = class {
1336
1362
  const messageId = await connection.sendMessage(smtpMessage, options?.signal);
1337
1363
  yield {
1338
1364
  successful: true,
1339
- messageId
1365
+ messageId,
1366
+ provider: "smtp"
1340
1367
  };
1341
1368
  } catch (error) {
1342
1369
  options?.signal?.throwIfAborted();
1343
1370
  connectionValid = false;
1344
- yield {
1345
- successful: false,
1346
- errorMessages: [error instanceof Error ? error.message : String(error)]
1347
- };
1371
+ yield createSmtpFailure(error instanceof Error ? error.message : String(error));
1348
1372
  }
1349
1373
  }
1350
1374
  else for (const message of messages) {
1351
1375
  options?.signal?.throwIfAborted();
1352
1376
  if (!connectionValid) {
1353
- yield {
1354
- successful: false,
1355
- errorMessages: ["Connection is no longer valid"]
1356
- };
1377
+ yield createSmtpFailure("Connection is no longer valid");
1357
1378
  continue;
1358
1379
  }
1359
1380
  try {
@@ -1362,15 +1383,13 @@ var SmtpTransport = class {
1362
1383
  const messageId = await connection.sendMessage(smtpMessage, options?.signal);
1363
1384
  yield {
1364
1385
  successful: true,
1365
- messageId
1386
+ messageId,
1387
+ provider: "smtp"
1366
1388
  };
1367
1389
  } catch (error) {
1368
1390
  options?.signal?.throwIfAborted();
1369
1391
  connectionValid = false;
1370
- yield {
1371
- successful: false,
1372
- errorMessages: [error instanceof Error ? error.message : String(error)]
1373
- };
1392
+ yield createSmtpFailure(error instanceof Error ? error.message : String(error));
1374
1393
  }
1375
1394
  }
1376
1395
  if (connectionValid) await this.returnConnection(connection);
@@ -1463,6 +1482,12 @@ var SmtpTransport = class {
1463
1482
  await this.closeAllConnections();
1464
1483
  }
1465
1484
  };
1485
+ function createSmtpFailure(message) {
1486
+ return (0, __upyo_core.createFailedReceipt)(message, {
1487
+ provider: "smtp",
1488
+ attempts: 1
1489
+ });
1490
+ }
1466
1491
 
1467
1492
  //#endregion
1468
1493
  exports.SmtpAuthError = SmtpAuthError;
package/dist/index.d.cts CHANGED
@@ -442,7 +442,8 @@ interface SmtpTlsOptions {
442
442
  * }
443
443
  * ```
444
444
  */
445
- declare class SmtpTransport implements Transport, AsyncDisposable {
445
+ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
446
+ readonly id = "smtp";
446
447
  /**
447
448
  * The SMTP configuration used by this transport.
448
449
  */
@@ -491,8 +492,10 @@ declare class SmtpTransport implements Transport, AsyncDisposable {
491
492
  * cancellation.
492
493
  * @returns A promise that resolves to a receipt indicating success or
493
494
  * failure.
495
+ * @throws {DOMException} If the operation is aborted through
496
+ * `options.signal`.
494
497
  */
495
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
498
+ send(message: Message, options?: TransportOptions): Promise<Receipt<"smtp">>;
496
499
  /**
497
500
  * Sends multiple email messages efficiently using a single SMTP connection.
498
501
  *
@@ -520,8 +523,10 @@ declare class SmtpTransport implements Transport, AsyncDisposable {
520
523
  * @param options Optional transport options including `AbortSignal` for
521
524
  * cancellation.
522
525
  * @returns An async iterable of receipts, one for each message.
526
+ * @throws {DOMException} If the operation is aborted through
527
+ * `options.signal`.
523
528
  */
524
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
529
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"smtp">>;
525
530
  private getConnection;
526
531
  private connectAndSetup;
527
532
  private returnConnection;
package/dist/index.d.ts CHANGED
@@ -442,7 +442,8 @@ interface SmtpTlsOptions {
442
442
  * }
443
443
  * ```
444
444
  */
445
- declare class SmtpTransport implements Transport, AsyncDisposable {
445
+ declare class SmtpTransport implements Transport<"smtp">, AsyncDisposable {
446
+ readonly id = "smtp";
446
447
  /**
447
448
  * The SMTP configuration used by this transport.
448
449
  */
@@ -491,8 +492,10 @@ declare class SmtpTransport implements Transport, AsyncDisposable {
491
492
  * cancellation.
492
493
  * @returns A promise that resolves to a receipt indicating success or
493
494
  * failure.
495
+ * @throws {DOMException} If the operation is aborted through
496
+ * `options.signal`.
494
497
  */
495
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
498
+ send(message: Message, options?: TransportOptions): Promise<Receipt<"smtp">>;
496
499
  /**
497
500
  * Sends multiple email messages efficiently using a single SMTP connection.
498
501
  *
@@ -520,8 +523,10 @@ declare class SmtpTransport implements Transport, AsyncDisposable {
520
523
  * @param options Optional transport options including `AbortSignal` for
521
524
  * cancellation.
522
525
  * @returns An async iterable of receipts, one for each message.
526
+ * @throws {DOMException} If the operation is aborted through
527
+ * `options.signal`.
523
528
  */
524
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
529
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"smtp">>;
525
530
  private getConnection;
526
531
  private connectAndSetup;
527
532
  private returnConnection;
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createFailedReceipt } from "@upyo/core";
1
2
  import { Socket } from "node:net";
2
3
  import { TLSSocket, connect } from "node:tls";
3
4
  import { Buffer } from "node:buffer";
@@ -131,7 +132,7 @@ function escapeSaslName(name) {
131
132
  */
132
133
  function selectOAuth2Mechanism(capabilities) {
133
134
  const authLine = capabilities.find((cap) => cap.toUpperCase().startsWith("AUTH"));
134
- const mechanisms = authLine == null ? [] : authLine.toUpperCase().split(/\s+/).slice(1);
135
+ const mechanisms = authLine == null ? [] : authLine.toUpperCase().replace(/=/g, " ").split(/\s+/).slice(1);
135
136
  if (mechanisms.includes("XOAUTH2")) return "xoauth2";
136
137
  if (mechanisms.includes("OAUTHBEARER")) return "oauthbearer";
137
138
  return "xoauth2";
@@ -195,8 +196,9 @@ function truncateErrorBody(text) {
195
196
  function abortable(promise, signal) {
196
197
  if (signal == null) return promise;
197
198
  return new Promise((resolve, reject) => {
198
- const onAbort = () => reject(signal.reason);
199
- if (signal.aborted) reject(signal.reason);
199
+ const abortReason = () => signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
200
+ const onAbort = () => reject(abortReason());
201
+ if (signal.aborted) reject(abortReason());
200
202
  else signal.addEventListener("abort", onAbort, { once: true });
201
203
  promise.then((value) => {
202
204
  signal.removeEventListener("abort", onAbort);
@@ -253,11 +255,13 @@ var OAuth2TokenManager = class {
253
255
  const auth = this.auth;
254
256
  if ("accessToken" in auth) return typeof auth.accessToken === "function" ? await auth.accessToken(signal) : auth.accessToken;
255
257
  const cached = this.cached;
256
- if (cached != null && cached.expiresAt - REFRESH_SAFETY_MARGIN_MS > Date.now()) return cached.accessToken;
258
+ if (cached != null && cached.expiresAt > Date.now()) return cached.accessToken;
257
259
  this.pending ??= this.refresh(auth).then((result) => {
260
+ const lifetimeMs = result.expiresIn * 1e3;
261
+ const safetyMargin = Math.min(REFRESH_SAFETY_MARGIN_MS, lifetimeMs / 2);
258
262
  this.cached = {
259
263
  accessToken: result.accessToken,
260
- expiresAt: Date.now() + result.expiresIn * 1e3
264
+ expiresAt: Date.now() + lifetimeMs - safetyMargin
261
265
  };
262
266
  return result;
263
267
  }).finally(() => {
@@ -291,8 +295,9 @@ var OAuth2TokenManager = class {
291
295
  }, TOKEN_REQUEST_TIMEOUT_MS);
292
296
  let response;
293
297
  let text;
298
+ const fetchFn = this.fetchFn;
294
299
  try {
295
- response = await this.fetchFn(auth.tokenEndpoint, {
300
+ response = await fetchFn(auth.tokenEndpoint, {
296
301
  method: "POST",
297
302
  headers: {
298
303
  "content-type": "application/x-www-form-urlencoded",
@@ -317,12 +322,12 @@ var OAuth2TokenManager = class {
317
322
  }
318
323
  if (typeof json !== "object" || json == null || typeof json.access_token !== "string") throw new SmtpAuthError(`OAuth 2.0 token endpoint response did not include an access_token: ${safeText}`);
319
324
  const record = json;
320
- let expiresIn = DEFAULT_EXPIRES_IN;
321
- if (typeof record.expires_in === "number") expiresIn = record.expires_in;
322
- else if (typeof record.expires_in === "string") {
323
- const parsed = Number.parseInt(record.expires_in, 10);
324
- if (Number.isFinite(parsed)) expiresIn = parsed;
325
- }
325
+ const rawExpiresIn = record.expires_in;
326
+ let parsedExpiresIn;
327
+ if (typeof rawExpiresIn === "number") parsedExpiresIn = rawExpiresIn;
328
+ else if (typeof rawExpiresIn === "string" && rawExpiresIn.trim() !== "") parsedExpiresIn = Number(rawExpiresIn);
329
+ else parsedExpiresIn = NaN;
330
+ const expiresIn = Number.isFinite(parsedExpiresIn) && parsedExpiresIn >= 0 ? parsedExpiresIn : DEFAULT_EXPIRES_IN;
326
331
  return {
327
332
  accessToken: record.access_token,
328
333
  expiresIn
@@ -340,6 +345,12 @@ const MAX_COMMAND_LINE_LENGTH = 512;
340
345
  /** The length of the CRLF terminator appended to every command. */
341
346
  const CRLF_LENGTH = 2;
342
347
  /**
348
+ * How long, in milliseconds, to wait for the graceful `QUIT` to flush during
349
+ * teardown before giving up, so an unresponsive server cannot block shutdown
350
+ * for the full socket timeout.
351
+ */
352
+ const QUIT_TIMEOUT_MS = 5e3;
353
+ /**
343
354
  * Whether a host refers to the local loopback interface, for which cleartext
344
355
  * OAuth 2.0 authentication is permitted (e.g. local testing and development).
345
356
  *
@@ -656,9 +667,22 @@ var SmtpConnection = class {
656
667
  async quit() {
657
668
  const socket = this.socket;
658
669
  if (!socket) return;
659
- if (socket.writable) try {
660
- await this.sendCommand("QUIT");
661
- } catch {}
670
+ if (socket.writable) await new Promise((resolve) => {
671
+ const done = () => {
672
+ clearTimeout(timer);
673
+ socket.off("error", done);
674
+ socket.off("close", done);
675
+ resolve();
676
+ };
677
+ const timer = setTimeout(done, QUIT_TIMEOUT_MS);
678
+ socket.once("error", done);
679
+ socket.once("close", done);
680
+ try {
681
+ socket.write("QUIT\r\n", done);
682
+ } catch {
683
+ done();
684
+ }
685
+ });
662
686
  try {
663
687
  socket.destroy();
664
688
  } catch {}
@@ -676,12 +700,17 @@ var SmtpConnection = class {
676
700
  * Decodes the Base64 JSON error challenge a server sends after a failed OAuth
677
701
  * SASL exchange, falling back to the raw message when it is not valid Base64.
678
702
  *
703
+ * The bytes are decoded as UTF-8 (via `TextDecoder`) so non-ASCII challenge
704
+ * messages are not corrupted, unlike decoding `atob`'s Latin-1 output directly.
705
+ *
679
706
  * @param message The challenge text from the server's 334 response.
680
707
  * @returns A human-readable description of the failure.
681
708
  */
682
709
  function decodeOAuth2Challenge(message) {
683
710
  try {
684
- return atob(message.trim());
711
+ const binary = atob(message.trim());
712
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
713
+ return new TextDecoder().decode(bytes);
685
714
  } catch {
686
715
  return message;
687
716
  }
@@ -1173,6 +1202,7 @@ function encodeBase64(data) {
1173
1202
  * ```
1174
1203
  */
1175
1204
  var SmtpTransport = class {
1205
+ id = "smtp";
1176
1206
  /**
1177
1207
  * The SMTP configuration used by this transport.
1178
1208
  */
@@ -1226,6 +1256,8 @@ var SmtpTransport = class {
1226
1256
  * cancellation.
1227
1257
  * @returns A promise that resolves to a receipt indicating success or
1228
1258
  * failure.
1259
+ * @throws {DOMException} If the operation is aborted through
1260
+ * `options.signal`.
1229
1261
  */
1230
1262
  async send(message, options) {
1231
1263
  options?.signal?.throwIfAborted();
@@ -1239,15 +1271,13 @@ var SmtpTransport = class {
1239
1271
  await this.returnConnection(connection);
1240
1272
  return {
1241
1273
  successful: true,
1242
- messageId
1274
+ messageId,
1275
+ provider: "smtp"
1243
1276
  };
1244
1277
  } catch (error) {
1245
1278
  if (connection != null) await this.discardConnection(connection);
1246
1279
  options?.signal?.throwIfAborted();
1247
- return {
1248
- successful: false,
1249
- errorMessages: [error instanceof Error ? error.message : String(error)]
1250
- };
1280
+ return createSmtpFailure(error instanceof Error ? error.message : String(error));
1251
1281
  }
1252
1282
  }
1253
1283
  /**
@@ -1277,6 +1307,8 @@ var SmtpTransport = class {
1277
1307
  * @param options Optional transport options including `AbortSignal` for
1278
1308
  * cancellation.
1279
1309
  * @returns An async iterable of receipts, one for each message.
1310
+ * @throws {DOMException} If the operation is aborted through
1311
+ * `options.signal`.
1280
1312
  */
1281
1313
  async *sendMany(messages, options) {
1282
1314
  options?.signal?.throwIfAborted();
@@ -1288,10 +1320,7 @@ var SmtpTransport = class {
1288
1320
  const errorMessage = error instanceof Error ? error.message : String(error);
1289
1321
  for await (const _ of messages) {
1290
1322
  options?.signal?.throwIfAborted();
1291
- yield {
1292
- successful: false,
1293
- errorMessages: [errorMessage]
1294
- };
1323
+ yield createSmtpFailure(errorMessage);
1295
1324
  }
1296
1325
  return;
1297
1326
  }
@@ -1301,10 +1330,7 @@ var SmtpTransport = class {
1301
1330
  if (isAsyncIterable) for await (const message of messages) {
1302
1331
  options?.signal?.throwIfAborted();
1303
1332
  if (!connectionValid) {
1304
- yield {
1305
- successful: false,
1306
- errorMessages: ["Connection is no longer valid"]
1307
- };
1333
+ yield createSmtpFailure("Connection is no longer valid");
1308
1334
  continue;
1309
1335
  }
1310
1336
  try {
@@ -1313,24 +1339,19 @@ var SmtpTransport = class {
1313
1339
  const messageId = await connection.sendMessage(smtpMessage, options?.signal);
1314
1340
  yield {
1315
1341
  successful: true,
1316
- messageId
1342
+ messageId,
1343
+ provider: "smtp"
1317
1344
  };
1318
1345
  } catch (error) {
1319
1346
  options?.signal?.throwIfAborted();
1320
1347
  connectionValid = false;
1321
- yield {
1322
- successful: false,
1323
- errorMessages: [error instanceof Error ? error.message : String(error)]
1324
- };
1348
+ yield createSmtpFailure(error instanceof Error ? error.message : String(error));
1325
1349
  }
1326
1350
  }
1327
1351
  else for (const message of messages) {
1328
1352
  options?.signal?.throwIfAborted();
1329
1353
  if (!connectionValid) {
1330
- yield {
1331
- successful: false,
1332
- errorMessages: ["Connection is no longer valid"]
1333
- };
1354
+ yield createSmtpFailure("Connection is no longer valid");
1334
1355
  continue;
1335
1356
  }
1336
1357
  try {
@@ -1339,15 +1360,13 @@ var SmtpTransport = class {
1339
1360
  const messageId = await connection.sendMessage(smtpMessage, options?.signal);
1340
1361
  yield {
1341
1362
  successful: true,
1342
- messageId
1363
+ messageId,
1364
+ provider: "smtp"
1343
1365
  };
1344
1366
  } catch (error) {
1345
1367
  options?.signal?.throwIfAborted();
1346
1368
  connectionValid = false;
1347
- yield {
1348
- successful: false,
1349
- errorMessages: [error instanceof Error ? error.message : String(error)]
1350
- };
1369
+ yield createSmtpFailure(error instanceof Error ? error.message : String(error));
1351
1370
  }
1352
1371
  }
1353
1372
  if (connectionValid) await this.returnConnection(connection);
@@ -1440,6 +1459,12 @@ var SmtpTransport = class {
1440
1459
  await this.closeAllConnections();
1441
1460
  }
1442
1461
  };
1462
+ function createSmtpFailure(message) {
1463
+ return createFailedReceipt(message, {
1464
+ provider: "smtp",
1465
+ attempts: 1
1466
+ });
1467
+ }
1443
1468
 
1444
1469
  //#endregion
1445
1470
  export { SmtpAuthError, SmtpTransport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/smtp",
3
- "version": "0.5.0-dev.128",
3
+ "version": "0.5.0-dev.154",
4
4
  "description": "SMTP transport for Upyo email library",
5
5
  "keywords": [
6
6
  "email",
@@ -53,22 +53,13 @@
53
53
  },
54
54
  "sideEffects": false,
55
55
  "peerDependencies": {
56
- "@upyo/core": "0.5.0-dev.128+6e126027"
56
+ "@upyo/core": "0.5.0-dev.154+2f72d353"
57
57
  },
58
58
  "devDependencies": {
59
- "@dotenvx/dotenvx": "^1.47.3",
60
59
  "tsdown": "^0.12.7",
61
60
  "typescript": "5.8.3"
62
61
  },
63
62
  "scripts": {
64
- "build": "tsdown",
65
- "prepublish": "tsdown",
66
- "test": "tsdown && dotenvx run --ignore=MISSING_ENV_FILE -- node --experimental-transform-types --test",
67
- "test:bun": "tsdown && bun test --timeout=30000 --env-file=.env",
68
- "test:deno": "deno test --allow-env --allow-net --env-file=.env",
69
- "mailpit:start": "docker run -d --name upyo-mailpit -p 1025:1025 -p 8025:8025 axllent/mailpit:latest",
70
- "mailpit:stop": "docker stop upyo-mailpit && docker rm upyo-mailpit",
71
- "mailpit:logs": "docker logs upyo-mailpit",
72
- "dev:mailpit": "docker run --rm -p 1025:1025 -p 8025:8025 axllent/mailpit:latest"
63
+ "prepublish": "mise run --no-deps :build"
73
64
  }
74
65
  }