@upyo/jmap 0.4.0-dev.1

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 ADDED
@@ -0,0 +1,685 @@
1
+
2
+ //#region src/errors.ts
3
+ /**
4
+ * Error class for JMAP API errors.
5
+ * @since 0.4.0
6
+ */
7
+ var JmapApiError = class extends Error {
8
+ statusCode;
9
+ responseBody;
10
+ jmapErrorType;
11
+ constructor(message, statusCode, responseBody, jmapErrorType) {
12
+ super(message);
13
+ this.name = "JmapApiError";
14
+ this.statusCode = statusCode;
15
+ this.responseBody = responseBody;
16
+ this.jmapErrorType = jmapErrorType;
17
+ }
18
+ };
19
+ /**
20
+ * JMAP-specific error types from RFC 8620.
21
+ * @since 0.4.0
22
+ */
23
+ const JMAP_ERROR_TYPES = {
24
+ unknownCapability: "urn:ietf:params:jmap:error:unknownCapability",
25
+ notJSON: "urn:ietf:params:jmap:error:notJSON",
26
+ notRequest: "urn:ietf:params:jmap:error:notRequest",
27
+ limit: "urn:ietf:params:jmap:error:limit"
28
+ };
29
+ /**
30
+ * Checks if the given error is a JMAP capability error.
31
+ * @param error The error to check.
32
+ * @returns `true` if the error is a JMAP capability error, `false` otherwise.
33
+ * @since 0.4.0
34
+ */
35
+ function isCapabilityError(error) {
36
+ return error instanceof JmapApiError && error.jmapErrorType === JMAP_ERROR_TYPES.unknownCapability;
37
+ }
38
+
39
+ //#endregion
40
+ //#region src/blob-uploader.ts
41
+ /**
42
+ * Upload a blob to the JMAP server.
43
+ *
44
+ * @param config - The resolved JMAP configuration
45
+ * @param uploadUrl - The upload URL template from the session (e.g., "https://server/upload/{accountId}")
46
+ * @param accountId - The account ID to upload to
47
+ * @param blob - The blob or file to upload
48
+ * @param signal - Optional abort signal
49
+ * @returns The upload response containing the blobId
50
+ */
51
+ async function uploadBlob(config, uploadUrl, accountId, blob, signal) {
52
+ signal?.throwIfAborted();
53
+ const url = uploadUrl.replace("{accountId}", accountId);
54
+ let authHeader;
55
+ if (config.bearerToken) authHeader = `Bearer ${config.bearerToken}`;
56
+ else if (config.basicAuth) {
57
+ const credentials = btoa(`${config.basicAuth.username}:${config.basicAuth.password}`);
58
+ authHeader = `Basic ${credentials}`;
59
+ } else throw new Error("No authentication method configured");
60
+ const headers = {
61
+ Authorization: authHeader,
62
+ "Content-Type": blob.type || "application/octet-stream"
63
+ };
64
+ for (const [key, value] of Object.entries(config.headers)) headers[key] = value;
65
+ const controller = new AbortController();
66
+ const timeoutId = setTimeout(() => controller.abort(), config.timeout);
67
+ const combinedSignal = signal ? combineSignals(signal, controller.signal) : controller.signal;
68
+ try {
69
+ const response = await fetch(url, {
70
+ method: "POST",
71
+ headers,
72
+ body: blob,
73
+ signal: combinedSignal
74
+ });
75
+ if (!response.ok) {
76
+ const body = await response.text();
77
+ throw new JmapApiError(`Blob upload failed: ${response.status} ${response.statusText}`, response.status, body);
78
+ }
79
+ const result = await response.json();
80
+ return result;
81
+ } finally {
82
+ clearTimeout(timeoutId);
83
+ }
84
+ }
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
+
100
+ //#endregion
101
+ //#region src/config.ts
102
+ /**
103
+ * Creates a resolved JMAP configuration with default values applied.
104
+ * @param config The user-provided configuration.
105
+ * @returns The resolved configuration with all fields populated.
106
+ * @throws Error if neither bearerToken nor basicAuth is provided.
107
+ * @since 0.4.0
108
+ */
109
+ function createJmapConfig(config) {
110
+ if (!config.bearerToken && !config.basicAuth) throw new Error("Either bearerToken or basicAuth must be provided");
111
+ return {
112
+ sessionUrl: config.sessionUrl,
113
+ bearerToken: config.bearerToken ?? null,
114
+ basicAuth: config.basicAuth ?? null,
115
+ accountId: config.accountId ?? null,
116
+ identityId: config.identityId ?? null,
117
+ timeout: config.timeout || 3e4,
118
+ retries: config.retries || 3,
119
+ headers: config.headers ?? {},
120
+ sessionCacheTtl: config.sessionCacheTtl || 3e5,
121
+ baseUrl: config.baseUrl ?? null
122
+ };
123
+ }
124
+
125
+ //#endregion
126
+ //#region src/http-client.ts
127
+ /**
128
+ * HTTP client for JMAP API requests.
129
+ * @since 0.4.0
130
+ */
131
+ var JmapHttpClient = class {
132
+ config;
133
+ constructor(config) {
134
+ this.config = config;
135
+ }
136
+ /**
137
+ * Fetches the JMAP session from the session URL.
138
+ * @param signal Optional abort signal.
139
+ * @returns The JMAP session response.
140
+ * @since 0.4.0
141
+ */
142
+ async fetchSession(signal) {
143
+ signal?.throwIfAborted();
144
+ const response = await this.fetchWithAuth(this.config.sessionUrl, { method: "GET" }, signal);
145
+ if (!response.ok) {
146
+ const text = await response.text();
147
+ throw new JmapApiError(`Session fetch failed: ${response.status}`, response.status, text);
148
+ }
149
+ return await response.json();
150
+ }
151
+ /**
152
+ * Executes a JMAP API request.
153
+ * @param apiUrl The JMAP API URL.
154
+ * @param request The JMAP request payload.
155
+ * @param signal Optional abort signal.
156
+ * @returns The JMAP response.
157
+ * @since 0.4.0
158
+ */
159
+ async executeRequest(apiUrl, request, signal) {
160
+ signal?.throwIfAborted();
161
+ let lastError = null;
162
+ for (let attempt = 0; attempt <= this.config.retries; attempt++) {
163
+ signal?.throwIfAborted();
164
+ try {
165
+ const response = await this.fetchWithAuth(apiUrl, {
166
+ method: "POST",
167
+ headers: { "Content-Type": "application/json" },
168
+ body: JSON.stringify(request)
169
+ }, signal);
170
+ if (!response.ok) {
171
+ const text = await response.text();
172
+ const error = new JmapApiError(`JMAP request failed: ${response.status}`, response.status, text);
173
+ if (response.status >= 400 && response.status < 500) throw error;
174
+ throw error;
175
+ }
176
+ return await response.json();
177
+ } catch (error) {
178
+ if (error instanceof Error && error.name === "AbortError") throw error;
179
+ if (error instanceof JmapApiError && error.statusCode !== void 0) {
180
+ if (error.statusCode >= 400 && error.statusCode < 500) throw error;
181
+ }
182
+ lastError = error instanceof Error ? error : new Error(String(error));
183
+ if (attempt === this.config.retries) throw error;
184
+ const delay = Math.pow(2, attempt) * 1e3;
185
+ await new Promise((resolve) => setTimeout(resolve, delay));
186
+ }
187
+ }
188
+ throw lastError || /* @__PURE__ */ new Error("Request failed after all retries");
189
+ }
190
+ /**
191
+ * Makes an authenticated fetch request.
192
+ * @param url The URL to fetch.
193
+ * @param options Fetch options.
194
+ * @param signal Optional abort signal.
195
+ * @returns The fetch response.
196
+ * @since 0.4.0
197
+ */
198
+ async fetchWithAuth(url, options, signal) {
199
+ const headers = new Headers(options.headers);
200
+ if (this.config.bearerToken) headers.set("Authorization", `Bearer ${this.config.bearerToken}`);
201
+ else if (this.config.basicAuth) {
202
+ const credentials = btoa(`${this.config.basicAuth.username}:${this.config.basicAuth.password}`);
203
+ headers.set("Authorization", `Basic ${credentials}`);
204
+ }
205
+ for (const [key, value] of Object.entries(this.config.headers)) headers.set(key, value);
206
+ const controller = new AbortController();
207
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
208
+ let combinedSignal = controller.signal;
209
+ if (signal) {
210
+ if (signal.aborted) {
211
+ clearTimeout(timeoutId);
212
+ signal.throwIfAborted();
213
+ }
214
+ signal.addEventListener("abort", () => controller.abort());
215
+ combinedSignal = controller.signal;
216
+ }
217
+ try {
218
+ return await globalThis.fetch(url, {
219
+ ...options,
220
+ headers,
221
+ signal: combinedSignal
222
+ });
223
+ } finally {
224
+ clearTimeout(timeoutId);
225
+ }
226
+ }
227
+ };
228
+
229
+ //#endregion
230
+ //#region src/message-converter.ts
231
+ /**
232
+ * Formats an Upyo Address to JMAP email address format.
233
+ * @param address The Upyo address to format.
234
+ * @returns The JMAP email address object.
235
+ * @since 0.4.0
236
+ */
237
+ function formatAddress(address) {
238
+ const result = { email: address.address };
239
+ if (address.name) return {
240
+ ...result,
241
+ name: address.name
242
+ };
243
+ return result;
244
+ }
245
+ /**
246
+ * Gets priority headers based on message priority.
247
+ * @param priority The message priority.
248
+ * @returns Array of JMAP headers for priority, or empty array for normal.
249
+ * @since 0.4.0
250
+ */
251
+ function getPriorityHeaders(priority) {
252
+ switch (priority) {
253
+ case "high": return [{
254
+ name: "X-Priority",
255
+ value: "1"
256
+ }, {
257
+ name: "Importance",
258
+ value: "high"
259
+ }];
260
+ case "low": return [{
261
+ name: "X-Priority",
262
+ value: "5"
263
+ }, {
264
+ name: "Importance",
265
+ value: "low"
266
+ }];
267
+ default: return [];
268
+ }
269
+ }
270
+ /**
271
+ * Extracts custom headers from message headers.
272
+ * @param message The message to extract headers from.
273
+ * @returns Array of JMAP headers.
274
+ * @since 0.4.0
275
+ */
276
+ function extractCustomHeaders(message) {
277
+ const headers = [];
278
+ for (const [name, value] of message.headers.entries()) headers.push({
279
+ name,
280
+ value
281
+ });
282
+ return headers;
283
+ }
284
+ /**
285
+ * Builds body structure for the message content.
286
+ * @param message The message to build body structure for.
287
+ * @param uploadedBlobs Map of contentId to blobId for attachments.
288
+ * @returns Object containing bodyStructure and bodyValues.
289
+ * @since 0.4.0
290
+ */
291
+ function buildBodyStructure(message, uploadedBlobs) {
292
+ const bodyValues = {};
293
+ const parts = [];
294
+ if ("text" in message.content && message.content.text) {
295
+ bodyValues["text"] = { value: message.content.text };
296
+ parts.push({
297
+ partId: "text",
298
+ type: "text/plain; charset=utf-8"
299
+ });
300
+ }
301
+ if ("html" in message.content && message.content.html) {
302
+ bodyValues["html"] = { value: message.content.html };
303
+ parts.push({
304
+ partId: "html",
305
+ type: "text/html; charset=utf-8"
306
+ });
307
+ }
308
+ let contentPart;
309
+ if (parts.length === 1) contentPart = parts[0];
310
+ else if (parts.length > 1) contentPart = {
311
+ type: "multipart/alternative",
312
+ subParts: parts
313
+ };
314
+ else {
315
+ bodyValues["text"] = { value: "" };
316
+ contentPart = {
317
+ partId: "text",
318
+ type: "text/plain; charset=utf-8"
319
+ };
320
+ }
321
+ const inlineParts = buildInlineAttachmentParts(message.attachments, uploadedBlobs);
322
+ const attachmentParts = buildAttachmentParts(message.attachments, uploadedBlobs);
323
+ let mainBodyPart;
324
+ if (inlineParts.length > 0) mainBodyPart = {
325
+ type: "multipart/related",
326
+ subParts: [contentPart, ...inlineParts]
327
+ };
328
+ else mainBodyPart = contentPart;
329
+ let bodyStructure;
330
+ if (attachmentParts.length > 0) bodyStructure = {
331
+ type: "multipart/mixed",
332
+ subParts: [mainBodyPart, ...attachmentParts]
333
+ };
334
+ else bodyStructure = mainBodyPart;
335
+ return {
336
+ bodyStructure,
337
+ bodyValues
338
+ };
339
+ }
340
+ /**
341
+ * Build inline attachment parts from uploaded blobs.
342
+ * @param attachments Array of attachments from the message.
343
+ * @param uploadedBlobs Map of contentId to blobId.
344
+ * @returns Array of JmapBodyPart for inline attachments.
345
+ * @since 0.4.0
346
+ */
347
+ function buildInlineAttachmentParts(attachments, uploadedBlobs) {
348
+ const parts = [];
349
+ for (const attachment of attachments) {
350
+ const blobId = uploadedBlobs.get(attachment.contentId);
351
+ if (!blobId) continue;
352
+ if (attachment.inline) parts.push({
353
+ type: attachment.contentType,
354
+ blobId,
355
+ name: attachment.filename,
356
+ disposition: "inline",
357
+ cid: attachment.contentId
358
+ });
359
+ }
360
+ return parts;
361
+ }
362
+ /**
363
+ * Build regular attachment parts from uploaded blobs.
364
+ * @param attachments Array of attachments from the message.
365
+ * @param uploadedBlobs Map of contentId to blobId.
366
+ * @returns Array of JmapBodyPart for regular attachments.
367
+ * @since 0.4.0
368
+ */
369
+ function buildAttachmentParts(attachments, uploadedBlobs) {
370
+ const parts = [];
371
+ for (const attachment of attachments) {
372
+ const blobId = uploadedBlobs.get(attachment.contentId);
373
+ if (!blobId) continue;
374
+ if (!attachment.inline) parts.push({
375
+ type: attachment.contentType,
376
+ blobId,
377
+ name: attachment.filename,
378
+ disposition: "attachment"
379
+ });
380
+ }
381
+ return parts;
382
+ }
383
+ /**
384
+ * Converts an Upyo Message to JMAP Email/set create format.
385
+ * @param message The Upyo message to convert.
386
+ * @param draftMailboxId The mailbox ID to store the draft in.
387
+ * @param uploadedBlobs Map of contentId to blobId for attachments.
388
+ * @returns The JMAP Email/set create object.
389
+ * @since 0.4.0
390
+ */
391
+ function convertMessage(message, draftMailboxId, uploadedBlobs) {
392
+ const { bodyStructure, bodyValues } = buildBodyStructure(message, uploadedBlobs);
393
+ const headers = [];
394
+ if (message.priority !== "normal") headers.push(...getPriorityHeaders(message.priority));
395
+ headers.push(...extractCustomHeaders(message));
396
+ const email = {
397
+ mailboxIds: { [draftMailboxId]: true },
398
+ from: [formatAddress(message.sender)],
399
+ subject: message.subject,
400
+ bodyStructure,
401
+ bodyValues,
402
+ ...message.recipients.length > 0 && { to: message.recipients.map(formatAddress) },
403
+ ...message.ccRecipients.length > 0 && { cc: message.ccRecipients.map(formatAddress) },
404
+ ...message.bccRecipients.length > 0 && { bcc: message.bccRecipients.map(formatAddress) },
405
+ ...message.replyRecipients.length > 0 && { replyTo: message.replyRecipients.map(formatAddress) },
406
+ ...headers.length > 0 && { headers }
407
+ };
408
+ return email;
409
+ }
410
+
411
+ //#endregion
412
+ //#region src/session.ts
413
+ /**
414
+ * Finds the first account with mail capability from a JMAP session.
415
+ * @param session The JMAP session response.
416
+ * @returns The account ID with mail capability, or `null` if none found.
417
+ * @since 0.4.0
418
+ */
419
+ function findMailAccount(session) {
420
+ for (const [accountId, account] of Object.entries(session.accounts)) if ("urn:ietf:params:jmap:mail" in account.accountCapabilities) return accountId;
421
+ return null;
422
+ }
423
+
424
+ //#endregion
425
+ //#region src/jmap-transport.ts
426
+ /**
427
+ * JMAP capabilities required for email sending.
428
+ * @since 0.4.0
429
+ */
430
+ const JMAP_CAPABILITIES = {
431
+ core: "urn:ietf:params:jmap:core",
432
+ mail: "urn:ietf:params:jmap:mail",
433
+ submission: "urn:ietf:params:jmap:submission"
434
+ };
435
+ /**
436
+ * JMAP transport for sending emails via JMAP protocol (RFC 8620/8621).
437
+ * @since 0.4.0
438
+ */
439
+ var JmapTransport = class {
440
+ config;
441
+ httpClient;
442
+ cachedSession = null;
443
+ /**
444
+ * Creates a new JMAP transport instance.
445
+ * @param config The JMAP transport configuration.
446
+ * @since 0.4.0
447
+ */
448
+ constructor(config) {
449
+ this.config = createJmapConfig(config);
450
+ this.httpClient = new JmapHttpClient(this.config);
451
+ }
452
+ /**
453
+ * Sends a single email message.
454
+ * @param message The message to send.
455
+ * @param options Optional transport options.
456
+ * @returns A receipt indicating success or failure.
457
+ * @since 0.4.0
458
+ */
459
+ async send(message, options) {
460
+ const signal = options?.signal;
461
+ try {
462
+ signal?.throwIfAborted();
463
+ const session = await this.getSession(signal);
464
+ signal?.throwIfAborted();
465
+ const accountId = this.config.accountId ?? findMailAccount(session);
466
+ if (!accountId) return {
467
+ successful: false,
468
+ errorMessages: ["No mail-capable account found in JMAP session"]
469
+ };
470
+ const draftsMailboxId = await this.getDraftsMailboxId(session, accountId, signal);
471
+ signal?.throwIfAborted();
472
+ const identityId = await this.getIdentityId(session, accountId, message.sender.address, signal);
473
+ signal?.throwIfAborted();
474
+ const uploadedBlobs = await this.uploadAttachments(session, accountId, message.attachments, signal);
475
+ signal?.throwIfAborted();
476
+ const emailCreate = convertMessage(message, draftsMailboxId, uploadedBlobs);
477
+ const response = await this.httpClient.executeRequest(session.apiUrl, {
478
+ using: [
479
+ JMAP_CAPABILITIES.core,
480
+ JMAP_CAPABILITIES.mail,
481
+ JMAP_CAPABILITIES.submission
482
+ ],
483
+ methodCalls: [[
484
+ "Email/set",
485
+ {
486
+ accountId,
487
+ create: { draft: emailCreate }
488
+ },
489
+ "c0"
490
+ ], [
491
+ "EmailSubmission/set",
492
+ {
493
+ accountId,
494
+ create: { submission: {
495
+ identityId,
496
+ emailId: "#draft"
497
+ } }
498
+ },
499
+ "c1"
500
+ ]]
501
+ }, signal);
502
+ return this.parseResponse(response);
503
+ } catch (error) {
504
+ if (error instanceof Error && error.name === "AbortError") return {
505
+ successful: false,
506
+ errorMessages: [`Request aborted: ${error.message}`]
507
+ };
508
+ if (error instanceof JmapApiError) return {
509
+ successful: false,
510
+ errorMessages: [error.message]
511
+ };
512
+ return {
513
+ successful: false,
514
+ errorMessages: [error instanceof Error ? error.message : String(error)]
515
+ };
516
+ }
517
+ }
518
+ /**
519
+ * Sends multiple messages sequentially.
520
+ * @param messages The messages to send.
521
+ * @param options Optional transport options.
522
+ * @yields Receipts for each message.
523
+ * @since 0.4.0
524
+ */
525
+ async *sendMany(messages, options) {
526
+ for await (const message of messages) yield await this.send(message, options);
527
+ }
528
+ /**
529
+ * Gets or refreshes the JMAP session.
530
+ * @param signal Optional abort signal.
531
+ * @returns The JMAP session.
532
+ * @since 0.4.0
533
+ */
534
+ async getSession(signal) {
535
+ const now = Date.now();
536
+ if (this.cachedSession && now - this.cachedSession.fetchedAt < this.config.sessionCacheTtl) return this.cachedSession.session;
537
+ let session = await this.httpClient.fetchSession(signal);
538
+ if (this.config.baseUrl) session = this.rewriteSessionUrls(session);
539
+ this.cachedSession = {
540
+ session,
541
+ fetchedAt: now
542
+ };
543
+ return session;
544
+ }
545
+ /**
546
+ * Rewrites session URLs to use the configured baseUrl.
547
+ * @param session The original session from the server.
548
+ * @returns A new session with rewritten URLs.
549
+ * @since 0.4.0
550
+ */
551
+ rewriteSessionUrls(session) {
552
+ const baseUrl = this.config.baseUrl;
553
+ const rewriteUrl = (url) => {
554
+ try {
555
+ const base = new URL(baseUrl);
556
+ return url.replace(/^(\w+):\/\/[^/]+/, `${base.protocol}//${base.host}`);
557
+ } catch {
558
+ return url;
559
+ }
560
+ };
561
+ return {
562
+ ...session,
563
+ apiUrl: rewriteUrl(session.apiUrl),
564
+ downloadUrl: rewriteUrl(session.downloadUrl),
565
+ uploadUrl: rewriteUrl(session.uploadUrl),
566
+ eventSourceUrl: session.eventSourceUrl ? rewriteUrl(session.eventSourceUrl) : void 0
567
+ };
568
+ }
569
+ /**
570
+ * Gets the drafts mailbox ID from the session.
571
+ * @param session The JMAP session.
572
+ * @param accountId The account ID.
573
+ * @param signal Optional abort signal.
574
+ * @returns The drafts mailbox ID.
575
+ * @since 0.4.0
576
+ */
577
+ async getDraftsMailboxId(session, accountId, signal) {
578
+ const response = await this.httpClient.executeRequest(session.apiUrl, {
579
+ using: [JMAP_CAPABILITIES.core, JMAP_CAPABILITIES.mail],
580
+ methodCalls: [[
581
+ "Mailbox/get",
582
+ {
583
+ accountId,
584
+ properties: [
585
+ "id",
586
+ "role",
587
+ "name"
588
+ ]
589
+ },
590
+ "c0"
591
+ ]]
592
+ }, signal);
593
+ const mailboxResponse = response.methodResponses.find((r) => r[0] === "Mailbox/get");
594
+ if (!mailboxResponse) throw new JmapApiError("No Mailbox/get response received");
595
+ const mailboxes = mailboxResponse[1].list;
596
+ if (!mailboxes) throw new JmapApiError("No mailboxes found");
597
+ const drafts = mailboxes.find((m) => m.role === "drafts");
598
+ if (!drafts) throw new JmapApiError("No drafts mailbox found");
599
+ return drafts.id;
600
+ }
601
+ /**
602
+ * Gets the identity ID for the sender.
603
+ * @param session The JMAP session.
604
+ * @param accountId The account ID.
605
+ * @param senderEmail The sender's email address.
606
+ * @param signal Optional abort signal.
607
+ * @returns The identity ID.
608
+ * @since 0.4.0
609
+ */
610
+ async getIdentityId(session, accountId, senderEmail, signal) {
611
+ if (this.config.identityId) return this.config.identityId;
612
+ const response = await this.httpClient.executeRequest(session.apiUrl, {
613
+ using: [JMAP_CAPABILITIES.core, JMAP_CAPABILITIES.submission],
614
+ methodCalls: [[
615
+ "Identity/get",
616
+ { accountId },
617
+ "c0"
618
+ ]]
619
+ }, signal);
620
+ const identityResponse = response.methodResponses.find((r) => r[0] === "Identity/get");
621
+ if (!identityResponse) throw new JmapApiError("No Identity/get response received");
622
+ const identities = identityResponse[1].list;
623
+ if (!identities || identities.length === 0) throw new JmapApiError("No identities found");
624
+ const matching = identities.find((i) => i.email.toLowerCase() === senderEmail.toLowerCase());
625
+ if (matching) return matching.id;
626
+ return identities[0].id;
627
+ }
628
+ /**
629
+ * Uploads all attachments and returns a map of contentId to blobId.
630
+ * @param session The JMAP session.
631
+ * @param accountId The account ID.
632
+ * @param attachments Array of attachments to upload.
633
+ * @param signal Optional abort signal.
634
+ * @returns Map of contentId to blobId.
635
+ * @since 0.4.0
636
+ */
637
+ async uploadAttachments(session, accountId, attachments, signal) {
638
+ const uploadedBlobs = /* @__PURE__ */ new Map();
639
+ for (const attachment of attachments) {
640
+ signal?.throwIfAborted();
641
+ const content = await attachment.content;
642
+ const arrayBuffer = content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
643
+ const blob = new Blob([arrayBuffer], { type: attachment.contentType });
644
+ const result = await uploadBlob(this.config, session.uploadUrl, accountId, blob, signal);
645
+ uploadedBlobs.set(attachment.contentId, result.blobId);
646
+ }
647
+ return uploadedBlobs;
648
+ }
649
+ /**
650
+ * Parses the JMAP response to extract receipt information.
651
+ * @param response The JMAP response.
652
+ * @returns A receipt indicating success or failure.
653
+ * @since 0.4.0
654
+ */
655
+ parseResponse(response) {
656
+ const errors = [];
657
+ const emailResponse = response.methodResponses.find((r) => r[0] === "Email/set");
658
+ if (emailResponse) {
659
+ const emailResult = emailResponse[1];
660
+ if (emailResult.notCreated) for (const [key, error] of Object.entries(emailResult.notCreated)) errors.push(`Email creation failed (${key}): ${error.type}${error.description ? ` - ${error.description}` : ""}`);
661
+ }
662
+ const submissionResponse = response.methodResponses.find((r) => r[0] === "EmailSubmission/set");
663
+ if (submissionResponse) {
664
+ const submissionResult = submissionResponse[1];
665
+ if (submissionResult.notCreated) for (const [key, error] of Object.entries(submissionResult.notCreated)) errors.push(`Email submission failed (${key}): ${error.type}${error.description ? ` - ${error.description}` : ""}`);
666
+ if (submissionResult.created?.submission) return {
667
+ successful: true,
668
+ messageId: submissionResult.created.submission.id
669
+ };
670
+ }
671
+ if (errors.length === 0) errors.push("Unknown error: No submission result received");
672
+ return {
673
+ successful: false,
674
+ errorMessages: errors
675
+ };
676
+ }
677
+ };
678
+
679
+ //#endregion
680
+ exports.JMAP_ERROR_TYPES = JMAP_ERROR_TYPES;
681
+ exports.JmapApiError = JmapApiError;
682
+ exports.JmapTransport = JmapTransport;
683
+ exports.createJmapConfig = createJmapConfig;
684
+ exports.isCapabilityError = isCapabilityError;
685
+ exports.uploadBlob = uploadBlob;