@upyo/jmap 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/errors.ts
3
27
  /**
@@ -8,12 +32,16 @@ var JmapApiError = class extends Error {
8
32
  statusCode;
9
33
  responseBody;
10
34
  jmapErrorType;
11
- constructor(message, statusCode, responseBody, jmapErrorType) {
35
+ retryAfterMilliseconds;
36
+ attempts;
37
+ constructor(message, statusCode, responseBody, jmapErrorType, retryAfterMilliseconds, attempts) {
12
38
  super(message);
13
39
  this.name = "JmapApiError";
14
40
  this.statusCode = statusCode;
15
41
  this.responseBody = responseBody;
16
42
  this.jmapErrorType = jmapErrorType;
43
+ this.retryAfterMilliseconds = retryAfterMilliseconds;
44
+ this.attempts = attempts;
17
45
  }
18
46
  };
19
47
  /**
@@ -64,13 +92,13 @@ async function uploadBlob(config, uploadUrl, accountId, blob, signal) {
64
92
  for (const [key, value] of Object.entries(config.headers)) headers[key] = value;
65
93
  const controller = new AbortController();
66
94
  const timeoutId = setTimeout(() => controller.abort(), config.timeout);
67
- const combinedSignal = signal ? combineSignals(signal, controller.signal) : controller.signal;
95
+ const combinedSignal = (0, __upyo_core.combineSignals)(controller.signal, signal);
68
96
  try {
69
97
  const response = await fetch(url, {
70
98
  method: "POST",
71
99
  headers,
72
100
  body: blob,
73
- signal: combinedSignal
101
+ signal: combinedSignal.signal
74
102
  });
75
103
  if (!response.ok) {
76
104
  const body = await response.text();
@@ -79,23 +107,10 @@ async function uploadBlob(config, uploadUrl, accountId, blob, signal) {
79
107
  const result = await response.json();
80
108
  return result;
81
109
  } finally {
110
+ combinedSignal.cleanup();
82
111
  clearTimeout(timeoutId);
83
112
  }
84
113
  }
85
- /**
86
- * Combine multiple abort signals into one.
87
- */
88
- function combineSignals(...signals) {
89
- const controller = new AbortController();
90
- for (const signal of signals) {
91
- if (signal.aborted) {
92
- controller.abort(signal.reason);
93
- break;
94
- }
95
- signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
96
- }
97
- return controller.signal;
98
- }
99
114
 
100
115
  //#endregion
101
116
  //#region src/config.ts
@@ -144,7 +159,7 @@ var JmapHttpClient = class {
144
159
  const response = await this.fetchWithAuth(this.config.sessionUrl, { method: "GET" }, signal);
145
160
  if (!response.ok) {
146
161
  const text = await response.text();
147
- throw new JmapApiError(`Session fetch failed: ${response.status}`, response.status, text);
162
+ throw new JmapApiError(`Session fetch failed: ${response.status}`, response.status, text, void 0, (0, __upyo_core.parseRetryAfter)(response.headers.get("Retry-After")), 1);
148
163
  }
149
164
  return await response.json();
150
165
  }
@@ -169,23 +184,28 @@ var JmapHttpClient = class {
169
184
  }, signal);
170
185
  if (!response.ok) {
171
186
  const text = await response.text();
172
- const error = new JmapApiError(`JMAP request failed: ${response.status}`, response.status, text);
187
+ const error = new JmapApiError(`JMAP request failed: ${response.status}`, response.status, text, void 0, (0, __upyo_core.parseRetryAfter)(response.headers.get("Retry-After")), attempt + 1);
173
188
  if (response.status >= 400 && response.status < 500) throw error;
174
189
  throw error;
175
190
  }
176
191
  return await response.json();
177
192
  } catch (error) {
178
- if (error instanceof Error && error.name === "AbortError") throw error;
193
+ if (isCallerAbort(error, signal)) throw error;
179
194
  if (error instanceof JmapApiError && error.statusCode !== void 0) {
180
195
  if (error.statusCode >= 400 && error.statusCode < 500) throw error;
181
196
  }
182
197
  lastError = error instanceof Error ? error : new Error(String(error));
183
- if (attempt === this.config.retries) throw error;
198
+ if (attempt === this.config.retries) {
199
+ if (error instanceof JmapApiError) throw error;
200
+ if (isAbortError$1(error)) throw new JmapApiError("JMAP request timed out.", void 0, void 0, void 0, void 0, attempt + 1);
201
+ throw new JmapApiError(lastError.message, void 0, void 0, void 0, void 0, attempt + 1);
202
+ }
184
203
  const delay = Math.pow(2, attempt) * 1e3;
185
204
  await new Promise((resolve) => setTimeout(resolve, delay));
186
205
  }
