@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 +68 -43
- package/dist/index.d.cts +8 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +68 -43
- package/package.json +3 -12
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
|
|
222
|
-
|
|
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
|
|
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() +
|
|
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
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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)
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
199
|
-
|
|
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
|
|
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() +
|
|
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
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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)
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
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
|
}
|