@upyo/jmap 0.5.0-dev.87 → 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 +129 -57
- package/dist/index.d.cts +11 -4
- package/dist/index.d.ts +11 -4
- package/dist/index.js +107 -57
- package/package.json +3 -8
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
|
-
|
|
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 =
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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 (
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
548
|
-
|
|
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
|
-
|
|
777
|
-
|
|
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
|
-
|
|
814
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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 (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
547
|
-
|
|
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
|
-
|
|
776
|
-
|
|
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
|
-
|
|
813
|
-
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
}
|