187
206
  }
188
- throw lastError || /* @__PURE__ */ new Error("Request failed after all retries");
207
+ if (lastError != null) throw lastError;
208
+ throw new Error("Request failed after all retries");
189
209
  }
190
210
  /**
191
211
  * Makes an authenticated fetch request.
@@ -231,6 +251,12 @@ var JmapHttpClient = class {
231
251
  }
232
252
  }
233
253
  };
254
+ function isCallerAbort(error, signal) {
255
+ return signal?.aborted === true && (isAbortError$1(error) || error === signal.reason);
256
+ }
257
+ function isAbortError$1(error) {
258
+ return error instanceof Error && error.name === "AbortError";
259
+ }
234
260
 
235
261
  //#endregion
236
262
  //#region src/message-converter.ts
@@ -443,6 +469,7 @@ const JMAP_CAPABILITIES = {
443
469
  * @since 0.4.0
444
470
  */
445
471
  var JmapTransport = class {
472
+ id = "jmap";
446
473
  config;
447
474
  httpClient;
448
475
  cachedSession = null;
@@ -460,19 +487,22 @@ var JmapTransport = class {
460
487
  * @param message The message to send.
461
488
  * @param options Optional transport options.
462
489
  * @returns A receipt indicating success or failure.
490
+ * @throws The abort reason or an `AbortError` when the caller aborts the
491
+ * operation.
463
492
  * @since 0.4.0
464
493
  */
465
494
  async send(message, options) {
466
495
  const signal = options?.signal;
496
+ signal?.throwIfAborted();
467
497
  try {
468
- signal?.throwIfAborted();
469
498
  const session = await this.getSession(signal);
470
499
  signal?.throwIfAborted();
471
500
  const accountId = this.config.accountId ?? findMailAccount(session);
472
- if (!accountId) return {
473
- successful: false,
474
- errorMessages: ["No mail-capable account found in JMAP session"]
475
- };
501
+ if (!accountId) return createJmapFailure("No mail-capable account found in JMAP session", void 0, {
502
+ category: "configuration",
503
+ code: "jmap.no_mail_account",
504
+ retryable: false
505
+ });
476
506
  const draftsMailboxId = await this.getDraftsMailboxId(session, accountId, signal);
477
507
  signal?.throwIfAborted();
478
508
  const identityId = await this.getIdentityId(session, accountId, message.sender.address, signal);
@@ -507,18 +537,14 @@ var JmapTransport = class {
507
537
  }, signal);
508
538
  return this.parseResponse(response);
509
539
  } catch (error) {
510
- if (error instanceof Error && error.name === "AbortError") return {
511
- successful: false,
512
- errorMessages: [`Request aborted: ${error.message}`]
513
- };
514
- if (error instanceof JmapApiError) return {
515
- successful: false,
516
- errorMessages: [error.message]
517
- };
518
- return {
519
- successful: false,
520
- errorMessages: [error instanceof Error ? error.message : String(error)]
521
- };
540
+ if (signal?.aborted) throw getAbortReason(signal, error);
541
+ if (error instanceof Error && error.name === "AbortError") return createJmapFailure(`Request aborted: ${error.message}`, error, {
542
+ category: "timeout",
543
+ code: "abort",
544
+ retryable: true
545
+ });
546
+ if (error instanceof JmapApiError) return createJmapFailure(error.message, error);
547
+ return createJmapFailure(error instanceof Error ? error.message : String(error), error);
522
548
  }
523
549
  }
524
550
  /**
@@ -526,6 +552,8 @@ var JmapTransport = class {
526
552
  * @param messages The messages to send.
527
553
  * @param options Optional transport options.
528
554
  * @yields Receipts for each message.
555
+ * @throws The abort reason or an `AbortError` when the caller aborts the
556
+ * operation.
529
557
  * @since 0.4.0
530
558
  */
531
559
  async *sendMany(messages, options) {
@@ -543,10 +571,11 @@ var JmapTransport = class {
543
571
  processingStage = "account discovery";
544
572
  const accountId = this.config.accountId ?? findMailAccount(session);
545
573
  if (!accountId) {
546
- for (let i = 0; i < messageArray.length; i++) yield {
547
- successful: false,
548
- errorMessages: ["No mail-capable account found in JMAP session"]
549
- };
574
+ for (let i = 0; i < messageArray.length; i++) yield createJmapFailure("No mail-capable account found in JMAP session", void 0, {
575
+ category: "configuration",
576
+ code: "jmap.no_mail_account",
577
+ retryable: false
578
+ });
550
579
  return;
551
580
  }
552
581
  processingStage = "mailbox discovery";
@@ -604,13 +633,16 @@ var JmapTransport = class {
604
633
  }, signal);
605
634
  for (let i = 0; i < messageArray.length; i++) yield this.parseBatchResponseForIndex(response, i);
606
635
  } catch (error) {
636
+ if (signal?.aborted) throw getAbortReason(signal, error);
637
+ const timeoutOverride = error instanceof Error && error.name === "AbortError" ? {
638
+ category: "timeout",
639
+ code: "abort",
640
+ retryable: true
641
+ } : void 0;
607
642
  const baseMessage = error instanceof Error && error.name === "AbortError" ? `Request aborted: ${error.message}` : error instanceof JmapApiError ? error.message : error instanceof Error ? error.message : String(error);
608
643
  let detailedMessage = `Failed during ${processingStage}: ${baseMessage}`;
609
644
  if (processingStage === "attachment upload" && attachmentsUploadedCount > 0) detailedMessage += ` (${attachmentsUploadedCount}/${messageArray.length} messages had attachments uploaded before failure)`;
610
- for (let i = 0; i < messageArray.length; i++) yield {
611
- successful: false,
612
- errorMessages: [detailedMessage]
613
- };
645
+ for (let i = 0; i < messageArray.length; i++) yield createJmapFailure(detailedMessage, error, timeoutOverride);
614
646
  }
615
647
  }
