@upyo/maileroo 0.6.0-dev.0 → 0.6.0-dev.223

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(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(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(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,26 @@ 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) {
332
+ async function convertAttachment(attachment, signal) {
333
+ signal?.throwIfAborted();
305
334
  const content = await attachment.content;
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 getAttachmentFileName(attachment) {
344
+ return attachment.inline && typeof attachment.contentId === "string" && attachment.contentId !== "" ? attachment.contentId : attachment.filename;
345
+ }
313
346
  function uint8ArrayToBase64(bytes) {
314
347
  const nativeToBase64 = getNativeToBase64(bytes);
315
348
  if (nativeToBase64 != null) return nativeToBase64();
316
- const chunkSize = 32768;
349
+ const bufferConverter = getBufferBase64Converter();
350
+ if (bufferConverter != null) return bufferConverter.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
351
+ const chunkSize = 4096;
317
352
  const chunks = [];
318
353
  for (let offset = 0; offset < bytes.length; offset += chunkSize) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)));
319
354
  return btoa(chunks.join(""));
@@ -324,6 +359,10 @@ function getNativeToBase64(bytes) {
324
359
  if (typeof toBase64 !== "function") return void 0;
325
360
  return () => toBase64.call(bytes);
326
361
  }
362
+ function getBufferBase64Converter() {
363
+ const candidate = globalThis.Buffer;
364
+ return typeof candidate?.from === "function" ? candidate : void 0;
365
+ }
327
366
  function isStandardHeader(headerName) {
328
367
  return STANDARD_HEADERS.has(headerName.toLowerCase());
329
368
  }
@@ -374,11 +413,12 @@ var MailerooTransport = class {
374
413
  * @param message The email message to send.
375
414
  * @param options Optional transport options including `AbortSignal`.
376
415
  * @returns A receipt indicating success or failure.
416
+ * @throws {Error} If the caller aborts the operation.
377
417
  */
378
418
  async send(message, options) {
379
419
  try {
380
420
  options?.signal?.throwIfAborted();
381
- const emailData = await convertMessage(message, this.config);
421
+ const emailData = await convertMessage(message, this.config, options?.signal);
382
422
  options?.signal?.throwIfAborted();
383
423
  const response = await this.httpClient.sendMessage(emailData, options?.signal);
384
424
  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(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(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(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,26 @@ 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) {
309
+ async function convertAttachment(attachment, signal) {
310
+ signal?.throwIfAborted();
282
311
  const content = await attachment.content;
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 getAttachmentFileName(attachment) {
321
+ return attachment.inline && typeof attachment.contentId === "string" && attachment.contentId !== "" ? attachment.contentId : attachment.filename;
322
+ }
290
323
  function uint8ArrayToBase64(bytes) {
291
324
  const nativeToBase64 = getNativeToBase64(bytes);
292
325
  if (nativeToBase64 != null) return nativeToBase64();
293
- const chunkSize = 32768;
326
+ const bufferConverter = getBufferBase64Converter();
327
+ if (bufferConverter != null) return bufferConverter.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
328
+ const chunkSize = 4096;
294
329
  const chunks = [];
295
330
  for (let offset = 0; offset < bytes.length; offset += chunkSize) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)));
296
331
  return btoa(chunks.join(""));
@@ -301,6 +336,10 @@ function getNativeToBase64(bytes) {
301
336
  if (typeof toBase64 !== "function") return void 0;
302
337
  return () => toBase64.call(bytes);
303
338
  }
339
+ function getBufferBase64Converter() {
340
+ const candidate = globalThis.Buffer;
341
+ return typeof candidate?.from === "function" ? candidate : void 0;
342
+ }
304
343
  function isStandardHeader(headerName) {
305
344
  return STANDARD_HEADERS.has(headerName.toLowerCase());
306
345
  }
@@ -351,11 +390,12 @@ var MailerooTransport = class {
351
390
  * @param message The email message to send.
352
391
  * @param options Optional transport options including `AbortSignal`.
353
392
  * @returns A receipt indicating success or failure.
393
+ * @throws {Error} If the caller aborts the operation.
354
394
  */
355
395
  async send(message, options) {
356
396
  try {
357
397
  options?.signal?.throwIfAborted();
358
- const emailData = await convertMessage(message, this.config);
398
+ const emailData = await convertMessage(message, this.config, options?.signal);
359
399
  options?.signal?.throwIfAborted();
360
400
  const response = await this.httpClient.sendMessage(emailData, options?.signal);
361
401
  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.223",
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.223+8fde224e"
57
57
  },
58
58
  "devDependencies": {
59
59
  "tsdown": "^0.12.7",