@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 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
- try {
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
- return await globalThis.fetch(url, {
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
- return Math.round(baseDelay / 2 + Math.random() * (baseDelay / 2));
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 (typeof errorBody.message === "string" && errorBody.message !== "") return errorBody.message;
197
- if (typeof errorBody.error === "string" && errorBody.error !== "") return errorBody.error;
198
- if (Array.isArray(errorBody.errors) && errorBody.errors.length > 0) return JSON.stringify(errorBody.errors);
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(new DOMException("The operation was aborted.", "AbortError"));
228
+ reject(abortReason$1(signal));
206
229
  return;
207
230
  }
231
+ const timeoutState = {};
208
232
  const onAbort = () => {
209
- clearTimeout(timeoutId);
233
+ if (timeoutState.id !== void 0) clearTimeout(timeoutState.id);
210
234
  signal?.removeEventListener("abort", onAbort);
211
- reject(new DOMException("The operation was aborted.", "AbortError"));
235
+ reject(abortReason$1(signal));
212
236
  };
213
- const timeoutId = setTimeout(() => {
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
- signal?.addEventListener("abort", onAbort, { once: true });
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
- const content = await attachment.content;
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.filename,
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 chunkSize = 32768;
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
- try {
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
- return await globalThis.fetch(url, {
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
- return Math.round(baseDelay / 2 + Math.random() * (baseDelay / 2));
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 (typeof errorBody.message === "string" && errorBody.message !== "") return errorBody.message;
174
- if (typeof errorBody.error === "string" && errorBody.error !== "") return errorBody.error;
175
- if (Array.isArray(errorBody.errors) && errorBody.errors.length > 0) return JSON.stringify(errorBody.errors);
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(new DOMException("The operation was aborted.", "AbortError"));
205
+ reject(abortReason$1(signal));
183
206
  return;
184
207
  }
208
+ const timeoutState = {};
185
209
  const onAbort = () => {
186
- clearTimeout(timeoutId);
210
+ if (timeoutState.id !== void 0) clearTimeout(timeoutState.id);
187
211
  signal?.removeEventListener("abort", onAbort);
188
- reject(new DOMException("The operation was aborted.", "AbortError"));
212
+ reject(abortReason$1(signal));
189
213
  };
190
- const timeoutId = setTimeout(() => {
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
- signal?.addEventListener("abort", onAbort, { once: true });
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
- const content = await attachment.content;
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.filename,
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 chunkSize = 32768;
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.0",
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",