616
648
  /**
@@ -768,14 +800,17 @@ var JmapTransport = class {
768
800
  if (submissionResult.notCreated) for (const [key, error] of Object.entries(submissionResult.notCreated)) errors.push(`Email submission failed (${key}): ${error.type}${error.description ? ` - ${error.description}` : ""}`);
769
801
  if (submissionResult.created?.submission) return {
770
802
  successful: true,
771
- messageId: submissionResult.created.submission.id
803
+ messageId: submissionResult.created.submission.id,
804
+ provider: "jmap"
772
805
  };
773
806
  }
774
807
  if (errors.length === 0) errors.push("Unknown error: No submission result received");
775
- return {
776
- successful: false,
777
- errorMessages: errors
778
- };
808
+ return (0, __upyo_core.createFailedReceipt)(errors, {
809
+ provider: "jmap",
810
+ category: "rejected",
811
+ code: "jmap.submission_failed",
812
+ retryable: false
813
+ });
779
814
  }
780
815
  /**
781
816
  * Parses the JMAP batch response to extract receipt for a specific index.
@@ -805,16 +840,53 @@ var JmapTransport = class {
805
840
  }
806
841
  if (submissionResult.created?.[subKey]) return {
807
842
  successful: true,
808
- messageId: submissionResult.created[subKey].id
843
+ messageId: submissionResult.created[subKey].id,
844
+ provider: "jmap"
809
845
  };
810
846
  }
811
847
  if (errors.length === 0) errors.push("Unknown error: No submission result received");
812
- return {
813
- successful: false,
814
- errorMessages: errors
815
- };
848
+ return (0, __upyo_core.createFailedReceipt)(errors, {
849
+ provider: "jmap",
850
+ category: "rejected",
851
+ code: "jmap.submission_failed",
852
+ retryable: false
853
+ });
816
854
  }
817
855
  };
856
+ function createJmapFailure(message, error, override = {}) {
857
+ const attempts = getAttemptCount(error);
858
+ if (error instanceof JmapApiError) return (0, __upyo_core.createFailedReceipt)(message, {
859
+ provider: "jmap",
860
+ statusCode: error.statusCode,
861
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
862
+ attempts,
863
+ providerDetails: {
864
+ responseBody: error.responseBody,
865
+ jmapErrorType: error.jmapErrorType
866
+ },
867
+ category: override.category,
868
+ code: override.code,
869
+ retryable: override.retryable
870
+ });
871
+ return (0, __upyo_core.createFailedReceipt)(message, {
872
+ provider: "jmap",
873
+ attempts,
874
+ category: override.category,
875
+ code: override.code,
876
+ retryable: override.retryable
877
+ });
878
+ }
879
+ function getAttemptCount(error) {
880
+ if (error instanceof JmapApiError) return error.attempts ?? 1;
881
+ if (typeof error === "object" && error !== null && "attempts" in error && typeof error.attempts === "number") return error.attempts;
882
+ return 1;
883
+ }
884
+ function getAbortReason(signal, fallback) {
885
+ return signal.reason ?? (isAbortError(fallback) ? fallback : void 0) ?? new DOMException("The operation was aborted.", "AbortError");
886
+ }
887
+ function isAbortError(error) {
888
+ return error instanceof Error && error.name === "AbortError";
889
+ }
818
890
 
819
891
  //#endregion
820
892
  exports.JMAP_ERROR_TYPES = JMAP_ERROR_TYPES;
package/dist/index.d.cts CHANGED
@@ -103,7 +103,8 @@ declare function createJmapConfig(config: JmapConfig): ResolvedJmapConfig;
103
103
  * JMAP transport for sending emails via JMAP protocol (RFC 8620/8621).
104
104
  * @since 0.4.0
105
105
  */
