@upyo/maileroo 0.6.0-dev.0 → 0.6.0-dev.225
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 +93 -32
- package/dist/index.d.cts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +93 -32
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -37,7 +37,7 @@ function createMailerooConfig(config) {
|
|
|
37
37
|
baseUrl: normalizeBaseUrl(config.baseUrl ?? "https://smtp.maileroo.com/api/v2"),
|
|
38
38
|
timeout: config.timeout ?? 3e4,
|
|
39
39
|
retries: config.retries ?? 3,
|
|
40
|
-
headers: config.headers
|
|
40
|
+
headers: config.headers == null ? {} : { ...config.headers },
|
|
41
41
|
tracking: config.tracking,
|
|
42
42
|
tags: config.tags == null ? void 0 : { ...config.tags }
|
|
43
43
|
};
|
|
@@ -48,6 +48,7 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
48
48
|
|
|
49
49
|
//#endregion
|
|
50
50
|
//#region src/http-client.ts
|
|
51
|
+
const maxErrorMessageLength = 500;
|
|
51
52
|
/**
|
|
52
53
|
* Maileroo API error class for API-specific failures.
|
|
53
54
|
*
|
|
@@ -125,6 +126,8 @@ var MailerooHttpClient = class {
|
|
|
125
126
|
* @param messageData The JSON data to send to Maileroo.
|
|
126
127
|
* @param signal Optional AbortSignal for cancellation.
|
|
127
128
|
* @returns Promise that resolves to the Maileroo response.
|
|
129
|
+
* @throws {MailerooApiError} If Maileroo returns an API error.
|
|
130
|
+
* @throws {MailerooTimeoutError} If the request timeout elapses.
|
|
128
131
|
*/
|
|
129
132
|
sendMessage(messageData, signal) {
|
|
130
133
|
const url = `${this.config.baseUrl}/emails`;
|
|
@@ -135,20 +138,15 @@ var MailerooHttpClient = class {
|
|
|
135
138
|
for (let attempt = 0; attempt <= this.config.retries; attempt++) {
|
|
136
139
|
signal?.throwIfAborted();
|
|
137
140
|
try {
|
|
138
|
-
const response = await this.fetchWithAuth(url, body, signal);
|
|
139
|
-
const text = await response.text();
|
|
141
|
+
const { response, text } = await this.fetchWithAuth(url, body, signal);
|
|
140
142
|
if (!response.ok) throw new MailerooApiError(parseErrorMessage(text, response.status), response.status, (0, __upyo_core.parseRetryAfter)(response.headers.get("Retry-After")), attempt + 1);
|
|
141
|
-
|
|
142
|
-
return JSON.parse(text);
|
|
143
|
-
} catch (error) {
|
|
144
|
-
throw new SyntaxError(`Invalid JSON response from Maileroo API: ${error instanceof Error ? error.message : String(error)}.`);
|
|
145
|
-
}
|
|
143
|
+
return parseResponse(text);
|
|
146
144
|
} catch (error) {
|
|
147
145
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
146
|
+
if (signal?.aborted) throw error;
|
|
148
147
|
if (error instanceof MailerooApiError && !isRetryable(error)) throw error;
|
|
149
|
-
if (error instanceof Error && error.name === "AbortError" && signal?.aborted) throw error;
|
|
150
148
|
if (attempt === this.config.retries) throw withAttempts(lastError, attempt + 1);
|
|
151
|
-
await sleep(calculateRetryDelay(attempt), signal);
|
|
149
|
+
await sleep(calculateRetryDelay(attempt, lastError), signal);
|
|
152
150
|
}
|
|
153
151
|
}
|
|
154
152
|
throw lastError ?? /* @__PURE__ */ new Error("Request failed after all retry attempts.");
|
|
@@ -163,12 +161,17 @@ var MailerooHttpClient = class {
|
|
|
163
161
|
const timeoutId = this.config.timeout > 0 ? setTimeout(() => timeoutController.abort(), this.config.timeout) : void 0;
|
|
164
162
|
const requestSignal = (0, __upyo_core.combineSignals)(timeoutController.signal, signal);
|
|
165
163
|
try {
|
|
166
|
-
|
|
164
|
+
const response = await globalThis.fetch(url, {
|
|
167
165
|
method: "POST",
|
|
168
166
|
headers,
|
|
169
167
|
body: JSON.stringify(body),
|
|
170
168
|
signal: requestSignal.signal
|
|
171
169
|
});
|
|
170
|
+
const text = await response.text();
|
|
171
|
+
return {
|
|
172
|
+
response,
|
|
173
|
+
text
|
|
174
|
+
};
|
|
172
175
|
} catch (error) {
|
|
173
176
|
if (error instanceof Error && error.name === "AbortError" && timeoutController.signal.aborted && !signal?.aborted) throw new MailerooTimeoutError(this.config.timeout);
|
|
174
177
|
throw error;
|
|
@@ -186,39 +189,60 @@ function withAttempts(error, attempts) {
|
|
|
186
189
|
function isRetryable(error) {
|
|
187
190
|
return error.statusCode === 408 || error.statusCode === 429 || error.statusCode >= 500;
|
|
188
191
|
}
|
|
189
|
-
function calculateRetryDelay(attempt) {
|
|
192
|
+
function calculateRetryDelay(attempt, error) {
|
|
190
193
|
const baseDelay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
|
|
191
|
-
|
|
194
|
+
const backoffDelay = Math.round(baseDelay / 2 + Math.random() * (baseDelay / 2));
|
|
195
|
+
if (error instanceof MailerooApiError) return Math.max(backoffDelay, error.retryAfterMilliseconds ?? 0);
|
|
196
|
+
return backoffDelay;
|
|
192
197
|
}
|
|
193
198
|
function parseErrorMessage(text, statusCode) {
|
|
194
199
|
try {
|
|
195
200
|
const errorBody = JSON.parse(text);
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
if (errorBody != null && typeof errorBody === "object") {
|
|
202
|
+
const mailerooError = errorBody;
|
|
203
|
+
if (typeof mailerooError.message === "string" && mailerooError.message !== "") return truncateErrorMessage(mailerooError.message);
|
|
204
|
+
if (typeof mailerooError.error === "string" && mailerooError.error !== "") return truncateErrorMessage(mailerooError.error);
|
|
205
|
+
if (Array.isArray(mailerooError.errors) && mailerooError.errors.length > 0) return truncateErrorMessage(JSON.stringify(mailerooError.errors));
|
|
206
|
+
}
|
|
199
207
|
} catch {}
|
|
200
|
-
return text || `HTTP ${statusCode}`;
|
|
208
|
+
return truncateErrorMessage(text) || `HTTP ${statusCode}`;
|
|
209
|
+
}
|
|
210
|
+
function parseResponse(text) {
|
|
211
|
+
try {
|
|
212
|
+
const response = JSON.parse(text);
|
|
213
|
+
if (response == null || typeof response !== "object") throw new SyntaxError("response is not an object");
|
|
214
|
+
return response;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw new SyntaxError(`Invalid JSON response from Maileroo API: ${error instanceof Error ? error.message : String(error)}.`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function truncateErrorMessage(message) {
|
|
220
|
+
return message.length > maxErrorMessageLength ? `${message.slice(0, maxErrorMessageLength)}...` : message;
|
|
221
|
+
}
|
|
222
|
+
function abortReason$1(signal) {
|
|
223
|
+
return signal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
201
224
|
}
|
|
202
225
|
function sleep(ms, signal) {
|
|
203
226
|
return new Promise((resolve, reject) => {
|
|
204
227
|
if (signal?.aborted) {
|
|
205
|
-
reject(
|
|
228
|
+
reject(abortReason$1(signal));
|
|
206
229
|
return;
|
|
207
230
|
}
|
|
231
|
+
const timeoutState = {};
|
|
208
232
|
const onAbort = () => {
|
|
209
|
-
clearTimeout(
|
|
233
|
+
if (timeoutState.id !== void 0) clearTimeout(timeoutState.id);
|
|
210
234
|
signal?.removeEventListener("abort", onAbort);
|
|
211
|
-
reject(
|
|
235
|
+
reject(abortReason$1(signal));
|
|
212
236
|
};
|
|
213
|
-
|
|
214
|
-
signal?.removeEventListener("abort", onAbort);
|
|
215
|
-
resolve();
|
|
216
|
-
}, ms);
|
|
237
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
217
238
|
if (signal?.aborted) {
|
|
218
239
|
onAbort();
|
|
219
240
|
return;
|
|
220
241
|
}
|
|
221
|
-
|
|
242
|
+
timeoutState.id = setTimeout(() => {
|
|
243
|
+
signal?.removeEventListener("abort", onAbort);
|
|
244
|
+
resolve();
|
|
245
|
+
}, ms);
|
|
222
246
|
});
|
|
223
247
|
}
|
|
224
248
|
|
|
@@ -243,10 +267,13 @@ const STANDARD_HEADERS = new Set([
|
|
|
243
267
|
*
|
|
244
268
|
* @param message The Upyo message to convert.
|
|
245
269
|
* @param config The resolved Maileroo configuration.
|
|
270
|
+
* @param signal Optional abort signal for cancellation.
|
|
246
271
|
* @returns JSON object ready for Maileroo API submission.
|
|
272
|
+
* @throws {Error} If the caller aborts the operation.
|
|
247
273
|
* @since 0.6.0
|
|
248
274
|
*/
|
|
249
|
-
async function convertMessage(message, config) {
|
|
275
|
+
async function convertMessage(message, config, signal) {
|
|
276
|
+
signal?.throwIfAborted();
|
|
250
277
|
const emailData = {
|
|
251
278
|
from: convertAddress(message.sender),
|
|
252
279
|
to: convertAddressList(message.recipients),
|
|
@@ -267,7 +294,8 @@ async function convertMessage(message, config) {
|
|
|
267
294
|
if (Object.keys(tags).length > 0) emailData.tags = tags;
|
|
268
295
|
const headers = convertHeaders(message);
|
|
269
296
|
if (Object.keys(headers).length > 0) emailData.headers = headers;
|
|
270
|
-
if (message.attachments.length > 0) emailData.attachments = await Promise.all(message.attachments.map(convertAttachment));
|
|
297
|
+
if (message.attachments.length > 0) emailData.attachments = await Promise.all(message.attachments.map((attachment) => convertAttachment(attachment, signal)));
|
|
298
|
+
signal?.throwIfAborted();
|
|
271
299
|
return emailData;
|
|
272
300
|
}
|
|
273
301
|
function convertAddress(address) {
|
|
@@ -301,19 +329,47 @@ function convertHeaders(message) {
|
|
|
301
329
|
for (const [key, value] of message.headers.entries()) if (!isStandardHeader(key)) headers[key] = value;
|
|
302
330
|
return headers;
|
|
303
331
|
}
|
|
304
|
-
async function convertAttachment(attachment) {
|
|
305
|
-
|
|
332
|
+
async function convertAttachment(attachment, signal) {
|
|
333
|
+
signal?.throwIfAborted();
|
|
334
|
+
const content = await waitForAttachmentContent(attachment.content, signal);
|
|
335
|
+
signal?.throwIfAborted();
|
|
306
336
|
return {
|
|
307
|
-
file_name: attachment
|
|
337
|
+
file_name: getAttachmentFileName(attachment),
|
|
308
338
|
content_type: attachment.contentType,
|
|
309
339
|
content: uint8ArrayToBase64(content),
|
|
310
340
|
inline: attachment.inline || void 0
|
|
311
341
|
};
|
|
312
342
|
}
|
|
343
|
+
function waitForAttachmentContent(content, signal) {
|
|
344
|
+
if (signal == null) return Promise.resolve(content);
|
|
345
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
346
|
+
return new Promise((resolve, reject) => {
|
|
347
|
+
const onAbort = () => {
|
|
348
|
+
signal.removeEventListener("abort", onAbort);
|
|
349
|
+
reject(abortReason(signal));
|
|
350
|
+
};
|
|
351
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
352
|
+
Promise.resolve(content).then((value) => {
|
|
353
|
+
signal.removeEventListener("abort", onAbort);
|
|
354
|
+
resolve(value);
|
|
355
|
+
}, (error) => {
|
|
356
|
+
signal.removeEventListener("abort", onAbort);
|
|
357
|
+
reject(error);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function abortReason(signal) {
|
|
362
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
363
|
+
}
|
|
364
|
+
function getAttachmentFileName(attachment) {
|
|
365
|
+
return attachment.inline && typeof attachment.contentId === "string" && attachment.contentId !== "" ? attachment.contentId : attachment.filename;
|
|
366
|
+
}
|
|
313
367
|
function uint8ArrayToBase64(bytes) {
|
|
314
368
|
const nativeToBase64 = getNativeToBase64(bytes);
|
|
315
369
|
if (nativeToBase64 != null) return nativeToBase64();
|
|
316
|
-
const
|
|
370
|
+
const bufferConverter = getBufferBase64Converter();
|
|
371
|
+
if (bufferConverter != null) return bufferConverter.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
|
|
372
|
+
const chunkSize = 4096;
|
|
317
373
|
const chunks = [];
|
|
318
374
|
for (let offset = 0; offset < bytes.length; offset += chunkSize) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)));
|
|
319
375
|
return btoa(chunks.join(""));
|
|
@@ -324,6 +380,10 @@ function getNativeToBase64(bytes) {
|
|
|
324
380
|
if (typeof toBase64 !== "function") return void 0;
|
|
325
381
|
return () => toBase64.call(bytes);
|
|
326
382
|
}
|
|
383
|
+
function getBufferBase64Converter() {
|
|
384
|
+
const candidate = globalThis.Buffer;
|
|
385
|
+
return typeof candidate?.from === "function" ? candidate : void 0;
|
|
386
|
+
}
|
|
327
387
|
function isStandardHeader(headerName) {
|
|
328
388
|
return STANDARD_HEADERS.has(headerName.toLowerCase());
|
|
329
389
|
}
|
|
@@ -374,11 +434,12 @@ var MailerooTransport = class {
|
|
|
374
434
|
* @param message The email message to send.
|
|
375
435
|
* @param options Optional transport options including `AbortSignal`.
|
|
376
436
|
* @returns A receipt indicating success or failure.
|
|
437
|
+
* @throws {Error} If the caller aborts the operation.
|
|
377
438
|
*/
|
|
378
439
|
async send(message, options) {
|
|
379
440
|
try {
|
|
380
441
|
options?.signal?.throwIfAborted();
|
|
381
|
-
const emailData = await convertMessage(message, this.config);
|
|
442
|
+
const emailData = await convertMessage(message, this.config, options?.signal);
|
|
382
443
|
options?.signal?.throwIfAborted();
|
|
383
444
|
const response = await this.httpClient.sendMessage(emailData, options?.signal);
|
|
384
445
|
return responseToReceipt(response);
|
package/dist/index.d.cts
CHANGED
|
@@ -32,6 +32,8 @@ interface MailerooConfig {
|
|
|
32
32
|
/**
|
|
33
33
|
* HTTP request timeout in milliseconds.
|
|
34
34
|
*
|
|
35
|
+
* Set to `0` to disable request timeouts.
|
|
36
|
+
*
|
|
35
37
|
* @default 30000
|
|
36
38
|
*/
|
|
37
39
|
readonly timeout?: number;
|
|
@@ -102,7 +104,7 @@ declare class MailerooTransport implements Transport<"maileroo"> {
|
|
|
102
104
|
/**
|
|
103
105
|
* The resolved Maileroo configuration used by this transport.
|
|
104
106
|
*/
|
|
105
|
-
config: ResolvedMailerooConfig;
|
|
107
|
+
readonly config: ResolvedMailerooConfig;
|
|
106
108
|
private httpClient;
|
|
107
109
|
/**
|
|
108
110
|
* Creates a new Maileroo transport instance.
|
|
@@ -116,6 +118,7 @@ declare class MailerooTransport implements Transport<"maileroo"> {
|
|
|
116
118
|
* @param message The email message to send.
|
|
117
119
|
* @param options Optional transport options including `AbortSignal`.
|
|
118
120
|
* @returns A receipt indicating success or failure.
|
|
121
|
+
* @throws {Error} If the caller aborts the operation.
|
|
119
122
|
*/
|
|
120
123
|
send(message: Message, options?: TransportOptions): Promise<Receipt<"maileroo">>;
|
|
121
124
|
/**
|
|
@@ -146,7 +149,7 @@ interface MailerooEmailAddress {
|
|
|
146
149
|
* @since 0.6.0
|
|
147
150
|
*/
|
|
148
151
|
interface MailerooAttachment {
|
|
149
|
-
/** Attachment filename. */
|
|
152
|
+
/** Attachment filename or inline content ID. */
|
|
150
153
|
readonly file_name: string;
|
|
151
154
|
/** Attachment MIME type. */
|
|
152
155
|
readonly content_type?: string;
|
|
@@ -179,7 +182,9 @@ interface MailerooEmail {
|
|
|
179
182
|
*
|
|
180
183
|
* @param message The Upyo message to convert.
|
|
181
184
|
* @param config The resolved Maileroo configuration.
|
|
185
|
+
* @param signal Optional abort signal for cancellation.
|
|
182
186
|
* @returns JSON object ready for Maileroo API submission.
|
|
187
|
+
* @throws {Error} If the caller aborts the operation.
|
|
183
188
|
* @since 0.6.0
|
|
184
189
|
*/
|
|
185
190
|
//#endregion
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ interface MailerooConfig {
|
|
|
32
32
|
/**
|
|
33
33
|
* HTTP request timeout in milliseconds.
|
|
34
34
|
*
|
|
35
|
+
* Set to `0` to disable request timeouts.
|
|
36
|
+
*
|
|
35
37
|
* @default 30000
|
|
36
38
|
*/
|
|
37
39
|
readonly timeout?: number;
|
|
@@ -102,7 +104,7 @@ declare class MailerooTransport implements Transport<"maileroo"> {
|
|
|
102
104
|
/**
|
|
103
105
|
* The resolved Maileroo configuration used by this transport.
|
|
104
106
|
*/
|
|
105
|
-
config: ResolvedMailerooConfig;
|
|
107
|
+
readonly config: ResolvedMailerooConfig;
|
|
106
108
|
private httpClient;
|
|
107
109
|
/**
|
|
108
110
|
* Creates a new Maileroo transport instance.
|
|
@@ -116,6 +118,7 @@ declare class MailerooTransport implements Transport<"maileroo"> {
|
|
|
116
118
|
* @param message The email message to send.
|
|
117
119
|
* @param options Optional transport options including `AbortSignal`.
|
|
118
120
|
* @returns A receipt indicating success or failure.
|
|
121
|
+
* @throws {Error} If the caller aborts the operation.
|
|
119
122
|
*/
|
|
120
123
|
send(message: Message, options?: TransportOptions): Promise<Receipt<"maileroo">>;
|
|
121
124
|
/**
|
|
@@ -146,7 +149,7 @@ interface MailerooEmailAddress {
|
|
|
146
149
|
* @since 0.6.0
|
|
147
150
|
*/
|
|
148
151
|
interface MailerooAttachment {
|
|
149
|
-
/** Attachment filename. */
|
|
152
|
+
/** Attachment filename or inline content ID. */
|
|
150
153
|
readonly file_name: string;
|
|
151
154
|
/** Attachment MIME type. */
|
|
152
155
|
readonly content_type?: string;
|
|
@@ -179,7 +182,9 @@ interface MailerooEmail {
|
|
|
179
182
|
*
|
|
180
183
|
* @param message The Upyo message to convert.
|
|
181
184
|
* @param config The resolved Maileroo configuration.
|
|
185
|
+
* @param signal Optional abort signal for cancellation.
|
|
182
186
|
* @returns JSON object ready for Maileroo API submission.
|
|
187
|
+
* @throws {Error} If the caller aborts the operation.
|
|
183
188
|
* @since 0.6.0
|
|
184
189
|
*/
|
|
185
190
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ function createMailerooConfig(config) {
|
|
|
14
14
|
baseUrl: normalizeBaseUrl(config.baseUrl ?? "https://smtp.maileroo.com/api/v2"),
|
|
15
15
|
timeout: config.timeout ?? 3e4,
|
|
16
16
|
retries: config.retries ?? 3,
|
|
17
|
-
headers: config.headers
|
|
17
|
+
headers: config.headers == null ? {} : { ...config.headers },
|
|
18
18
|
tracking: config.tracking,
|
|
19
19
|
tags: config.tags == null ? void 0 : { ...config.tags }
|
|
20
20
|
};
|
|
@@ -25,6 +25,7 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
25
25
|
|
|
26
26
|
//#endregion
|
|
27
27
|
//#region src/http-client.ts
|
|
28
|
+
const maxErrorMessageLength = 500;
|
|
28
29
|
/**
|
|
29
30
|
* Maileroo API error class for API-specific failures.
|
|
30
31
|
*
|
|
@@ -102,6 +103,8 @@ var MailerooHttpClient = class {
|
|
|
102
103
|
* @param messageData The JSON data to send to Maileroo.
|
|
103
104
|
* @param signal Optional AbortSignal for cancellation.
|
|
104
105
|
* @returns Promise that resolves to the Maileroo response.
|
|
106
|
+
* @throws {MailerooApiError} If Maileroo returns an API error.
|
|
107
|
+
* @throws {MailerooTimeoutError} If the request timeout elapses.
|
|
105
108
|
*/
|
|
106
109
|
sendMessage(messageData, signal) {
|
|
107
110
|
const url = `${this.config.baseUrl}/emails`;
|
|
@@ -112,20 +115,15 @@ var MailerooHttpClient = class {
|
|
|
112
115
|
for (let attempt = 0; attempt <= this.config.retries; attempt++) {
|
|
113
116
|
signal?.throwIfAborted();
|
|
114
117
|
try {
|
|
115
|
-
const response = await this.fetchWithAuth(url, body, signal);
|
|
116
|
-
const text = await response.text();
|
|
118
|
+
const { response, text } = await this.fetchWithAuth(url, body, signal);
|
|
117
119
|
if (!response.ok) throw new MailerooApiError(parseErrorMessage(text, response.status), response.status, parseRetryAfter(response.headers.get("Retry-After")), attempt + 1);
|
|
118
|
-
|
|
119
|
-
return JSON.parse(text);
|
|
120
|
-
} catch (error) {
|
|
121
|
-
throw new SyntaxError(`Invalid JSON response from Maileroo API: ${error instanceof Error ? error.message : String(error)}.`);
|
|
122
|
-
}
|
|
120
|
+
return parseResponse(text);
|
|
123
121
|
} catch (error) {
|
|
124
122
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
123
|
+
if (signal?.aborted) throw error;
|
|
125
124
|
if (error instanceof MailerooApiError && !isRetryable(error)) throw error;
|
|
126
|
-
if (error instanceof Error && error.name === "AbortError" && signal?.aborted) throw error;
|
|
127
125
|
if (attempt === this.config.retries) throw withAttempts(lastError, attempt + 1);
|
|
128
|
-
await sleep(calculateRetryDelay(attempt), signal);
|
|
126
|
+
await sleep(calculateRetryDelay(attempt, lastError), signal);
|
|
129
127
|
}
|
|
130
128
|
}
|
|
131
129
|
throw lastError ?? /* @__PURE__ */ new Error("Request failed after all retry attempts.");
|
|
@@ -140,12 +138,17 @@ var MailerooHttpClient = class {
|
|
|
140
138
|
const timeoutId = this.config.timeout > 0 ? setTimeout(() => timeoutController.abort(), this.config.timeout) : void 0;
|
|
141
139
|
const requestSignal = combineSignals(timeoutController.signal, signal);
|
|
142
140
|
try {
|
|
143
|
-
|
|
141
|
+
const response = await globalThis.fetch(url, {
|
|
144
142
|
method: "POST",
|
|
145
143
|
headers,
|
|
146
144
|
body: JSON.stringify(body),
|
|
147
145
|
signal: requestSignal.signal
|
|
148
146
|
});
|
|
147
|
+
const text = await response.text();
|
|
148
|
+
return {
|
|
149
|
+
response,
|
|
150
|
+
text
|
|
151
|
+
};
|
|
149
152
|
} catch (error) {
|
|
150
153
|
if (error instanceof Error && error.name === "AbortError" && timeoutController.signal.aborted && !signal?.aborted) throw new MailerooTimeoutError(this.config.timeout);
|
|
151
154
|
throw error;
|
|
@@ -163,39 +166,60 @@ function withAttempts(error, attempts) {
|
|
|
163
166
|
function isRetryable(error) {
|
|
164
167
|
return error.statusCode === 408 || error.statusCode === 429 || error.statusCode >= 500;
|
|
165
168
|
}
|
|
166
|
-
function calculateRetryDelay(attempt) {
|
|
169
|
+
function calculateRetryDelay(attempt, error) {
|
|
167
170
|
const baseDelay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
|
|
168
|
-
|
|
171
|
+
const backoffDelay = Math.round(baseDelay / 2 + Math.random() * (baseDelay / 2));
|
|
172
|
+
if (error instanceof MailerooApiError) return Math.max(backoffDelay, error.retryAfterMilliseconds ?? 0);
|
|
173
|
+
return backoffDelay;
|
|
169
174
|
}
|
|
170
175
|
function parseErrorMessage(text, statusCode) {
|
|
171
176
|
try {
|
|
172
177
|
const errorBody = JSON.parse(text);
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
if (errorBody != null && typeof errorBody === "object") {
|
|
179
|
+
const mailerooError = errorBody;
|
|
180
|
+
if (typeof mailerooError.message === "string" && mailerooError.message !== "") return truncateErrorMessage(mailerooError.message);
|
|
181
|
+
if (typeof mailerooError.error === "string" && mailerooError.error !== "") return truncateErrorMessage(mailerooError.error);
|
|
182
|
+
if (Array.isArray(mailerooError.errors) && mailerooError.errors.length > 0) return truncateErrorMessage(JSON.stringify(mailerooError.errors));
|
|
183
|
+
}
|
|
176
184
|
} catch {}
|
|
177
|
-
return text || `HTTP ${statusCode}`;
|
|
185
|
+
return truncateErrorMessage(text) || `HTTP ${statusCode}`;
|
|
186
|
+
}
|
|
187
|
+
function parseResponse(text) {
|
|
188
|
+
try {
|
|
189
|
+
const response = JSON.parse(text);
|
|
190
|
+
if (response == null || typeof response !== "object") throw new SyntaxError("response is not an object");
|
|
191
|
+
return response;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw new SyntaxError(`Invalid JSON response from Maileroo API: ${error instanceof Error ? error.message : String(error)}.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function truncateErrorMessage(message) {
|
|
197
|
+
return message.length > maxErrorMessageLength ? `${message.slice(0, maxErrorMessageLength)}...` : message;
|
|
198
|
+
}
|
|
199
|
+
function abortReason$1(signal) {
|
|
200
|
+
return signal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
178
201
|
}
|
|
179
202
|
function sleep(ms, signal) {
|
|
180
203
|
return new Promise((resolve, reject) => {
|
|
181
204
|
if (signal?.aborted) {
|
|
182
|
-
reject(
|
|
205
|
+
reject(abortReason$1(signal));
|
|
183
206
|
return;
|
|
184
207
|
}
|
|
208
|
+
const timeoutState = {};
|
|
185
209
|
const onAbort = () => {
|
|
186
|
-
clearTimeout(
|
|
210
|
+
if (timeoutState.id !== void 0) clearTimeout(timeoutState.id);
|
|
187
211
|
signal?.removeEventListener("abort", onAbort);
|
|
188
|
-
reject(
|
|
212
|
+
reject(abortReason$1(signal));
|
|
189
213
|
};
|
|
190
|
-
|
|
191
|
-
signal?.removeEventListener("abort", onAbort);
|
|
192
|
-
resolve();
|
|
193
|
-
}, ms);
|
|
214
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
194
215
|
if (signal?.aborted) {
|
|
195
216
|
onAbort();
|
|
196
217
|
return;
|
|
197
218
|
}
|
|
198
|
-
|
|
219
|
+
timeoutState.id = setTimeout(() => {
|
|
220
|
+
signal?.removeEventListener("abort", onAbort);
|
|
221
|
+
resolve();
|
|
222
|
+
}, ms);
|
|
199
223
|
});
|
|
200
224
|
}
|
|
201
225
|
|
|
@@ -220,10 +244,13 @@ const STANDARD_HEADERS = new Set([
|
|
|
220
244
|
*
|
|
221
245
|
* @param message The Upyo message to convert.
|
|
222
246
|
* @param config The resolved Maileroo configuration.
|
|
247
|
+
* @param signal Optional abort signal for cancellation.
|
|
223
248
|
* @returns JSON object ready for Maileroo API submission.
|
|
249
|
+
* @throws {Error} If the caller aborts the operation.
|
|
224
250
|
* @since 0.6.0
|
|
225
251
|
*/
|
|
226
|
-
async function convertMessage(message, config) {
|
|
252
|
+
async function convertMessage(message, config, signal) {
|
|
253
|
+
signal?.throwIfAborted();
|
|
227
254
|
const emailData = {
|
|
228
255
|
from: convertAddress(message.sender),
|
|
229
256
|
to: convertAddressList(message.recipients),
|
|
@@ -244,7 +271,8 @@ async function convertMessage(message, config) {
|
|
|
244
271
|
if (Object.keys(tags).length > 0) emailData.tags = tags;
|
|
245
272
|
const headers = convertHeaders(message);
|
|
246
273
|
if (Object.keys(headers).length > 0) emailData.headers = headers;
|
|
247
|
-
if (message.attachments.length > 0) emailData.attachments = await Promise.all(message.attachments.map(convertAttachment));
|
|
274
|
+
if (message.attachments.length > 0) emailData.attachments = await Promise.all(message.attachments.map((attachment) => convertAttachment(attachment, signal)));
|
|
275
|
+
signal?.throwIfAborted();
|
|
248
276
|
return emailData;
|
|
249
277
|
}
|
|
250
278
|
function convertAddress(address) {
|
|
@@ -278,19 +306,47 @@ function convertHeaders(message) {
|
|
|
278
306
|
for (const [key, value] of message.headers.entries()) if (!isStandardHeader(key)) headers[key] = value;
|
|
279
307
|
return headers;
|
|
280
308
|
}
|
|
281
|
-
async function convertAttachment(attachment) {
|
|
282
|
-
|
|
309
|
+
async function convertAttachment(attachment, signal) {
|
|
310
|
+
signal?.throwIfAborted();
|
|
311
|
+
const content = await waitForAttachmentContent(attachment.content, signal);
|
|
312
|
+
signal?.throwIfAborted();
|
|
283
313
|
return {
|
|
284
|
-
file_name: attachment
|
|
314
|
+
file_name: getAttachmentFileName(attachment),
|
|
285
315
|
content_type: attachment.contentType,
|
|
286
316
|
content: uint8ArrayToBase64(content),
|
|
287
317
|
inline: attachment.inline || void 0
|
|
288
318
|
};
|
|
289
319
|
}
|
|
320
|
+
function waitForAttachmentContent(content, signal) {
|
|
321
|
+
if (signal == null) return Promise.resolve(content);
|
|
322
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
323
|
+
return new Promise((resolve, reject) => {
|
|
324
|
+
const onAbort = () => {
|
|
325
|
+
signal.removeEventListener("abort", onAbort);
|
|
326
|
+
reject(abortReason(signal));
|
|
327
|
+
};
|
|
328
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
329
|
+
Promise.resolve(content).then((value) => {
|
|
330
|
+
signal.removeEventListener("abort", onAbort);
|
|
331
|
+
resolve(value);
|
|
332
|
+
}, (error) => {
|
|
333
|
+
signal.removeEventListener("abort", onAbort);
|
|
334
|
+
reject(error);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function abortReason(signal) {
|
|
339
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
340
|
+
}
|
|
341
|
+
function getAttachmentFileName(attachment) {
|
|
342
|
+
return attachment.inline && typeof attachment.contentId === "string" && attachment.contentId !== "" ? attachment.contentId : attachment.filename;
|
|
343
|
+
}
|
|
290
344
|
function uint8ArrayToBase64(bytes) {
|
|
291
345
|
const nativeToBase64 = getNativeToBase64(bytes);
|
|
292
346
|
if (nativeToBase64 != null) return nativeToBase64();
|
|
293
|
-
const
|
|
347
|
+
const bufferConverter = getBufferBase64Converter();
|
|
348
|
+
if (bufferConverter != null) return bufferConverter.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
|
|
349
|
+
const chunkSize = 4096;
|
|
294
350
|
const chunks = [];
|
|
295
351
|
for (let offset = 0; offset < bytes.length; offset += chunkSize) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)));
|
|
296
352
|
return btoa(chunks.join(""));
|
|
@@ -301,6 +357,10 @@ function getNativeToBase64(bytes) {
|
|
|
301
357
|
if (typeof toBase64 !== "function") return void 0;
|
|
302
358
|
return () => toBase64.call(bytes);
|
|
303
359
|
}
|
|
360
|
+
function getBufferBase64Converter() {
|
|
361
|
+
const candidate = globalThis.Buffer;
|
|
362
|
+
return typeof candidate?.from === "function" ? candidate : void 0;
|
|
363
|
+
}
|
|
304
364
|
function isStandardHeader(headerName) {
|
|
305
365
|
return STANDARD_HEADERS.has(headerName.toLowerCase());
|
|
306
366
|
}
|
|
@@ -351,11 +411,12 @@ var MailerooTransport = class {
|
|
|
351
411
|
* @param message The email message to send.
|
|
352
412
|
* @param options Optional transport options including `AbortSignal`.
|
|
353
413
|
* @returns A receipt indicating success or failure.
|
|
414
|
+
* @throws {Error} If the caller aborts the operation.
|
|
354
415
|
*/
|
|
355
416
|
async send(message, options) {
|
|
356
417
|
try {
|
|
357
418
|
options?.signal?.throwIfAborted();
|
|
358
|
-
const emailData = await convertMessage(message, this.config);
|
|
419
|
+
const emailData = await convertMessage(message, this.config, options?.signal);
|
|
359
420
|
options?.signal?.throwIfAborted();
|
|
360
421
|
const response = await this.httpClient.sendMessage(emailData, options?.signal);
|
|
361
422
|
return responseToReceipt(response);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upyo/maileroo",
|
|
3
|
-
"version": "0.6.0-dev.
|
|
3
|
+
"version": "0.6.0-dev.225",
|
|
4
4
|
"description": "Maileroo transport for Upyo email library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"sideEffects": false,
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@upyo/core": "0.6.0"
|
|
56
|
+
"@upyo/core": "0.6.0-dev.225+1a4bf2e0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"tsdown": "^0.12.7",
|