@upyo/mailgun 0.5.0-dev.86 → 0.5.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 CHANGED
@@ -1,3 +1,27 @@
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"));
1
25
 
2
26
  //#region src/config.ts
3
27
  /**
@@ -76,6 +100,8 @@ var MailgunHttpClient = class {
76
100
  * @param url The URL to make the request to.
77
101
  * @param options Fetch options.
78
102
  * @returns Promise that resolves to the parsed response.
103
+ * @throws {MailgunApiError} If Mailgun returns an error response or all
104
+ * retry attempts are exhausted.
79
105
  */
80
106
  async makeRequest(url, options) {
81
107
  let lastError = null;
@@ -87,7 +113,7 @@ var MailgunHttpClient = class {
87
113
  try {
88
114
  errorMessage = JSON.parse(text)?.message;
89
115
  } catch {}
90
- throw new MailgunApiError(errorMessage || text || `HTTP ${response.status}`, response.status);
116
+ throw new MailgunApiError(errorMessage || truncateErrorBody(text) || `HTTP ${response.status}`, response.status, (0, __upyo_core.parseRetryAfter)(response.headers.get("Retry-After")), attempt + 1);
91
117
  }
92
118
  try {
93
119
  return JSON.parse(text);
@@ -96,10 +122,15 @@ var MailgunHttpClient = class {
96
122
  }
97
123
  } catch (error) {
98
124
  lastError = error instanceof Error ? error : new Error(String(error));
125
+ if (options.signal?.aborted) throw createAbortError(options.signal);
126
+ if (isAbortError$1(error)) throw error;
99
127
  if (error instanceof MailgunApiError && error.statusCode && error.statusCode >= 400 && error.statusCode < 500) throw error;
100
- if (attempt === this.config.retries) throw error;
128
+ if (attempt === this.config.retries) {
129
+ if (lastError instanceof MailgunApiError) throw lastError;
130
+ throw new MailgunApiError(lastError.message, void 0, void 0, attempt + 1);
131
+ }
101
132
  const delay = Math.pow(2, attempt) * 1e3;
102
- await new Promise((resolve) => setTimeout(resolve, delay));
133
+ await sleep(delay, options.signal);
103
134
  }
104
135
  throw lastError || /* @__PURE__ */ new Error("Request failed after all retries");
105
136
  }