106
- declare class JmapTransport implements Transport {
106
+ declare class JmapTransport implements Transport<"jmap"> {
107
+ readonly id = "jmap";
107
108
  readonly config: ResolvedJmapConfig;
108
109
  private readonly httpClient;
109
110
  private cachedSession;
@@ -118,17 +119,21 @@ declare class JmapTransport implements Transport {
118
119
  * @param message The message to send.
119
120
  * @param options Optional transport options.
120
121
  * @returns A receipt indicating success or failure.
122
+ * @throws The abort reason or an `AbortError` when the caller aborts the
123
+ * operation.
121
124
  * @since 0.4.0
122
125
  */
123
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
126
+ send(message: Message, options?: TransportOptions): Promise<Receipt<"jmap">>;
124
127
  /**
125
128
  * Sends multiple messages in a single batched JMAP request.
126
129
  * @param messages The messages to send.
127
130
  * @param options Optional transport options.
128
131
  * @yields Receipts for each message.
132
+ * @throws The abort reason or an `AbortError` when the caller aborts the
133
+ * operation.
129
134
  * @since 0.4.0
130
135
  */
131
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
136
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"jmap">>;
132
137
  /**
133
138
  * Gets or refreshes the JMAP session.
134
139
  * @param signal Optional abort signal.
@@ -207,7 +212,9 @@ declare class JmapApiError extends Error {
207
212
  readonly statusCode?: number;
208
213
  readonly responseBody?: string;
209
214
  readonly jmapErrorType?: string;
210
- constructor(message: string, statusCode?: number, responseBody?: string, jmapErrorType?: string);
215
+ readonly retryAfterMilliseconds?: number;
216
+ readonly attempts?: number;
217
+ constructor(message: string, statusCode?: number, responseBody?: string, jmapErrorType?: string, retryAfterMilliseconds?: number, attempts?: number);
211
218
  }
212
219
  /**
213
220
  * JMAP-specific error types from RFC 8620.
package/dist/index.d.ts CHANGED
@@ -103,7 +103,8 @@ declare function createJmapConfig(config: JmapConfig): ResolvedJmapConfig;
103
103
  * JMAP transport for sending emails via JMAP protocol (RFC 8620/8621).
104
104
  * @since 0.4.0
105
105
  */
106
- declare class JmapTransport implements Transport {
106
+ declare class JmapTransport implements Transport<"jmap"> {
107
+ readonly id = "jmap";
107
108
  readonly config: ResolvedJmapConfig;
108
109
  private readonly httpClient;
109
110
  private cachedSession;
@@ -118,17 +119,21 @@ declare class JmapTransport implements Transport {
118
119
  * @param message The message to send.
119
120
  * @param options Optional transport options.
120
121
  * @returns A receipt indicating success or failure.
122
+ * @throws The abort reason or an `AbortError` when the caller aborts the
123
+ * operation.
121
124
  * @since 0.4.0
122
125
  */
123
- send(message: Message, options?: TransportOptions): Promise<Receipt>;
126
+ send(message: Message, options?: TransportOptions): Promise<Receipt<"jmap">>;
124
127
  /**
125
128
  * Sends multiple messages in a single batched JMAP request.
126
129
  * @param messages The messages to send.
127
130
  * @param options Optional transport options.
128
131
  * @yields Receipts for each message.
132
+ * @throws The abort reason or an `AbortError` when the caller aborts the
133
+ * operation.
129
134
  * @since 0.4.0
130
135
  */
131
- sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt>;
136
+ sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<"jmap">>;
132
137
  /**
133
138
  * Gets or refreshes the JMAP session.
134
139
  * @param signal Optional abort signal.
@@ -207,7 +212,9 @@ declare class JmapApiError extends Error {
207
212
  readonly statusCode?: number;
208
213
  readonly responseBody?: string;
209
214
  readonly jmapErrorType?: string;
210
- constructor(message: string, statusCode?: number, responseBody?: string, jmapErrorType?: string);
215
+ readonly retryAfterMilliseconds?: number;
216
+ readonly attempts?: number;
217
+ constructor(message: string, statusCode?: number, responseBody?: string, jmapErrorType?: string, retryAfterMilliseconds?: number, attempts?: number);
211
218
  }
212
219
  /**
213
220
  * JMAP-specific error types from RFC 8620.
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { combineSignals, createFailedReceipt, parseRetryAfter } from "@upyo/core";
2
+
1
3
  //#region src/errors.ts
2
4
  /**
3
5
  * Error class for JMAP API errors.
@@ -7,12 +9,16 @@ var JmapApiError = class extends Error {
7
9
  statusCode;
8
10
  responseBody;
9
11
  jmapErrorType;
10
- constructor(message, statusCode, responseBody, jmapErrorType) {
12
+ retryAfterMilliseconds;
13
+ attempts;
14
+ constructor(message, statusCode, responseBody, jmapErrorType, retryAfterMilliseconds, attempts) {
11
15
  super(message);
12
16
  this.name = "JmapApiError";
13
17
  this.statusCode = statusCode;
14
18
  this.responseBody = responseBody;
15
19
  this.jmapErrorType = jmapErrorType;
20
+ this.retryAfterMilliseconds = retryAfterMilliseconds;
21
+ this.attempts = attempts;
16
22
  }
17
23
  };
18
24
  /**
@@ -63,13 +69,13 @@ async function uploadBlob(config, uploadUrl, accountId, blob, signal) {
63
69
  for (const [key, value] of Object.entries(config.headers)) headers[key] = value;
64
70
  const controller = new AbortController();
65
71
  const timeoutId = setTimeout(() => controller.abort(), config.timeout);
66
- const combinedSignal = signal ? combineSignals(signal, controller.signal) : controller.signal;
72
+ const combinedSignal = combineSignals(controller.signal, signal);
67
73
  try {
68
74
  const response = await fetch(url, {
69
75
  method: "POST",
70
76
  headers,
71
77
  body: blob,
72
- signal: combinedSignal
78
+ signal: combinedSignal.signal
73
79
  });
74
80
  if (!response.ok) {
75
81
  const body = await response.text();
@@ -78,23 +84,10 @@ async function uploadBlob(config, uploadUrl, accountId, blob, signal) {
78
84
  const result = await response.json();
79
85
  return result;
80
86
  } finally {
87
+ combinedSignal.cleanup();
81
88
  clearTimeout(timeoutId);
82
89
  }
83
90
  }
84
- /**
85
- * Combine multiple abort signals into one.
86
- */
87
- function combineSignals(...signals) {
88
- const controller = new AbortController();
89
- for (const signal of signals) {
90
- if (signal.aborted) {
91
- controller.abort(signal.reason);
92
- break;
93
- }
94
- signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
95
- }
96
- return controller.signal;
97
- }
98
91
 
99
92
  //#endregion
100
93
  //#region src/config.ts
@@ -143,7 +136,7 @@ var JmapHttpClient = class {
143
136
  const response = await this.fetchWithAuth(this.config.sessionUrl, { method: "GET" }, signal);
144
137
  if (!response.ok) {
145
138
  const text = await response.text();
146
- throw new JmapApiError(`Session fetch failed: ${response.status}`, response.status, text);
139
+ throw new JmapApiError(`Session fetch failed: ${response.status}`, response.status, text, void 0, parseRetryAfter(response.headers.get("Retry-After")), 1);
147
140
  }
148
141
  return await response.json();
149
142
  }
@@ -168,23 +161,28 @@ var JmapHttpClient = class {
168
161
  }, signal);
169
162
  if (!response.ok) {
170
163
  const text = await response.text();
171
- const error = new JmapApiError(`JMAP request failed: ${response.status}`, response.status, text);
164
+ const error = new JmapApiError(`JMAP request failed: ${response.status}`, response.status, text, void 0, parseRetryAfter(response.headers.get("Retry-After")), attempt + 1);
172
165
  if (response.status >= 400 && response.status < 500) throw error;
173
166
  throw error;
174
167
  }
175
168
  return await response.json();
176
169
  } catch (error) {
177
- if (error instanceof Error && error.name === "AbortError") throw error;
170
+ if (isCallerAbort(error, signal)) throw error;
178
171
  if (error instanceof JmapApiError && error.statusCode !== void 0) {
179
172
  if (error.statusCode >= 400 && error.statusCode < 500) throw error;
180
173
  }
181
174
  lastError = error instanceof Error ? error : new Error(String(error));
182
- if (attempt === this.config.retries) throw error;
175
+ if (attempt === this.config.retries) {
176
+ if (error instanceof JmapApiError) throw error;
177
+ if (isAbortError$1(error)) throw new JmapApiError("JMAP request timed out.", void 0, void 0, void 0, void 0, attempt + 1);
178
+ throw new JmapApiError(lastError.message, void 0, void 0, void 0, void 0, attempt + 1);
179
+ }
183
180
  const delay = Math.pow(2, attempt) * 1e3;
184
181
  await new Promise((resolve) => setTimeout(resolve, delay));
185
182
  }
186
183
  }
187
- throw lastError || /* @__PURE__ */ new Error("Request failed after all retries");
184
+ if (lastError != null) throw lastError;
185
+ throw new Error("Request failed after all retries");
188
186
  }
189
187
  /**
190
188
  * Makes an authenticated fetch request.
@@ -230,6 +228,12 @@ var JmapHttpClient = class {
230
228
  }
231
229
  }
232
230
  };
231
+ function isCallerAbort(error, signal) {
232
+ return signal?.aborted === true && (isAbortError$1(error) || error === signal.reason);
233
+ }
234
+ function isAbortError$1(error) {
235
+ return error instanceof Error && error.name === "AbortError";
236
+ }
233
237
 
234
238
  //#endregion
235
239
  //#region src/message-converter.ts
@@ -442,6 +446,7 @@ const JMAP_CAPABILITIES = {
442
446
  * @since 0.4.0
443
447
  */
444
448
  var JmapTransport = class {
449
+ id = "jmap";
445
450
  config;
446
451
  httpClient;
447
452
  cachedSession = null;
@@ -459,19 +464,22 @@ var JmapTransport = class {
459
464
  * @param message The message to send.
460
465
  * @param options Optional transport options.
461
466
  * @returns A receipt indicating success or failure.
467
+ * @throws The abort reason or an `AbortError` when the caller aborts the
468
+ * operation.
462
469
  * @since 0.4.0
463
470
  */
464
471
  async send(message, options) {
465
472
  const signal = options?.signal;
473
+ signal?.throwIfAborted();
466
474
  try {
467
- signal?.throwIfAborted();
468
475
  const session = await this.getSession(signal);
469
476
  signal?.throwIfAborted();
470
477
  const accountId = this.config.accountId ?? findMailAccount(session);
471
- if (!accountId) return {
472
- successful: false,
473
- errorMessages: ["No mail-capable account found in JMAP session"]
474
- };
478
+ if (!accountId) return createJmapFailure("No mail-capable account found in JMAP session", void 0, {
479
+ category: "configuration",
480
+ code: "jmap.no_mail_account",
481
+ retryable: false
482
+ });
475
483
  const draftsMailboxId = await this.getDraftsMailboxId(session, accountId, signal);
476
484
  signal?.throwIfAborted();
477
485
  const identityId = await this.getIdentityId(session, accountId, message.sender.address, signal);
@@ -506,18 +514,14 @@ var JmapTransport = class {
506
514
  }, signal);
507
515
  return this.parseResponse(response);
508
516
  } catch (error) {
509
- if (error instanceof Error && error.name === "AbortError") return {
510
- successful: false,
511
- errorMessages: [`Request aborted: ${error.message}`]
512
- };
513
- if (error instanceof JmapApiError) return {
514
- successful: false,
515
- errorMessages: [error.message]
516
- };
517
- return {
518
- successful: false,
519
- errorMessages: [error instanceof Error ? error.message : String(error)]
520
- };
517
+ if (signal?.aborted) throw getAbortReason(signal, error);
518
+ if (error instanceof Error && error.name === "AbortError") return createJmapFailure(`Request aborted: ${error.message}`, error, {
519
+ category: "timeout",
520
+ code: "abort",
521
+ retryable: true
522
+ });
523
+ if (error instanceof JmapApiError) return createJmapFailure(error.message, error);
524
+ return createJmapFailure(error instanceof Error ? error.message : String(error), error);
521
525
  }
522
526
  }
523
527
  /**
@@ -525,6 +529,8 @@ var JmapTransport = class {
525
529
  * @param messages The messages to send.
526
530
  * @param options Optional transport options.
527
531
  * @yields Receipts for each message.
532
+ * @throws The abort reason or an `AbortError` when the caller aborts the
533
+ * operation.
528
534
  * @since 0.4.0
529
535
  */
530
536
  async *sendMany(messages, options) {
@@ -542,10 +548,11 @@ var JmapTransport = class {
542
548
  processingStage = "account discovery";
543
549
  const accountId = this.config.accountId ?? findMailAccount(session);
544
550
  if (!accountId) {
545
- for (let i = 0; i < messageArray.length; i++) yield {
546
- successful: false,
547
- errorMessages: ["No mail-capable account found in JMAP session"]
548
- };
551
+ for (let i = 0; i < messageArray.length; i++) yield createJmapFailure("No mail-capable account found in JMAP session", void 0, {
552
+ category: "configuration",
553
+ code: "jmap.no_mail_account",
554
+ retryable: false
555
+ });
549
556
  return;
550
557
  }
551
558
  processingStage = "mailbox discovery";
@@ -603,13 +610,16 @@ var JmapTransport = class {
603
610
  }, signal);
604
611
  for (let i = 0; i < messageArray.length; i++) yield this.parseBatchResponseForIndex(response, i);
605
612
  } catch (error) {
613
+ if (signal?.aborted) throw getAbortReason(signal, error);
614
+ const timeoutOverride = error instanceof Error && error.name === "AbortError" ? {
615
+ category: "timeout",
616
+ code: "abort",
617
+ retryable: true
618
+ } : void 0;
606
619
  const baseMessage = error instanceof Error && error.name === "AbortError" ? `Request aborted: ${error.message}` : error instanceof JmapApiError ? error.message : error instanceof Error ? error.message : String(error);
607
620
  let detailedMessage = `Failed during ${processingStage}: ${baseMessage}`;
608
621
  if (processingStage === "attachment upload" && attachmentsUploadedCount > 0) detailedMessage += ` (${attachmentsUploadedCount}/${messageArray.length} messages had attachments uploaded before failure)`;
609
- for (let i = 0; i < messageArray.length; i++) yield {
610
- successful: false,
611
- errorMessages: [detailedMessage]
612
- };
622
+ for (let i = 0; i < messageArray.length; i++) yield createJmapFailure(detailedMessage, error, timeoutOverride);
613
623
  }
614
624
  }
615
625
  /**
@@ -767,14 +777,17 @@ var JmapTransport = class {
767
777
  if (submissionResult.notCreated) for (const [key, error] of Object.entries(submissionResult.notCreated)) errors.push(`Email submission failed (${key}): ${error.type}${error.description ? ` - ${error.description}` : ""}`);
768
778
  if (submissionResult.created?.submission) return {
769
779
  successful: true,
770
- messageId: submissionResult.created.submission.id
780
+ messageId: submissionResult.created.submission.id,
781
+ provider: "jmap"
771
782
  };
772
783
  }
773
784
  if (errors.length === 0) errors.push("Unknown error: No submission result received");
774
- return {
775
- successful: false,
776
- errorMessages: errors
777
- };
785
+ return createFailedReceipt(errors, {
786
+ provider: "jmap",
787
+ category: "rejected",
788
+ code: "jmap.submission_failed",
789
+ retryable: false
790
+ });
778
791
  }
779
792
  /**
780
793
  * Parses the JMAP batch response to extract receipt for a specific index.
@@ -804,16 +817,53 @@ var JmapTransport = class {
804
817
  }
805
818
  if (submissionResult.created?.[subKey]) return {
806
819
  successful: true,
807
- messageId: submissionResult.created[subKey].id
820
+ messageId: submissionResult.created[subKey].id,
821
+ provider: "jmap"
808
822
  };
809
823
  }
810
824
  if (errors.length === 0) errors.push("Unknown error: No submission result received");
811
- return {
812
- successful: false,
813
- errorMessages: errors
814
- };
825
+ return createFailedReceipt(errors, {
826
+ provider: "jmap",
827
+ category: "rejected",
828
+ code: "jmap.submission_failed",
829
+ retryable: false
830
+ });
815
831
  }
816
832
  };
833
+ function createJmapFailure(message, error, override = {}) {
834
+ const attempts = getAttemptCount(error);
835
+ if (error instanceof JmapApiError) return createFailedReceipt(message, {
836
+ provider: "jmap",
837
+ statusCode: error.statusCode,
838
+ retryAfterMilliseconds: error.retryAfterMilliseconds,
839
+ attempts,
840
+ providerDetails: {
841
+ responseBody: error.responseBody,
842
+ jmapErrorType: error.jmapErrorType
843
+ },
844
+ category: override.category,
845
+ code: override.code,
846
+ retryable: override.retryable
847
+ });
848
+ return createFailedReceipt(message, {
849
+ provider: "jmap",
850
+ attempts,
851
+ category: override.category,
852
+ code: override.code,
853
+ retryable: override.retryable
854
+ });
855
+ }
856
+ function getAttemptCount(error) {
857
+ if (error instanceof JmapApiError) return error.attempts ?? 1;
858
+ if (typeof error === "object" && error !== null && "attempts" in error && typeof error.attempts === "number") return error.attempts;
859
+ return 1;
860
+ }
861
+ function getAbortReason(signal, fallback) {
862
+ return signal.reason ?? (isAbortError(fallback) ? fallback : void 0) ?? new DOMException("The operation was aborted.", "AbortError");
863
+ }
864
+ function isAbortError(error) {
865
+ return error instanceof Error && error.name === "AbortError";
866
+ }
817
867
 
818
868
  //#endregion
819
869
  export { JMAP_ERROR_TYPES, JmapApiError, JmapTransport, createJmapConfig, isCapabilityError, uploadBlob };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/jmap",
3
- "version": "0.5.0-dev.86",
3
+ "version": "0.5.0",
4
4
  "description": "JMAP transport for Upyo email library",
5
5
  "keywords": [
6
6
  "email",
@@ -53,19 +53,14 @@
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
  "jmap-rfc-types": "^0.1.2",
61
60
  "tsdown": "^0.12.7",
62
61
  "typescript": "5.8.3"
63
62
  },
64
63
  "scripts": {
65
- "build": "tsdown",
66
- "prepublish": "tsdown",
67
- "test": "tsdown && dotenvx run --ignore=MISSING_ENV_FILE -- node --experimental-transform-types --test",
68
- "test:bun": "tsdown && bun test --timeout=30000 --env-file=.env",
69
- "test:deno": "DENO_JOBS=1 deno test --allow-env --allow-net --env-file=.env"
64
+ "prepublish": "mise run --no-deps :build"
70
65
  }
71
66
  }