@upyo/mailtrap 0.6.0-dev.0

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 ADDED
@@ -0,0 +1,656 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+ const __upyo_core = __toESM(require("@upyo/core"));
25
+
26
+ //#region src/config.ts
27
+ const DEFAULT_SEND_BASE_URL = "https://send.api.mailtrap.io";
28
+ const DEFAULT_SANDBOX_BASE_URL = "https://sandbox.api.mailtrap.io";
29
+ const DEFAULT_CATEGORY = "transactional";
30
+ const DEFAULT_USER_AGENT = "@upyo/mailtrap";
31
+ /**
32
+ * Creates a resolved Mailtrap configuration by applying default values.
33
+ *
34
+ * @param config The Mailtrap configuration with optional fields.
35
+ * @returns A resolved configuration with all defaults applied.
36
+ * @throws {RangeError} If `sandbox` is `true` and `inboxId` is missing.
37
+ * @since 0.6.0
38
+ */
39
+ function createMailtrapConfig(config) {
40
+ const sandbox = config.sandbox ?? false;
41
+ if (sandbox && !isValidInboxId(config.inboxId)) throw new RangeError("`inboxId` is required when Mailtrap sandbox mode is enabled.");
42
+ return {
43
+ apiToken: config.apiToken,
44
+ sandbox,
45
+ inboxId: config.inboxId,
46
+ sendBaseUrl: normalizeBaseUrl(config.sendBaseUrl ?? DEFAULT_SEND_BASE_URL),
47
+ sandboxBaseUrl: normalizeBaseUrl(config.sandboxBaseUrl ?? DEFAULT_SANDBOX_BASE_URL),
48
+ defaultCategory: config.defaultCategory ?? DEFAULT_CATEGORY,
49
+ metadata: config.metadata == null ? void 0 : { ...config.metadata },
50
+ userAgent: config.userAgent ?? DEFAULT_USER_AGENT,
51
+ timeout: config.timeout ?? 3e4,
52
+ retries: config.retries ?? 3,
53
+ validateSsl: config.validateSsl ?? true,
54
+ headers: config.headers == null ? {} : { ...config.headers }
55
+ };
56
+ }
57
+ function isValidInboxId(inboxId) {
58
+ if (inboxId === void 0 || inboxId === null) return false;
59
+ if (typeof inboxId === "number") return Number.isFinite(inboxId);
60
+ return inboxId.trim().length > 0;
61
+ }
62
+ function normalizeBaseUrl(baseUrl) {
63
+ return baseUrl.replace(/\/+$/, "");
64
+ }
65
+
66
+ //#endregion
67
+ //#region src/http-client.ts
68
+ const maxErrorMessageLength = 500;
69
+ const sandboxRateLimitWindowMilliseconds = 1e4;
70
+ const sandboxRateLimitMessage = "too many emails per second";
71
+ /**
72
+ * Mailtrap API error class for API-specific failures.
73
+ *
74
+ * @since 0.6.0
75
+ */
76
+ var MailtrapApiError = class extends Error {
77
+ statusCode;
78
+ retryAfterMilliseconds;
79
+ attempts;
80
+ /**
81
+ * Creates a Mailtrap API error.
82
+ *
83
+ * @param message Error message.
84
+ * @param statusCode HTTP status code.
85
+ * @param retryAfterMilliseconds Retry delay from the response or Mailtrap's
86
+ * known sandbox rate-limit window.
87
+ * @param attempts Number of attempts made before this error.
88
+ */
89
+ constructor(message, statusCode, retryAfterMilliseconds, attempts) {
90
+ super(message);
91
+ this.name = "MailtrapApiError";
92
+ this.statusCode = statusCode;
93
+ this.retryAfterMilliseconds = retryAfterMilliseconds;
94
+ this.attempts = attempts;
95
+ }
96
+ };
97
+ /**
98
+ * Mailtrap request timeout error.
99
+ *
100
+ * @since 0.6.0
101
+ */
102
+ var MailtrapTimeoutError = class extends Error {
103
+ /**
104
+ * Request timeout in milliseconds.
105
+ *
106
+ * @since 0.6.0
107
+ */
108
+ timeout;
109
+ /**
110
+ * Number of attempts made before this error was produced.
111
+ *
112
+ * @since 0.6.0
113
+ */
114
+ attempts;
115
+ /**
116
+ * Creates a Mailtrap request timeout error.
117
+ *
118
+ * @param timeout Request timeout in milliseconds.
119
+ * @param attempts Number of attempts made before this error.
120
+ */
121
+ constructor(timeout, attempts) {
122
+ super(`Mailtrap API request timed out after ${timeout} ms.`);
123
+ this.name = "MailtrapTimeoutError";
124
+ this.timeout = timeout;
125
+ this.attempts = attempts;
126
+ }
127
+ };
128
+ /**
129
+ * HTTP client wrapper for Mailtrap API requests.
130
+ *
131
+ * @since 0.6.0
132
+ */
133
+ var MailtrapHttpClient = class {
134
+ config;
135
+ /**
136
+ * Creates a new Mailtrap HTTP client.
137
+ *
138
+ * @param config Resolved Mailtrap configuration.
139
+ */
140
+ constructor(config) {
141
+ this.config = config;
142
+ }
143
+ /**
144
+ * Sends a single message via Mailtrap API.
145
+ *
146
+ * @param messageData The JSON data to send to Mailtrap.
147
+ * @param signal Optional AbortSignal for cancellation.
148
+ * @returns Promise that resolves to the Mailtrap response.
149
+ */
150
+ sendMessage(messageData, signal) {
151
+ const url = this.resolveUrl("send");
152
+ return this.makeRequest(url, messageData, signal);
153
+ }
154
+ /**
155
+ * Sends multiple messages via Mailtrap batch API.
156
+ *
157
+ * @param requests The messages to send to Mailtrap.
158
+ * @param signal Optional AbortSignal for cancellation.
159
+ * @returns Promise that resolves to the Mailtrap batch response.
160
+ */
161
+ sendBatch(requests, signal) {
162
+ const url = this.resolveUrl("batch");
163
+ return this.makeRequest(url, { requests }, signal);
164
+ }
165
+ resolveUrl(kind) {
166
+ const baseUrl = this.config.sandbox ? this.config.sandboxBaseUrl : this.config.sendBaseUrl;
167
+ const suffix = this.config.sandbox && this.config.inboxId != null ? `/${this.config.inboxId}` : "";
168
+ return `${baseUrl}/api/${kind}${suffix}`;
169
+ }
170
+ async makeRequest(url, body, signal) {
171
+ let lastError = null;
172
+ for (let attempt = 0; attempt <= this.config.retries; attempt++) {
173
+ signal?.throwIfAborted();
174
+ let responseText;
175
+ try {
176
+ const { response, text } = await this.fetchWithAuth(url, body, signal);
177
+ responseText = text;
178
+ if (!response.ok) {
179
+ const message = parseErrorMessage(responseText, response.status);
180
+ throw new MailtrapApiError(message, response.status, parseRetryAfter(response.headers.get("Retry-After")) ?? inferSandboxRetryAfter(message, response.status, this.config.sandbox), attempt + 1);
181
+ }
182
+ } catch (error) {
183
+ lastError = error instanceof Error ? error : new Error(String(error));
184
+ if (error instanceof MailtrapApiError && !isRetryable(error)) throw error;
185
+ if (error instanceof Error && error.name === "AbortError" && signal?.aborted) throw error;
186
+ if (attempt === this.config.retries) throw withAttempts(lastError, attempt + 1);
187
+ await sleep(calculateRetryDelay(attempt, lastError), signal);
188
+ continue;
189
+ }
190
+ try {
191
+ return JSON.parse(responseText);
192
+ } catch (error) {
193
+ throw new SyntaxError(`Invalid JSON response from Mailtrap API: ${error instanceof Error ? error.message : String(error)}.`);
194
+ }
195
+ }
196
+ throw lastError ?? /* @__PURE__ */ new Error("Request failed after all retry attempts.");
197
+ }
198
+ async fetchWithAuth(url, body, signal) {
199
+ const headers = new Headers({
200
+ "Content-Type": "application/json",
201
+ "Api-Token": this.config.apiToken,
202
+ "User-Agent": this.config.userAgent
203
+ });
204
+ for (const [key, value] of Object.entries(this.config.headers)) headers.set(key, value);
205
+ const timeoutController = new AbortController();
206
+ const timeoutId = this.config.timeout > 0 ? setTimeout(() => timeoutController.abort(), this.config.timeout) : void 0;
207
+ const requestSignal = (0, __upyo_core.combineSignals)(timeoutController.signal, signal);
208
+ try {
209
+ const response = await globalThis.fetch(url, {
210
+ method: "POST",
211
+ headers,
212
+ body: JSON.stringify(body),
213
+ signal: requestSignal.signal
214
+ });
215
+ const text = await response.text();
216
+ return {
217
+ response,
218
+ text
219
+ };
220
+ } catch (error) {
221
+ if (error instanceof Error && error.name === "AbortError" && timeoutController.signal.aborted && !signal?.aborted) throw new MailtrapTimeoutError(this.config.timeout);
222
+ throw error;
223
+ } finally {
224
+ requestSignal.cleanup();
225
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
226
+ }
227
+ }
228
+ };
229
+ function isRetryable(error) {
230
+ return error.statusCode === 408 || error.statusCode === 429 || error.statusCode >= 500;
231
+ }
232
+ function calculateRetryDelay(attempt, error) {
233
+ const baseDelay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
234
+ const backoffDelay = Math.round(baseDelay / 2 + Math.random() * (baseDelay / 2));
235
+ if (error instanceof MailtrapApiError) return Math.max(backoffDelay, error.retryAfterMilliseconds ?? 0);
236
+ return backoffDelay;
237
+ }
238
+ function withAttempts(error, attempts) {
239
+ if (error instanceof MailtrapTimeoutError) return new MailtrapTimeoutError(error.timeout, attempts);
240
+ if (error instanceof MailtrapApiError) return new MailtrapApiError(error.message, error.statusCode, error.retryAfterMilliseconds, attempts);
241
+ return Object.assign(error, { attempts });
242
+ }
243
+ function parseRetryAfter(header) {
244
+ if (header == null || header === "") return void 0;
245
+ const asSeconds = Number(header);
246
+ if (Number.isFinite(asSeconds) && asSeconds >= 0) return Math.round(asSeconds * 1e3);
247
+ const asDate = Date.parse(header);
248
+ if (Number.isNaN(asDate)) return void 0;
249
+ return Math.max(0, asDate - Date.now());
250
+ }
251
+ function inferSandboxRetryAfter(message, statusCode, sandbox) {
252
+ if (sandbox && statusCode === 429 && message.toLowerCase().includes(sandboxRateLimitMessage)) return sandboxRateLimitWindowMilliseconds;
253
+ return void 0;
254
+ }
255
+ function parseErrorMessage(text, statusCode) {
256
+ try {
257
+ const errorBody = JSON.parse(text);
258
+ if (typeof errorBody.message === "string" && errorBody.message !== "") return truncateErrorMessage(errorBody.message);
259
+ if (Array.isArray(errorBody.errors) && errorBody.errors.length > 0) return truncateErrorMessage(errorBody.errors.join("; "));
260
+ } catch {}
261
+ return truncateErrorMessage(text) || `HTTP ${statusCode}`;
262
+ }
263
+ function truncateErrorMessage(message) {
264
+ return message.length > maxErrorMessageLength ? `${message.slice(0, maxErrorMessageLength)}...` : message;
265
+ }
266
+ function abortReason$1(signal) {
267
+ return signal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
268
+ }
269
+ function sleep(ms, signal) {
270
+ return new Promise((resolve, reject) => {
271
+ if (signal?.aborted) {
272
+ reject(abortReason$1(signal));
273
+ return;
274
+ }
275
+ const timeoutState = {};
276
+ const onAbort = () => {
277
+ if (timeoutState.id !== void 0) clearTimeout(timeoutState.id);
278
+ signal?.removeEventListener("abort", onAbort);
279
+ reject(abortReason$1(signal));
280
+ };
281
+ signal?.addEventListener("abort", onAbort, { once: true });
282
+ if (signal?.aborted) {
283
+ onAbort();
284
+ return;
285
+ }
286
+ timeoutState.id = setTimeout(() => {
287
+ signal?.removeEventListener("abort", onAbort);
288
+ resolve();
289
+ }, ms);
290
+ });
291
+ }
292
+
293
+ //#endregion
294
+ //#region src/message-converter.ts
295
+ const STANDARD_HEADERS = new Set([
296
+ "from",
297
+ "to",
298
+ "cc",
299
+ "bcc",
300
+ "reply-to",
301
+ "subject",
302
+ "date",
303
+ "message-id",
304
+ "content-type",
305
+ "content-transfer-encoding",
306
+ "mime-version",
307
+ "x-priority"
308
+ ]);
309
+ /**
310
+ * Converts an Upyo message to Mailtrap API JSON format.
311
+ *
312
+ * @param message The Upyo message to convert.
313
+ * @param config The resolved Mailtrap configuration.
314
+ * @param signal Optional abort signal for cancellation.
315
+ * @returns JSON object ready for Mailtrap API submission.
316
+ * @throws {RangeError} If the message has no text or HTML content.
317
+ * @throws {Error} If the caller aborts the operation.
318
+ * @since 0.6.0
319
+ */
320
+ async function convertMessage(message, config, signal) {
321
+ signal?.throwIfAborted();
322
+ const emailData = {
323
+ from: formatAddress(message.sender),
324
+ to: message.recipients.map(formatAddress),
325
+ subject: message.subject,
326
+ category: resolveCategory(message.tags, config.defaultCategory)
327
+ };
328
+ if (message.ccRecipients.length > 0) emailData.cc = message.ccRecipients.map(formatAddress);
329
+ if (message.bccRecipients.length > 0) emailData.bcc = message.bccRecipients.map(formatAddress);
330
+ if (message.replyRecipients.length > 0) emailData.reply_to = formatAddress(message.replyRecipients[0]);
331
+ if ("html" in message.content) {
332
+ emailData.html = message.content.html;
333
+ if (message.content.text) emailData.text = message.content.text;
334
+ } else emailData.text = message.content.text;
335
+ if (!emailData.text && !emailData.html) throw new RangeError("Mailtrap requires at least one of text or HTML content.");
336
+ const customVariables = buildCustomVariables(message.tags, config.metadata);
337
+ if (Object.keys(customVariables).length > 0) emailData.custom_variables = customVariables;
338
+ const headers = {};
339
+ if (message.priority !== "normal") {
340
+ const priorityMap = {
341
+ "high": "1",
342
+ "normal": "3",
343
+ "low": "5"
344
+ };
345
+ headers["X-Priority"] = priorityMap[message.priority];
346
+ }
347
+ for (const [key, value] of message.headers.entries()) if (!isStandardHeader(key)) headers[key] = value;
348
+ if (Object.keys(headers).length > 0) emailData.headers = headers;
349
+ if (message.attachments.length > 0) emailData.attachments = await Promise.all(message.attachments.map((attachment) => convertAttachment(attachment, signal)));
350
+ signal?.throwIfAborted();
351
+ return emailData;
352
+ }
353
+ function resolveCategory(tags, fallback) {
354
+ return tags[0] ?? fallback;
355
+ }
356
+ function buildCustomVariables(tags, metadata) {
357
+ const customVariables = {};
358
+ if (metadata != null) for (const [key, value] of Object.entries(metadata)) customVariables[key] = value;
359
+ for (const tag of tags.slice(1)) customVariables[`tag_${tag}`] = tag;
360
+ return customVariables;
361
+ }
362
+ function formatAddress(address) {
363
+ if (address.name) return {
364
+ email: address.address,
365
+ name: address.name
366
+ };
367
+ return { email: address.address };
368
+ }
369
+ async function convertAttachment(attachment, signal) {
370
+ signal?.throwIfAborted();
371
+ const contentBytes = await waitForAttachmentContent(attachment.content, signal);
372
+ signal?.throwIfAborted();
373
+ const converted = {
374
+ content: uint8ArrayToBase64(contentBytes),
375
+ filename: attachment.filename
376
+ };
377
+ if (attachment.contentType) converted.type = attachment.contentType;
378
+ if (attachment.inline && attachment.contentId) {
379
+ converted.disposition = "inline";
380
+ converted.content_id = attachment.contentId;
381
+ } else converted.disposition = "attachment";
382
+ return converted;
383
+ }
384
+ function waitForAttachmentContent(content, signal) {
385
+ if (content instanceof Uint8Array) return signal?.aborted ? Promise.reject(abortReason(signal)) : Promise.resolve(content);
386
+ if (signal == null) return content;
387
+ if (signal.aborted) return Promise.reject(abortReason(signal));
388
+ return new Promise((resolve, reject) => {
389
+ const onAbort = () => {
390
+ signal.removeEventListener("abort", onAbort);
391
+ reject(abortReason(signal));
392
+ };
393
+ signal.addEventListener("abort", onAbort, { once: true });
394
+ content.then((value) => {
395
+ signal.removeEventListener("abort", onAbort);
396
+ resolve(value);
397
+ }, (error) => {
398
+ signal.removeEventListener("abort", onAbort);
399
+ reject(error);
400
+ });
401
+ });
402
+ }
403
+ function abortReason(signal) {
404
+ return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
405
+ }
406
+ function uint8ArrayToBase64(bytes) {
407
+ const nativeToBase64 = getNativeToBase64(bytes);
408
+ if (nativeToBase64 != null) return nativeToBase64();
409
+ const bufferBase64 = getBufferBase64(bytes);
410
+ if (bufferBase64 != null) return bufferBase64;
411
+ const chunkSize = 32768;
412
+ const chunks = [];
413
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)));
414
+ return btoa(chunks.join(""));
415
+ }
416
+ function getNativeToBase64(bytes) {
417
+ const candidate = bytes;
418
+ const toBase64 = candidate.toBase64;
419
+ if (typeof toBase64 !== "function") return void 0;
420
+ return () => toBase64.call(bytes);
421
+ }
422
+ function getBufferBase64(bytes) {
423
+ const candidate = globalThis.Buffer;
424
+ if (typeof candidate?.from !== "function") return void 0;
425
+ try {
426
+ const buffer = candidate.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
427
+ if (buffer == null || typeof buffer.toString !== "function") return void 0;
428
+ const base64 = buffer.toString("base64");
429
+ return typeof base64 === "string" ? base64 : void 0;
430
+ } catch {
431
+ return void 0;
432
+ }
433
+ }
434
+ function isStandardHeader(headerName) {
435
+ return STANDARD_HEADERS.has(headerName.toLowerCase());
436
+ }
437
+
438
+ //#endregion
439
+ //#region src/mailtrap-transport.ts
440
+ const MAX_BATCH_SIZE = 500;
441
+ /**
442
+ * Mailtrap transport implementation for sending emails via Mailtrap API.
443
+ *
444
+ * @example
445
+ * ```typescript
446
+ * import { createMessage } from "@upyo/core";
447
+ * import { MailtrapTransport } from "@upyo/mailtrap";
448
+ *
449
+ * const transport = new MailtrapTransport({
450
+ * apiToken: "your-api-token",
451
+ * sandbox: true,
452
+ * inboxId: 12345,
453
+ * });
454
+ *
455
+ * const receipt = await transport.send(createMessage({
456
+ * from: "sender@example.com",
457
+ * to: "recipient@example.com",
458
+ * subject: "Hello from Mailtrap",
459
+ * content: { text: "Hello!" },
460
+ * }));
461
+ * ```
462
+ *
463
+ * @since 0.6.0
464
+ */
465
+ var MailtrapTransport = class {
466
+ id = "mailtrap";
467
+ /**
468
+ * The resolved Mailtrap configuration used by this transport.
469
+ */
470
+ config;
471
+ httpClient;
472
+ /**
473
+ * Creates a new Mailtrap transport instance.
474
+ *
475
+ * @param config Mailtrap configuration including API token and options.
476
+ */
477
+ constructor(config) {
478
+ this.config = createMailtrapConfig(config);
479
+ this.httpClient = new MailtrapHttpClient(this.config);
480
+ }
481
+ /**
482
+ * Sends a single email message via Mailtrap API.
483
+ *
484
+ * @param message The email message to send.
485
+ * @param options Optional transport options including `AbortSignal`.
486
+ * @returns A receipt indicating success or failure.
487
+ * @throws {Error} If the caller aborts the operation.
488
+ */
489
+ async send(message, options) {
490
+ try {
491
+ options?.signal?.throwIfAborted();
492
+ const emailData = await convertMessage(message, this.config, options?.signal);
493
+ options?.signal?.throwIfAborted();
494
+ const response = await this.httpClient.sendMessage(emailData, options?.signal);
495
+ return responseToReceipt(response);
496
+ } catch (error) {
497
+ if (isCallerAbort(error, options?.signal)) throw error;
498
+ return createMailtrapFailure(error instanceof Error ? error.message : String(error), error);
499
+ }
500
+ }
501
+ /**
502
+ * Sends multiple email messages via Mailtrap batch API.
503
+ *
504
+ * Messages are chunked into Mailtrap's maximum batch size of 500 messages.
505
+ *
506
+ * @param messages An iterable or async iterable of messages to send.
507
+ * @param options Optional transport options including `AbortSignal`.
508
+ * @returns An async iterable of receipts, one for each message.
509
+ */
510
+ async *sendMany(messages, options) {
511
+ options?.signal?.throwIfAborted();
512
+ let chunk = [];
513
+ for await (const message of messages) {
514
+ options?.signal?.throwIfAborted();
515
+ chunk.push(message);
516
+ if (chunk.length === MAX_BATCH_SIZE) {
517
+ yield* this.sendBatch(chunk, options);
518
+ chunk = [];
519
+ }
520
+ }
521
+ yield* this.sendBatch(chunk, options);
522
+ }
523
+ async *sendBatch(messages, options) {
524
+ if (messages.length === 0) return;
525
+ const batchData = [];
526
+ const receipts = [];
527
+ for (const message of messages) try {
528
+ batchData.push(await convertMessage(message, this.config, options?.signal));
529
+ receipts.push(void 0);
530
+ } catch (error) {
531
+ if (isCallerAbort(error, options?.signal)) throw error;
532
+ receipts.push(createMailtrapFailure(error instanceof Error ? error.message : String(error), error));
533
+ }
534
+ if (batchData.length === 0) {
535
+ for (const receipt of receipts) if (receipt !== void 0) yield receipt;
536
+ return;
537
+ }
538
+ try {
539
+ options?.signal?.throwIfAborted();
540
+ const response = await this.httpClient.sendBatch(batchData, options?.signal);
541
+ if (response.success === false) {
542
+ const errorMessage = formatErrors(response.errors) ?? "Mailtrap batch request failed.";
543
+ for (const receipt of receipts) {
544
+ if (receipt !== void 0) {
545
+ yield receipt;
546
+ continue;
547
+ }
548
+ yield (0, __upyo_core.createFailedReceipt)(errorMessage, {
549
+ provider: "mailtrap",
550
+ category: "rejected",
551
+ code: "mailtrap.batch_failed",
552
+ retryable: false,
553
+ providerDetails: response
554
+ });
555
+ }
556
+ return;
557
+ }
558
+ const itemResponses = response.responses ?? [];
559
+ let responseIndex = 0;
560
+ for (const receipt of receipts) {
561
+ if (receipt !== void 0) {
562
+ yield receipt;
563
+ continue;
564
+ }
565
+ const item = itemResponses[responseIndex++];
566
+ yield itemResponseToReceipt(item);
567
+ }
568
+ } catch (error) {
569
+ if (isCallerAbort(error, options?.signal)) throw error;
570
+ const errorMessage = error instanceof Error ? error.message : String(error);
571
+ for (const receipt of receipts) {
572
+ if (receipt !== void 0) {
573
+ yield receipt;
574
+ continue;
575
+ }
576
+ yield createMailtrapFailure(errorMessage, error);
577
+ }
578
+ }
579
+ }
580
+ };
581
+ function responseToReceipt(response) {
582
+ return toReceipt(response, {
583
+ unsuccessfulMessage: "Mailtrap reported send failure.",
584
+ unsuccessfulCode: "mailtrap.unsuccessful",
585
+ missingMessageIdMessage: "Mailtrap response is missing a message ID."
586
+ });
587
+ }
588
+ function itemResponseToReceipt(response) {
589
+ return toReceipt(response, {
590
+ unsuccessfulMessage: "Mailtrap reported batch item failure.",
591
+ unsuccessfulCode: "mailtrap.batch_item_failed",
592
+ missingMessageIdMessage: "Mailtrap batch response is missing a message ID."
593
+ });
594
+ }
595
+ function toReceipt(response, options) {
596
+ if (response?.success === false) return (0, __upyo_core.createFailedReceipt)(formatErrors(response.errors) ?? options.unsuccessfulMessage, {
597
+ provider: "mailtrap",
598
+ category: "rejected",
599
+ code: options.unsuccessfulCode,
600
+ retryable: false,
601
+ providerDetails: response
602
+ });
603
+ const messageId = response?.message_ids?.[0];
604
+ if (messageId == null || messageId === "") return (0, __upyo_core.createFailedReceipt)(options.missingMessageIdMessage, {
605
+ provider: "mailtrap",
606
+ category: "unknown",
607
+ code: "mailtrap.missing_message_id",
608
+ retryable: false,
609
+ providerDetails: response
610
+ });
611
+ return {
612
+ successful: true,
613
+ messageId,
614
+ provider: "mailtrap"
615
+ };
616
+ }
617
+ function createMailtrapFailure(message, error) {
618
+ if (error instanceof MailtrapApiError) return (0, __upyo_core.createFailedReceipt)(message, {
619
+ provider: "mailtrap",
620
+ statusCode: error.statusCode,
621
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
622
+ attempts: error.attempts
623
+ });
624
+ if (error instanceof MailtrapTimeoutError) return (0, __upyo_core.createFailedReceipt)(message, {
625
+ provider: "mailtrap",
626
+ category: "timeout",
627
+ code: "timeout",
628
+ retryable: true,
629
+ attempts: error.attempts
630
+ });
631
+ return (0, __upyo_core.createFailedReceipt)(message, {
632
+ provider: "mailtrap",
633
+ attempts: getErrorAttempts(error)
634
+ });
635
+ }
636
+ function formatErrors(errors) {
637
+ if (errors == null || errors.length === 0) return void 0;
638
+ return errors.join("; ");
639
+ }
640
+ function getErrorAttempts(error) {
641
+ if (typeof error !== "object" || error == null || !("attempts" in error)) return void 0;
642
+ const attempts = error.attempts;
643
+ return typeof attempts === "number" ? attempts : void 0;
644
+ }
645
+ function isAbortError(error) {
646
+ return error instanceof Error && error.name === "AbortError";
647
+ }
648
+ function isCallerAbort(error, signal) {
649
+ return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
650
+ }
651
+
652
+ //#endregion
653
+ exports.MailtrapApiError = MailtrapApiError;
654
+ exports.MailtrapTimeoutError = MailtrapTimeoutError;
655
+ exports.MailtrapTransport = MailtrapTransport;
656
+ exports.createMailtrapConfig = createMailtrapConfig;