@@ -109,6 +140,8 @@ var MailgunHttpClient = class {
109
140
  * @param url The URL to make the request to.
110
141
  * @param options Fetch options.
111
142
  * @returns Promise that resolves to the fetch response.
143
+ * @throws {Error} If the configured request timeout is reached.
144
+ * @throws {DOMException} If the caller aborts the request.
112
145
  */
113
146
  async fetchWithAuth(url, options) {
114
147
  const headers = new Headers(options.headers);
@@ -117,30 +150,80 @@ var MailgunHttpClient = class {
117
150
  for (const [key, value] of Object.entries(this.config.headers)) headers.set(key, value);
118
151
  const controller = new AbortController();
119
152
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
120
- let signal = controller.signal;
121
- if (options.signal) signal = AbortSignal.any([controller.signal, options.signal]);
153
+ const combinedSignal = (0, __upyo_core.combineSignals)(controller.signal, options.signal);
122
154
  try {
123
155
  return await globalThis.fetch(url, {
124
156
  ...options,
125
157
  headers,
126
- signal
158
+ signal: combinedSignal.signal
127
159
  });
160
+ } catch (error) {
161
+ if (isAbortError$1(error) && controller.signal.aborted && !options.signal?.aborted) throw new Error(`Mailgun API request timed out after ${this.config.timeout} ms.`);
162
+ throw error;
128
163
  } finally {
164
+ combinedSignal.cleanup();
129
165
  clearTimeout(timeoutId);
130
166
  }
131
167
  }
132
168
  };
133
169
  /**
134
- * Custom error class for Mailgun API errors.
170
+ * Error thrown when a Mailgun API request fails.
171
+ *
172
+ * @since 0.5.0
135
173
  */
136
174
  var MailgunApiError = class extends Error {
175
+ /**
176
+ * HTTP status code returned by Mailgun, if the request reached the API.
177
+ */
137
178
  statusCode;
138
- constructor(message, statusCode) {
179
+ /**
180
+ * Retry delay from Mailgun's `Retry-After` response header.
181
+ */
182
+ retryAfterMilliseconds;
183
+ /**
184
+ * Number of attempts made before this error was produced.
185
+ */
186
+ attempts;
187
+ /**
188
+ * Creates a Mailgun API error.
189
+ *
190
+ * @param message Error message.
191
+ * @param statusCode HTTP status code returned by Mailgun.
192
+ * @param retryAfterMilliseconds Retry delay from the response.
193
+ * @param attempts Number of attempts made before this error.
194
+ */
195
+ constructor(message, statusCode, retryAfterMilliseconds, attempts) {
139
196
  super(message);
140
197
  this.name = "MailgunApiError";
141
198
  this.statusCode = statusCode;
199
+ this.retryAfterMilliseconds = retryAfterMilliseconds;
200
+ this.attempts = attempts;
142
201
  }
143
202
  };
203
+ function isAbortError$1(error) {
204
+ return error instanceof Error && error.name === "AbortError";
205
+ }
206
+ function sleep(milliseconds, signal) {
207
+ if (signal?.aborted) return Promise.reject(createAbortError(signal));
208
+ return new Promise((resolve, reject) => {
209
+ function abort() {
210
+ clearTimeout(timeoutId);
211
+ signal?.removeEventListener("abort", abort);
212
+ reject(createAbortError(signal));
213
+ }
214
+ const timeoutId = setTimeout(() => {
215
+ signal?.removeEventListener("abort", abort);
216
+ resolve();
217
+ }, milliseconds);
218
+ signal?.addEventListener("abort", abort, { once: true });
219
+ });
220
+ }
221
+ function createAbortError(signal) {
222
+ return signal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
223
+ }
224
+ function truncateErrorBody(text) {
225
+ return text.length > 500 ? `${text.slice(0, 500)}...` : text;
226
+ }
144
227
 
145
228
  //#endregion
146
229
  //#region src/message-converter.ts
@@ -267,6 +350,7 @@ function isStandardHeader(headerName) {
267
350
  * ```
268
351
  */
269
352
  var MailgunTransport = class {
353
+ id = "mailgun";
270
354
  /**
271
355
  * The resolved Mailgun configuration used by this transport.
272
356
  */
@@ -322,14 +406,13 @@ var MailgunTransport = class {
322
406
  const response = await this.httpClient.sendMessage(formData, options?.signal);
323
407
  return {
324
408
  successful: true,
325
- messageId: response.id
409
+ messageId: response.id,
410
+ provider: "mailgun"
326
411
  };
327
412
  } catch (error) {
413
+ if (isCallerAbort(error, options?.signal)) throw error;
328
414
  const errorMessage = error instanceof Error ? error.message : String(error);
329
- return {
330
- successful: false,
331
- errorMessages: [errorMessage]
332
- };
415
+ return createMailgunFailure(errorMessage, error);
333
416
  }
334
417
  }
335
418
  /**
@@ -397,6 +480,21 @@ var MailgunTransport = class {
397
480
  }
398
481
  }
399
482
  };
483
+ function createMailgunFailure(message, error) {
484
+ if (error instanceof MailgunApiError) return (0, __upyo_core.createFailedReceipt)(message, {
485
+ provider: "mailgun",
486
+ statusCode: error.statusCode,
487
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
488
+ attempts: error.attempts
489
+ });
490
+ return (0, __upyo_core.createFailedReceipt)(message, { provider: "mailgun" });
491
+ }
492
+ function isCallerAbort(error, signal) {
493
+ return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
494
+ }
495
+ function isAbortError(error) {
496
+ return error instanceof Error && error.name === "AbortError";
497
+ }
400
498
 
401
499
  //#endregion
402
500
  exports.MailgunApiError = MailgunApiError;
package/dist/index.d.cts CHANGED
@@ -132,7 +132,8 @@ type ResolvedMailgunConfig = Required<MailgunConfig>;
132
132
  * }
133
133
  * ```
134
134
  */
135
- declare class MailgunTransport implements Transport {
135
+ declare class MailgunTransport implements Transport<"mailgun"> {
136
+ readonly id = "mailgun";
136
137
  /**
137
138
  * The resolved Mailgun configuration used by this transport.
138
139
  */
@@ -177,7 +178,7 @@ declare class MailgunTransport implements Transport {
177
178
  * @returns A promise that resolves to a receipt indicating success or
178
179
  * failure.
179
180
  */
180
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
181
+ send(message: Message, options?: TransportOptions): Promise<Receipt<"mailgun">>;
181
182
  /**
182
183
  * Sends multiple email messages efficiently via Mailgun API.
183
184
  *
@@ -230,17 +231,38 @@ declare class MailgunTransport implements Transport {
230
231
  * cancellation.
231
232
  * @returns An async iterable of receipts, one for each message.
232
233
  */
233
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
234
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"mailgun">>;
234
235
  }
235
236
  //#endregion
236
237
  //#region src/http-client.d.ts
237
238
 
238
239
  /**
239
- * Custom error class for Mailgun API errors.
240
+ * Error thrown when a Mailgun API request fails.
241
+ *
242
+ * @since 0.5.0
240
243
  */
241
244
  declare class MailgunApiError extends Error {
245
+ /**
246
+ * HTTP status code returned by Mailgun, if the request reached the API.
247
+ */
242
248
  statusCode?: number;
243
- constructor(message: string, statusCode?: number);
249
+ /**
250
+ * Retry delay from Mailgun's `Retry-After` response header.
251
+ */
252
+ retryAfterMilliseconds?: number;
253
+ /**
254
+ * Number of attempts made before this error was produced.
255
+ */
256
+ attempts?: number;
257
+ /**
258
+ * Creates a Mailgun API error.
259
+ *
260
+ * @param message Error message.
261
+ * @param statusCode HTTP status code returned by Mailgun.
262
+ * @param retryAfterMilliseconds Retry delay from the response.
263
+ * @param attempts Number of attempts made before this error.
264
+ */
265
+ constructor(message: string, statusCode?: number, retryAfterMilliseconds?: number, attempts?: number);
244
266
  }
245
267
  //#endregion
246
268
  export { MailgunApiError, MailgunConfig, MailgunTransport };
package/dist/index.d.ts CHANGED
@@ -132,7 +132,8 @@ type ResolvedMailgunConfig = Required<MailgunConfig>;
132
132
  * }
133
133
  * ```
134
134
  */
135
- declare class MailgunTransport implements Transport {
135
+ declare class MailgunTransport implements Transport<"mailgun"> {
136
+ readonly id = "mailgun";
136
137
  /**
137
138
  * The resolved Mailgun configuration used by this transport.
138
139
  */
@@ -177,7 +178,7 @@ declare class MailgunTransport implements Transport {
177
178
  * @returns A promise that resolves to a receipt indicating success or
178
179
  * failure.
179
180
  */
180
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
181
+ send(message: Message, options?: TransportOptions): Promise<Receipt<"mailgun">>;
181
182
  /**
182
183
  * Sends multiple email messages efficiently via Mailgun API.
183
184
  *
@@ -230,17 +231,38 @@ declare class MailgunTransport implements Transport {
230
231
  * cancellation.
231
232
  * @returns An async iterable of receipts, one for each message.
232
233
  */
233
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
234
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"mailgun">>;
234
235
  }
235
236
  //#endregion
236
237
  //#region src/http-client.d.ts
237
238
 
238
239
  /**
239
- * Custom error class for Mailgun API errors.
240
+ * Error thrown when a Mailgun API request fails.
241
+ *
242
+ * @since 0.5.0
240
243
  */
241
244
  declare class MailgunApiError extends Error {
245
+ /**
246
+ * HTTP status code returned by Mailgun, if the request reached the API.
247
+ */
242
248
  statusCode?: number;
243
- constructor(message: string, statusCode?: number);
249
+ /**
250
+ * Retry delay from Mailgun's `Retry-After` response header.
251
+ */
252
+ retryAfterMilliseconds?: number;
253
+ /**
254
+ * Number of attempts made before this error was produced.
255
+ */
256
+ attempts?: number;
257
+ /**
258
+ * Creates a Mailgun API error.
259
+ *
260
+ * @param message Error message.
261
+ * @param statusCode HTTP status code returned by Mailgun.
262
+ * @param retryAfterMilliseconds Retry delay from the response.
263
+ * @param attempts Number of attempts made before this error.
264
+ */
265
+ constructor(message: string, statusCode?: number, retryAfterMilliseconds?: number, attempts?: number);
244
266
  }
245
267
  //#endregion
246
268
  export { MailgunApiError, MailgunConfig, MailgunTransport };
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { combineSignals, createFailedReceipt, parseRetryAfter } from "@upyo/core";
2
+
1
3
  //#region src/config.ts
2
4
  /**
3
5
  * Creates a resolved Mailgun configuration by applying default values to optional fields.
@@ -75,6 +77,8 @@ var MailgunHttpClient = class {
75
77
  * @param url The URL to make the request to.
76
78
  * @param options Fetch options.
77
79
  * @returns Promise that resolves to the parsed response.
80
+ * @throws {MailgunApiError} If Mailgun returns an error response or all
81
+ * retry attempts are exhausted.
78
82
  */
79
83
  async makeRequest(url, options) {
80
84
  let lastError = null;
@@ -86,7 +90,7 @@ var MailgunHttpClient = class {
86
90
  try {
87
91
  errorMessage = JSON.parse(text)?.message;
88
92
  } catch {}
89
- throw new MailgunApiError(errorMessage || text || `HTTP ${response.status}`, response.status);
93
+ throw new MailgunApiError(errorMessage || truncateErrorBody(text) || `HTTP ${response.status}`, response.status, parseRetryAfter(response.headers.get("Retry-After")), attempt + 1);
90
94
  }
91
95
  try {
92
96
  return JSON.parse(text);
@@ -95,10 +99,15 @@ var MailgunHttpClient = class {
95
99
  }
96
100
  } catch (error) {
97
101
  lastError = error instanceof Error ? error : new Error(String(error));
102
+ if (options.signal?.aborted) throw createAbortError(options.signal);
103
+ if (isAbortError$1(error)) throw error;
98
104
  if (error instanceof MailgunApiError && error.statusCode && error.statusCode >= 400 && error.statusCode < 500) throw error;
99
- if (attempt === this.config.retries) throw error;
105
+ if (attempt === this.config.retries) {
106
+ if (lastError instanceof MailgunApiError) throw lastError;
107
+ throw new MailgunApiError(lastError.message, void 0, void 0, attempt + 1);
108
+ }
100
109
  const delay = Math.pow(2, attempt) * 1e3;
101
- await new Promise((resolve) => setTimeout(resolve, delay));
110
+ await sleep(delay, options.signal);
102
111
  }
103
112
  throw lastError || /* @__PURE__ */ new Error("Request failed after all retries");
104
113
  }
@@ -108,6 +117,8 @@ var MailgunHttpClient = class {
108
117
  * @param url The URL to make the request to.
109
118
  * @param options Fetch options.
110
119
  * @returns Promise that resolves to the fetch response.
120
+ * @throws {Error} If the configured request timeout is reached.
121
+ * @throws {DOMException} If the caller aborts the request.
111
122
  */
112
123
  async fetchWithAuth(url, options) {
113
124
  const headers = new Headers(options.headers);
@@ -116,30 +127,80 @@ var MailgunHttpClient = class {
116
127
  for (const [key, value] of Object.entries(this.config.headers)) headers.set(key, value);
117
128
  const controller = new AbortController();
118
129
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
119
- let signal = controller.signal;
120
- if (options.signal) signal = AbortSignal.any([controller.signal, options.signal]);
130
+ const combinedSignal = combineSignals(controller.signal, options.signal);
121
131
  try {
122
132
  return await globalThis.fetch(url, {
123
133
  ...options,
124
134
  headers,
125
- signal
135
+ signal: combinedSignal.signal
126
136
  });
137
+ } catch (error) {
138
+ if (isAbortError$1(error) && controller.signal.aborted && !options.signal?.aborted) throw new Error(`Mailgun API request timed out after ${this.config.timeout} ms.`);
139
+ throw error;
127
140
  } finally {
141
+ combinedSignal.cleanup();
128
142
  clearTimeout(timeoutId);
129
143
  }
130
144
  }
131
145
  };
132
146
  /**
133
- * Custom error class for Mailgun API errors.
147
+ * Error thrown when a Mailgun API request fails.
148
+ *
149
+ * @since 0.5.0
134
150
  */
135
151
  var MailgunApiError = class extends Error {
152
+ /**
153
+ * HTTP status code returned by Mailgun, if the request reached the API.
154
+ */
136
155
  statusCode;
137
- constructor(message, statusCode) {
156
+ /**
157
+ * Retry delay from Mailgun's `Retry-After` response header.
158
+ */
159
+ retryAfterMilliseconds;
160
+ /**
161
+ * Number of attempts made before this error was produced.
162
+ */
163
+ attempts;
164
+ /**
165
+ * Creates a Mailgun API error.
166
+ *
167
+ * @param message Error message.
168
+ * @param statusCode HTTP status code returned by Mailgun.
169
+ * @param retryAfterMilliseconds Retry delay from the response.
170
+ * @param attempts Number of attempts made before this error.
171
+ */
172
+ constructor(message, statusCode, retryAfterMilliseconds, attempts) {
138
173
  super(message);
139
174
  this.name = "MailgunApiError";
140
175
  this.statusCode = statusCode;
176
+ this.retryAfterMilliseconds = retryAfterMilliseconds;
177
+ this.attempts = attempts;
141
178
  }
142
179
  };
180
+ function isAbortError$1(error) {
181
+ return error instanceof Error && error.name === "AbortError";
182
+ }
183
+ function sleep(milliseconds, signal) {
184
+ if (signal?.aborted) return Promise.reject(createAbortError(signal));
185
+ return new Promise((resolve, reject) => {
186
+ function abort() {
187
+ clearTimeout(timeoutId);
188
+ signal?.removeEventListener("abort", abort);
189
+ reject(createAbortError(signal));
190
+ }
191
+ const timeoutId = setTimeout(() => {
192
+ signal?.removeEventListener("abort", abort);
193
+ resolve();
194
+ }, milliseconds);
195
+ signal?.addEventListener("abort", abort, { once: true });
196
+ });
197
+ }
198
+ function createAbortError(signal) {
199
+ return signal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
200
+ }
201
+ function truncateErrorBody(text) {
202
+ return text.length > 500 ? `${text.slice(0, 500)}...` : text;
203
+ }
143
204
 
144
205
  //#endregion
145
206
  //#region src/message-converter.ts
@@ -266,6 +327,7 @@ function isStandardHeader(headerName) {
266
327
  * ```
267
328
  */
268
329
  var MailgunTransport = class {
330
+ id = "mailgun";
269
331
  /**
270
332
  * The resolved Mailgun configuration used by this transport.
271
333
  */
@@ -321,14 +383,13 @@ var MailgunTransport = class {
321
383
  const response = await this.httpClient.sendMessage(formData, options?.signal);
322
384
  return {
323
385
  successful: true,
324
- messageId: response.id
386
+ messageId: response.id,
387
+ provider: "mailgun"
325
388
  };
326
389
  } catch (error) {
390
+ if (isCallerAbort(error, options?.signal)) throw error;
327
391
  const errorMessage = error instanceof Error ? error.message : String(error);
328
- return {
329
- successful: false,
330
- errorMessages: [errorMessage]
331
- };
392
+ return createMailgunFailure(errorMessage, error);
332
393
  }
333
394
  }
334
395
  /**
@@ -396,6 +457,21 @@ var MailgunTransport = class {
396
457
  }
397
458
  }
398
459
  };
460
+ function createMailgunFailure(message, error) {
461
+ if (error instanceof MailgunApiError) return createFailedReceipt(message, {
462
+ provider: "mailgun",
463
+ statusCode: error.statusCode,
464
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
465
+ attempts: error.attempts
466
+ });
467
+ return createFailedReceipt(message, { provider: "mailgun" });
468
+ }
469
+ function isCallerAbort(error, signal) {
470
+ return signal?.aborted === true && (isAbortError(error) || error === signal.reason);
471
+ }
472
+ function isAbortError(error) {
473
+ return error instanceof Error && error.name === "AbortError";
474
+ }
399
475
 
400
476
  //#endregion
401
477
  export { MailgunApiError, MailgunTransport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/mailgun",
3
- "version": "0.5.0-dev.86",
3
+ "version": "0.5.0",
4
4
  "description": "Mailgun transport for Upyo email library",
5
5
  "keywords": [
6
6
  "email",
@@ -53,18 +53,13 @@
53
53
  },
54
54
  "sideEffects": false,
55
55
  "peerDependencies": {
56
- "@upyo/core": "0.5.0-dev.86+2a12a704"
56
+ "@upyo/core": "0.5.0"
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"
63
+ "prepublish": "mise run --no-deps :build"
69
64
  }
70
65
  }