@upyo/lettermint 0.5.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright 2025 Hong Minhee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ <!-- deno-fmt-ignore-file -->
2
+
3
+ @upyo/lettermint
4
+ ================
5
+
6
+ [![JSR][JSR badge]][JSR]
7
+ [![npm][npm badge]][npm]
8
+
9
+ [Lettermint] transport for the [Upyo] email library.
10
+
11
+ [JSR badge]: https://jsr.io/badges/@upyo/lettermint
12
+ [JSR]: https://jsr.io/@upyo/lettermint
13
+ [npm badge]: https://img.shields.io/npm/v/@upyo/lettermint?logo=npm
14
+ [npm]: https://www.npmjs.com/package/@upyo/lettermint
15
+ [Lettermint]: https://lettermint.co/
16
+ [Upyo]: https://upyo.org/
17
+
18
+
19
+ Features
20
+ --------
21
+
22
+ - Single and batch email sending via Lettermint's HTTP API
23
+ - Idempotency key support through `Message.idempotencyKey`
24
+ - Cross-runtime compatibility (Node.js, Deno, Bun, edge functions)
25
+ - Rich content support: HTML emails, attachments, inline images, and custom
26
+ headers
27
+ - Lettermint route, tag, metadata, and tracking settings
28
+ - Retry logic with exponential backoff
29
+ - Type-safe configuration with sensible defaults
30
+
31
+
32
+ Installation
33
+ ------------
34
+
35
+ ~~~~ sh
36
+ npm add @upyo/core @upyo/lettermint
37
+ pnpm add @upyo/core @upyo/lettermint
38
+ yarn add @upyo/core @upyo/lettermint
39
+ deno add --jsr @upyo/core @upyo/lettermint
40
+ bun add @upyo/core @upyo/lettermint
41
+ ~~~~
42
+
43
+
44
+ Usage
45
+ -----
46
+
47
+ ~~~~ typescript
48
+ import { createMessage } from "@upyo/core";
49
+ import { LettermintTransport } from "@upyo/lettermint";
50
+ import process from "node:process";
51
+
52
+ const message = createMessage({
53
+ from: "sender@example.com",
54
+ to: "recipient@example.net",
55
+ subject: "Hello from Upyo!",
56
+ content: { text: "This is a test email." },
57
+ idempotencyKey: "welcome-recipient-example-net",
58
+ });
59
+
60
+ const transport = new LettermintTransport({
61
+ apiToken: process.env.LETTERMINT_API_TOKEN!,
62
+ });
63
+
64
+ const receipt = await transport.send(message);
65
+ if (receipt.successful) {
66
+ console.log("Message sent with ID:", receipt.messageId);
67
+ } else {
68
+ console.error("Send failed:", receipt.errorMessages.join(", "));
69
+ }
70
+ ~~~~
71
+
72
+ ### Sending multiple emails
73
+
74
+ ~~~~ typescript
75
+ const messages = [message1, message2, message3];
76
+
77
+ for await (const receipt of transport.sendMany(messages)) {
78
+ if (receipt.successful) {
79
+ console.log(`Email sent with ID: ${receipt.messageId}`);
80
+ } else {
81
+ console.error(`Email failed: ${receipt.errorMessages.join(", ")}`);
82
+ }
83
+ }
84
+ ~~~~
85
+
86
+
87
+ Configuration
88
+ -------------
89
+
90
+ See the [Lettermint docs] for more information about configuration options.
91
+
92
+ [Lettermint docs]: https://docs.lettermint.co/
93
+
94
+ ### Available options
95
+
96
+ - `apiToken`: Your Lettermint project sending API token
97
+ - `baseUrl`: Lettermint API base URL (default:
98
+ `https://api.lettermint.co`)
99
+ - `timeout`: Request timeout in milliseconds (default: `30000`)
100
+ - `retries`: Number of retry attempts (default: `3`)
101
+ - `headers`: Additional HTTP headers
102
+ - `route`: Lettermint route for sent messages
103
+ - `tag`: Default Lettermint tag when a message has no `Message.tags`
104
+ - `metadata`: Metadata to track with sent messages
105
+ - `settings`: Lettermint tracking settings (`trackOpens`, `trackClicks`)
package/dist/index.cjs ADDED
@@ -0,0 +1,496 @@
1
+
2
+ //#region src/config.ts
3
+ /**
4
+ * Creates a resolved Lettermint configuration by applying default values.
5
+ *
6
+ * @param config The Lettermint configuration with optional fields.
7
+ * @returns A resolved configuration with all defaults applied.
8
+ * @since 0.5.0
9
+ */
10
+ function createLettermintConfig(config) {
11
+ return {
12
+ apiToken: config.apiToken,
13
+ baseUrl: config.baseUrl ?? "https://api.lettermint.co",
14
+ timeout: config.timeout ?? 3e4,
15
+ retries: config.retries ?? 3,
16
+ headers: config.headers ?? {},
17
+ route: config.route,
18
+ tag: config.tag,
19
+ metadata: config.metadata == null ? void 0 : { ...config.metadata },
20
+ settings: config.settings == null ? void 0 : { ...config.settings }
21
+ };
22
+ }
23
+
24
+ //#endregion
25
+ //#region src/http-client.ts
26
+ /**
27
+ * Lettermint API error class for API-specific failures.
28
+ *
29
+ * @since 0.5.0
30
+ */
31
+ var LettermintApiError = class extends Error {
32
+ statusCode;
33
+ /**
34
+ * Creates a Lettermint API error.
35
+ *
36
+ * @param message Error message.
37
+ * @param statusCode HTTP status code.
38
+ */
39
+ constructor(message, statusCode) {
40
+ super(message);
41
+ this.name = "LettermintApiError";
42
+ this.statusCode = statusCode;
43
+ }
44
+ };
45
+ /**
46
+ * Lettermint request timeout error.
47
+ *
48
+ * @since 0.5.0
49
+ */
50
+ var LettermintTimeoutError = class extends Error {
51
+ timeout;
52
+ /**
53
+ * Creates a Lettermint request timeout error.
54
+ *
55
+ * @param timeout Request timeout in milliseconds.
56
+ */
57
+ constructor(timeout) {
58
+ super(`Lettermint API request timed out after ${timeout} ms.`);
59
+ this.name = "LettermintTimeoutError";
60
+ this.timeout = timeout;
61
+ }
62
+ };
63
+ /**
64
+ * HTTP client wrapper for Lettermint API requests.
65
+ *
66
+ * @since 0.5.0
67
+ */
68
+ var LettermintHttpClient = class {
69
+ config;
70
+ /**
71
+ * Creates a new Lettermint HTTP client.
72
+ *
73
+ * @param config Resolved Lettermint configuration.
74
+ */
75
+ constructor(config) {
76
+ this.config = config;
77
+ }
78
+ /**
79
+ * Sends a single message via Lettermint API.
80
+ *
81
+ * @param messageData The JSON data to send to Lettermint.
82
+ * @param signal Optional AbortSignal for cancellation.
83
+ * @param idempotencyKey Optional idempotency key for request deduplication.
84
+ * @returns Promise that resolves to the Lettermint response.
85
+ */
86
+ sendMessage(messageData, signal, idempotencyKey) {
87
+ const url = `${this.config.baseUrl}/v1/send`;
88
+ return this.makeRequest(url, messageData, signal, idempotencyKey);
89
+ }
90
+ /**
91
+ * Sends multiple messages via Lettermint batch API.
92
+ *
93
+ * @param messagesData The messages to send to Lettermint.
94
+ * @param signal Optional AbortSignal for cancellation.
95
+ * @param idempotencyKey Optional idempotency key for request deduplication.
96
+ * @returns Promise that resolves to the Lettermint batch response.
97
+ */
98
+ sendBatch(messagesData, signal, idempotencyKey) {
99
+ const url = `${this.config.baseUrl}/v1/send/batch`;
100
+ return this.makeRequest(url, messagesData, signal, idempotencyKey);
101
+ }
102
+ async makeRequest(url, body, signal, idempotencyKey) {
103
+ let lastError = null;
104
+ for (let attempt = 0; attempt <= this.config.retries; attempt++) {
105
+ signal?.throwIfAborted();
106
+ try {
107
+ const response = await this.fetchWithAuth(url, body, signal, idempotencyKey);
108
+ const text = await response.text();
109
+ if (!response.ok) throw new LettermintApiError(parseErrorMessage(text, response.status), response.status);
110
+ try {
111
+ return JSON.parse(text);
112
+ } catch (error) {
113
+ throw new SyntaxError(`Invalid JSON response from Lettermint API: ${error instanceof Error ? error.message : String(error)}.`);
114
+ }
115
+ } catch (error) {
116
+ lastError = error instanceof Error ? error : new Error(String(error));
117
+ if (error instanceof LettermintApiError && !isRetryable(error)) throw error;
118
+ if (error instanceof Error && error.name === "AbortError" && signal?.aborted) throw error;
119
+ if (attempt === this.config.retries) throw lastError;
120
+ await sleep(calculateRetryDelay(attempt), signal);
121
+ }
122
+ }
123
+ throw lastError ?? /* @__PURE__ */ new Error("Request failed after all retry attempts.");
124
+ }
125
+ async fetchWithAuth(url, body, signal, idempotencyKey) {
126
+ const headers = new Headers({
127
+ "Content-Type": "application/json",
128
+ "x-lettermint-token": this.config.apiToken
129
+ });
130
+ if (idempotencyKey) headers.set("Idempotency-Key", idempotencyKey);
131
+ for (const [key, value] of Object.entries(this.config.headers)) headers.set(key, value);
132
+ const timeoutController = new AbortController();
133
+ const timeoutId = this.config.timeout > 0 ? setTimeout(() => timeoutController.abort(), this.config.timeout) : void 0;
134
+ const requestSignal = combineSignals(timeoutController.signal, signal);
135
+ try {
136
+ return await globalThis.fetch(url, {
137
+ method: "POST",
138
+ headers,
139
+ body: JSON.stringify(body),
140
+ signal: requestSignal.signal
141
+ });
142
+ } catch (error) {
143
+ if (error instanceof Error && error.name === "AbortError" && timeoutController.signal.aborted && !signal?.aborted) throw new LettermintTimeoutError(this.config.timeout);
144
+ throw error;
145
+ } finally {
146
+ requestSignal.cleanup();
147
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
148
+ }
149
+ }
150
+ };
151
+ function isRetryable(error) {
152
+ return error.statusCode === 408 || error.statusCode === 429 || error.statusCode >= 500;
153
+ }
154
+ function calculateRetryDelay(attempt) {
155
+ const baseDelay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
156
+ return Math.round(baseDelay / 2 + Math.random() * (baseDelay / 2));
157
+ }
158
+ function parseErrorMessage(text, statusCode) {
159
+ try {
160
+ const errorBody = JSON.parse(text);
161
+ if (typeof errorBody.message === "string" && errorBody.message !== "") return errorBody.message;
162
+ if (typeof errorBody.error === "string" && errorBody.error !== "") return errorBody.error;
163
+ if (Array.isArray(errorBody.errors) && errorBody.errors.length > 0) return JSON.stringify(errorBody.errors);
164
+ } catch {}
165
+ return text || `HTTP ${statusCode}`;
166
+ }
167
+ function combineSignals(timeoutSignal, externalSignal) {
168
+ if (externalSignal == null) return {
169
+ signal: timeoutSignal,
170
+ cleanup: () => {}
171
+ };
172
+ if (typeof AbortSignal.any === "function") return {
173
+ signal: AbortSignal.any([timeoutSignal, externalSignal]),
174
+ cleanup: () => {}
175
+ };
176
+ const controller = new AbortController();
177
+ const abort = () => controller.abort();
178
+ timeoutSignal.addEventListener("abort", abort, { once: true });
179
+ externalSignal.addEventListener("abort", abort, { once: true });
180
+ if (timeoutSignal.aborted || externalSignal.aborted) controller.abort();
181
+ return {
182
+ signal: controller.signal,
183
+ cleanup: () => {
184
+ timeoutSignal.removeEventListener("abort", abort);
185
+ externalSignal.removeEventListener("abort", abort);
186
+ }
187
+ };
188
+ }
189
+ function sleep(ms, signal) {
190
+ return new Promise((resolve, reject) => {
191
+ if (signal?.aborted) {
192
+ reject(new DOMException("The operation was aborted.", "AbortError"));
193
+ return;
194
+ }
195
+ const onAbort = () => {
196
+ clearTimeout(timeoutId);
197
+ signal?.removeEventListener("abort", onAbort);
198
+ reject(new DOMException("The operation was aborted.", "AbortError"));
199
+ };
200
+ const timeoutId = setTimeout(() => {
201
+ signal?.removeEventListener("abort", onAbort);
202
+ resolve();
203
+ }, ms);
204
+ if (signal?.aborted) {
205
+ onAbort();
206
+ return;
207
+ }
208
+ signal?.addEventListener("abort", onAbort, { once: true });
209
+ });
210
+ }
211
+
212
+ //#endregion
213
+ //#region src/message-converter.ts
214
+ const STANDARD_HEADERS = new Set([
215
+ "from",
216
+ "to",
217
+ "cc",
218
+ "bcc",
219
+ "reply-to",
220
+ "subject",
221
+ "date",
222
+ "message-id",
223
+ "content-type",
224
+ "content-transfer-encoding",
225
+ "mime-version",
226
+ "x-priority"
227
+ ]);
228
+ /**
229
+ * Converts an Upyo message to Lettermint API JSON format.
230
+ *
231
+ * @param message The Upyo message to convert.
232
+ * @param config The resolved Lettermint configuration.
233
+ * @returns JSON object ready for Lettermint API submission.
234
+ * @throws {RangeError} If the message has more than one tag.
235
+ * @since 0.5.0
236
+ */
237
+ async function convertMessage(message, config) {
238
+ if (message.tags.length > 1) throw new RangeError("Lettermint supports at most one tag per message.");
239
+ const emailData = {
240
+ from: formatAddress(message.sender),
241
+ to: message.recipients.map(formatAddress),
242
+ subject: message.subject
243
+ };
244
+ if (message.ccRecipients.length > 0) emailData.cc = message.ccRecipients.map(formatAddress);
245
+ if (message.bccRecipients.length > 0) emailData.bcc = message.bccRecipients.map(formatAddress);
246
+ if (message.replyRecipients.length > 0) emailData.reply_to = message.replyRecipients.map(formatAddress);
247
+ if ("html" in message.content) {
248
+ emailData.html = message.content.html;
249
+ if (message.content.text) emailData.text = message.content.text;
250
+ } else emailData.text = message.content.text;
251
+ if (config.route) emailData.route = config.route;
252
+ if (message.tags.length === 1) emailData.tag = message.tags[0];
253
+ else if (config.tag !== void 0) emailData.tag = config.tag;
254
+ if (config.metadata != null && Object.keys(config.metadata).length > 0) emailData.metadata = { ...config.metadata };
255
+ const settings = convertSettings(config);
256
+ if (settings != null) emailData.settings = settings;
257
+ const headers = {};
258
+ if (message.priority !== "normal") {
259
+ const priorityMap = {
260
+ "high": "1",
261
+ "normal": "3",
262
+ "low": "5"
263
+ };
264
+ headers["X-Priority"] = priorityMap[message.priority];
265
+ }
266
+ for (const [key, value] of message.headers.entries()) if (!isStandardHeader(key)) headers[key] = value;
267
+ if (Object.keys(headers).length > 0) emailData.headers = headers;
268
+ if (message.attachments.length > 0) emailData.attachments = await Promise.all(message.attachments.map(convertAttachment));
269
+ return emailData;
270
+ }
271
+ function convertSettings(config) {
272
+ if (config.settings == null) return void 0;
273
+ const settings = {};
274
+ if (config.settings.trackOpens !== void 0) settings.track_opens = config.settings.trackOpens;
275
+ if (config.settings.trackClicks !== void 0) settings.track_clicks = config.settings.trackClicks;
276
+ return Object.keys(settings).length > 0 ? settings : void 0;
277
+ }
278
+ function formatAddress(address) {
279
+ if (address.name) {
280
+ const escapedName = address.name.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
281
+ return `"${escapedName}" <${address.address}>`;
282
+ }
283
+ return address.address;
284
+ }
285
+ async function convertAttachment(attachment) {
286
+ const content = await attachment.content;
287
+ const converted = {
288
+ filename: attachment.filename,
289
+ content: uint8ArrayToBase64(content),
290
+ content_type: attachment.contentType
291
+ };
292
+ if (attachment.inline && attachment.contentId) converted.content_id = attachment.contentId;
293
+ return converted;
294
+ }
295
+ function uint8ArrayToBase64(bytes) {
296
+ const chunkSize = 32768;
297
+ const chunks = [];
298
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)));
299
+ return btoa(chunks.join(""));
300
+ }
301
+ function isStandardHeader(headerName) {
302
+ return STANDARD_HEADERS.has(headerName.toLowerCase());
303
+ }
304
+ /**
305
+ * Generates a random idempotency key for request deduplication.
306
+ *
307
+ * @returns A unique string suitable for use as an idempotency key.
308
+ * @since 0.5.0
309
+ */
310
+ function generateIdempotencyKey() {
311
+ return globalThis.crypto.randomUUID();
312
+ }
313
+
314
+ //#endregion
315
+ //#region src/lettermint-transport.ts
316
+ const MAX_BATCH_SIZE = 500;
317
+ /**
318
+ * Lettermint transport implementation for sending emails via Lettermint API.
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * import { createMessage } from "@upyo/core";
323
+ * import { LettermintTransport } from "@upyo/lettermint";
324
+ *
325
+ * const transport = new LettermintTransport({
326
+ * apiToken: "your-project-api-token",
327
+ * });
328
+ *
329
+ * const receipt = await transport.send(createMessage({
330
+ * from: "sender@example.com",
331
+ * to: "recipient@example.com",
332
+ * subject: "Hello from Lettermint",
333
+ * content: { text: "Hello!" },
334
+ * }));
335
+ * ```
336
+ *
337
+ * @since 0.5.0
338
+ */
339
+ var LettermintTransport = class {
340
+ /**
341
+ * The resolved Lettermint configuration used by this transport.
342
+ */
343
+ config;
344
+ httpClient;
345
+ /**
346
+ * Creates a new Lettermint transport instance.
347
+ *
348
+ * @param config Lettermint configuration including API token and options.
349
+ */
350
+ constructor(config) {
351
+ this.config = createLettermintConfig(config);
352
+ this.httpClient = new LettermintHttpClient(this.config);
353
+ }
354
+ /**
355
+ * Sends a single email message via Lettermint API.
356
+ *
357
+ * @param message The email message to send.
358
+ * @param options Optional transport options including `AbortSignal`.
359
+ * @returns A receipt indicating success or failure.
360
+ */
361
+ async send(message, options) {
362
+ try {
363
+ options?.signal?.throwIfAborted();
364
+ const emailData = await convertMessage(message, this.config);
365
+ const idempotencyKey = normalizeIdempotencyKey(message.idempotencyKey);
366
+ options?.signal?.throwIfAborted();
367
+ const response = await this.httpClient.sendMessage(emailData, options?.signal, idempotencyKey);
368
+ return responseToReceipt(response);
369
+ } catch (error) {
370
+ if (isAbortError(error)) throw error;
371
+ return {
372
+ successful: false,
373
+ errorMessages: [error instanceof Error ? error.message : String(error)]
374
+ };
375
+ }
376
+ }
377
+ /**
378
+ * Sends multiple email messages via Lettermint batch API.
379
+ *
380
+ * Messages are chunked into Lettermint's maximum batch size of 500 messages.
381
+ *
382
+ * @param messages An iterable or async iterable of messages to send.
383
+ * @param options Optional transport options including `AbortSignal`.
384
+ * @returns An async iterable of receipts, one for each message.
385
+ */
386
+ async *sendMany(messages, options) {
387
+ options?.signal?.throwIfAborted();
388
+ let chunk = [];
389
+ for await (const message of messages) {
390
+ options?.signal?.throwIfAborted();
391
+ chunk.push(message);
392
+ if (chunk.length === MAX_BATCH_SIZE) {
393
+ yield* this.sendBatch(chunk, options);
394
+ chunk = [];
395
+ }
396
+ }
397
+ yield* this.sendBatch(chunk, options);
398
+ }
399
+ async *sendBatch(messages, options) {
400
+ if (messages.length === 0) return;
401
+ if (messages.some(hasIdempotencyKey)) {
402
+ for (const message of messages) yield await this.send(message, options);
403
+ return;
404
+ }
405
+ const idempotencyKey = normalizeIdempotencyKey(messages[0]?.idempotencyKey);
406
+ const batchData = [];
407
+ const receipts = [];
408
+ for (const message of messages) try {
409
+ batchData.push(await convertMessage(message, this.config));
410
+ receipts.push(void 0);
411
+ } catch (error) {
412
+ receipts.push({
413
+ successful: false,
414
+ errorMessages: [error instanceof Error ? error.message : String(error)]
415
+ });
416
+ }
417
+ if (batchData.length === 0) {
418
+ for (const receipt of receipts) if (receipt !== void 0) yield receipt;
419
+ return;
420
+ }
421
+ try {
422
+ options?.signal?.throwIfAborted();
423
+ const response = await this.httpClient.sendBatch(batchData, options?.signal, idempotencyKey);
424
+ let responseIndex = 0;
425
+ for (let index = 0; index < receipts.length; index++) {
426
+ const receipt = receipts[index];
427
+ if (receipt !== void 0) {
428
+ yield receipt;
429
+ continue;
430
+ }
431
+ const result = response[responseIndex++];
432
+ yield responseToReceipt(result);
433
+ }
434
+ } catch (error) {
435
+ if (isAbortError(error)) throw error;
436
+ const errorMessage = error instanceof Error ? error.message : String(error);
437
+ for (const receipt of receipts) {
438
+ if (receipt !== void 0) {
439
+ yield receipt;
440
+ continue;
441
+ }
442
+ yield {
443
+ successful: false,
444
+ errorMessages: [errorMessage]
445
+ };
446
+ }
447
+ }
448
+ }
449
+ };
450
+ function normalizeIdempotencyKey(idempotencyKey) {
451
+ return idempotencyKey != null && idempotencyKey !== "" ? idempotencyKey : generateIdempotencyKey();
452
+ }
453
+ function hasIdempotencyKey(message) {
454
+ return message.idempotencyKey != null && message.idempotencyKey !== "";
455
+ }
456
+ function responseToReceipt(response) {
457
+ if (response?.message_id == null || response.message_id === "") return {
458
+ successful: false,
459
+ errorMessages: ["Lettermint response is missing a message ID."]
460
+ };
461
+ if (isSuccessfulStatus(response.status)) return {
462
+ successful: true,
463
+ messageId: response.message_id
464
+ };
465
+ return {
466
+ successful: false,
467
+ errorMessages: [`Lettermint reported message status "${response.status}".`]
468
+ };
469
+ }
470
+ function isSuccessfulStatus(status) {
471
+ switch (status) {
472
+ case "pending":
473
+ case "queued":
474
+ case "processed":
475
+ case "delivered":
476
+ case "opened":
477
+ case "clicked": return true;
478
+ case "suppressed":
479
+ case "soft_bounced":
480
+ case "hard_bounced":
481
+ case "spam_complaint":
482
+ case "failed":
483
+ case "blocked":
484
+ case "policy_rejected":
485
+ case "unsubscribed": return false;
486
+ }
487
+ }
488
+ function isAbortError(error) {
489
+ return error instanceof Error && error.name === "AbortError";
490
+ }
491
+
492
+ //#endregion
493
+ exports.LettermintApiError = LettermintApiError;
494
+ exports.LettermintTimeoutError = LettermintTimeoutError;
495
+ exports.LettermintTransport = LettermintTransport;
496
+ exports.createLettermintConfig = createLettermintConfig;