@upyo/jmap 0.4.0-dev.73 → 0.4.0-dev.77

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
@@ -114,10 +114,10 @@ function createJmapConfig(config) {
114
114
  basicAuth: config.basicAuth ?? null,
115
115
  accountId: config.accountId ?? null,
116
116
  identityId: config.identityId ?? null,
117
- timeout: config.timeout || 3e4,
118
- retries: config.retries || 3,
117
+ timeout: config.timeout ?? 3e4,
118
+ retries: config.retries ?? 3,
119
119
  headers: config.headers ?? {},
120
- sessionCacheTtl: config.sessionCacheTtl || 3e5,
120
+ sessionCacheTtl: config.sessionCacheTtl ?? 3e5,
121
121
  baseUrl: config.baseUrl ?? null
122
122
  };
123
123
  }
@@ -207,12 +207,18 @@ var JmapHttpClient = class {
207
207
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
208
208
  let combinedSignal = controller.signal;
209
209
  if (signal) {
210
- if (signal.aborted) {
211
- clearTimeout(timeoutId);
212
- signal.throwIfAborted();
210
+ const AbortSignalAny = AbortSignal.any;
211
+ if (typeof AbortSignalAny === "function") combinedSignal = AbortSignalAny([signal, controller.signal]);
212
+ else {
213
+ if (signal.aborted) {
214
+ clearTimeout(timeoutId);
215
+ signal.throwIfAborted();
216
+ }
217
+ signal.addEventListener("abort", () => {
218
+ controller.abort(signal.reason);
219
+ });
220
+ combinedSignal = controller.signal;
213
221
  }
214
- signal.addEventListener("abort", () => controller.abort());
215
- combinedSignal = controller.signal;
216
222
  }
217
223
  try {
218
224
  return await globalThis.fetch(url, {
@@ -516,14 +522,96 @@ var JmapTransport = class {
516
522
  }
517
523
  }
518
524
  /**
519
- * Sends multiple messages sequentially.
525
+ * Sends multiple messages in a single batched JMAP request.
520
526
  * @param messages The messages to send.
521
527
  * @param options Optional transport options.
522
528
  * @yields Receipts for each message.
523
529
  * @since 0.4.0
524
530
  */
525
531
  async *sendMany(messages, options) {
526
- for await (const message of messages) yield await this.send(message, options);
532
+ const signal = options?.signal;
533
+ const messageArray = [];
534
+ for await (const message of messages) messageArray.push(message);
535
+ if (messageArray.length === 0) return;
536
+ let processingStage = "initialization";
537
+ let attachmentsUploadedCount = 0;
538
+ try {
539
+ signal?.throwIfAborted();
540
+ processingStage = "session fetch";
541
+ const session = await this.getSession(signal);
542
+ signal?.throwIfAborted();
543
+ processingStage = "account discovery";
544
+ const accountId = this.config.accountId ?? findMailAccount(session);
545
+ 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
+ };
550
+ return;
551
+ }
552
+ processingStage = "mailbox discovery";
553
+ const draftsMailboxId = await this.getDraftsMailboxId(session, accountId, signal);
554
+ signal?.throwIfAborted();
555
+ processingStage = "identity resolution";
556
+ const identityMap = await this.getIdentityMap(session, accountId, signal);
557
+ signal?.throwIfAborted();
558
+ processingStage = "attachment upload";
559
+ const allUploadedBlobs = /* @__PURE__ */ new Map();
560
+ for (let i = 0; i < messageArray.length; i++) {
561
+ const message = messageArray[i];
562
+ const uploadedBlobs = await this.uploadAttachments(session, accountId, message.attachments, signal);
563
+ allUploadedBlobs.set(i, uploadedBlobs);
564
+ attachmentsUploadedCount = i + 1;
565
+ signal?.throwIfAborted();
566
+ }
567
+ processingStage = "message conversion";
568
+ const emailCreates = {};
569
+ const submissionCreates = {};
570
+ for (let i = 0; i < messageArray.length; i++) {
571
+ const message = messageArray[i];
572
+ const uploadedBlobs = allUploadedBlobs.get(i);
573
+ const emailCreate = convertMessage(message, draftsMailboxId, uploadedBlobs);
574
+ const senderEmail = message.sender.address.toLowerCase();
575
+ const identityId = identityMap.get(senderEmail) ?? identityMap.values().next().value;
576
+ emailCreates[`draft${i}`] = emailCreate;
577
+ submissionCreates[`sub${i}`] = {
578
+ identityId,
579
+ emailId: `#draft${i}`
580
+ };
581
+ }
582
+ processingStage = "batch request execution";
583
+ const response = await this.httpClient.executeRequest(session.apiUrl, {
584
+ using: [
585
+ JMAP_CAPABILITIES.core,
586
+ JMAP_CAPABILITIES.mail,
587
+ JMAP_CAPABILITIES.submission
588
+ ],
589
+ methodCalls: [[
590
+ "Email/set",
591
+ {
592
+ accountId,
593
+ create: emailCreates
594
+ },
595
+ "c0"
596
+ ], [
597
+ "EmailSubmission/set",
598
+ {
599
+ accountId,
600
+ create: submissionCreates
601
+ },
602
+ "c1"
603
+ ]]
604
+ }, signal);
605
+ for (let i = 0; i < messageArray.length; i++) yield this.parseBatchResponseForIndex(response, i);
606
+ } catch (error) {
607
+ 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
+ let detailedMessage = `Failed during ${processingStage}: ${baseMessage}`;
609
+ 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
+ };
614
+ }
527
615
  }
528
616
  /**
529
617
  * Gets or refreshes the JMAP session.
@@ -609,6 +697,21 @@ var JmapTransport = class {
609
697
  */
610
698
  async getIdentityId(session, accountId, senderEmail, signal) {
611
699
  if (this.config.identityId) return this.config.identityId;
700
+ const identityMap = await this.getIdentityMap(session, accountId, signal);
701
+ const matching = identityMap.get(senderEmail.toLowerCase());
702
+ if (matching) return matching;
703
+ return identityMap.values().next().value;
704
+ }
705
+ /**
706
+ * Gets all identities and builds a map of email to identity ID.
707
+ * @param session The JMAP session.
708
+ * @param accountId The account ID.
709
+ * @param signal Optional abort signal.
710
+ * @returns Map of lowercase email to identity ID.
711
+ * @since 0.4.0
712
+ */
713
+ async getIdentityMap(session, accountId, signal) {
714
+ if (this.config.identityId) return new Map([["*", this.config.identityId]]);
612
715
  const response = await this.httpClient.executeRequest(session.apiUrl, {
613
716
  using: [JMAP_CAPABILITIES.core, JMAP_CAPABILITIES.submission],
614
717
  methodCalls: [[
@@ -621,9 +724,9 @@ var JmapTransport = class {
621
724
  if (!identityResponse) throw new JmapApiError("No Identity/get response received");
622
725
  const identities = identityResponse[1].list;
623
726
  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;
727
+ const identityMap = /* @__PURE__ */ new Map();
728
+ for (const identity of identities) identityMap.set(identity.email.toLowerCase(), identity.id);
729
+ return identityMap;
627
730
  }
628
731
  /**
629
732
  * Uploads all attachments and returns a map of contentId to blobId.
@@ -674,6 +777,43 @@ var JmapTransport = class {
674
777
  errorMessages: errors
675
778
  };
676
779
  }
780
+ /**
781
+ * Parses the JMAP batch response to extract receipt for a specific index.
782
+ * @param response The JMAP response.
783
+ * @param index The message index in the batch.
784
+ * @returns A receipt indicating success or failure for that message.
785
+ * @since 0.4.0
786
+ */
787
+ parseBatchResponseForIndex(response, index) {
788
+ const errors = [];
789
+ const draftKey = `draft${index}`;
790
+ const subKey = `sub${index}`;
791
+ const emailResponse = response.methodResponses.find((r) => r[0] === "Email/set");
792
+ if (emailResponse) {
793
+ const emailResult = emailResponse[1];
794
+ if (emailResult.notCreated?.[draftKey]) {
795
+ const error = emailResult.notCreated[draftKey];
796
+ errors.push(`Email creation failed: ${error.type}${error.description ? ` - ${error.description}` : ""}`);
797
+ }
798
+ }
799
+ const submissionResponse = response.methodResponses.find((r) => r[0] === "EmailSubmission/set");
800
+ if (submissionResponse) {
801
+ const submissionResult = submissionResponse[1];
802
+ if (submissionResult.notCreated?.[subKey]) {
803
+ const error = submissionResult.notCreated[subKey];
804
+ errors.push(`Email submission failed: ${error.type}${error.description ? ` - ${error.description}` : ""}`);
805
+ }
806
+ if (submissionResult.created?.[subKey]) return {
807
+ successful: true,
808
+ messageId: submissionResult.created[subKey].id
809
+ };
810
+ }
811
+ if (errors.length === 0) errors.push("Unknown error: No submission result received");
812
+ return {
813
+ successful: false,
814
+ errorMessages: errors
815
+ };
816
+ }
677
817
  };
678
818
 
679
819
  //#endregion
package/dist/index.d.cts CHANGED
@@ -122,7 +122,7 @@ declare class JmapTransport implements Transport {
122
122
  */
123
123
  send(message: Message, options?: TransportOptions): Promise<Receipt>;
124
124
  /**
125
- * Sends multiple messages sequentially.
125
+ * Sends multiple messages in a single batched JMAP request.
126
126
  * @param messages The messages to send.
127
127
  * @param options Optional transport options.
128
128
  * @yields Receipts for each message.
@@ -162,6 +162,15 @@ declare class JmapTransport implements Transport {
162
162
  * @since 0.4.0
163
163
  */
164
164
  private getIdentityId;
165
+ /**
166
+ * Gets all identities and builds a map of email to identity ID.
167
+ * @param session The JMAP session.
168
+ * @param accountId The account ID.
169
+ * @param signal Optional abort signal.
170
+ * @returns Map of lowercase email to identity ID.
171
+ * @since 0.4.0
172
+ */
173
+ private getIdentityMap;
165
174
  /**
166
175
  * Uploads all attachments and returns a map of contentId to blobId.
167
176
  * @param session The JMAP session.
@@ -179,6 +188,14 @@ declare class JmapTransport implements Transport {
179
188
  * @since 0.4.0
180
189
  */
181
190
  private parseResponse;
191
+ /**
192
+ * Parses the JMAP batch response to extract receipt for a specific index.
193
+ * @param response The JMAP response.
194
+ * @param index The message index in the batch.
195
+ * @returns A receipt indicating success or failure for that message.
196
+ * @since 0.4.0
197
+ */
198
+ private parseBatchResponseForIndex;
182
199
  }
183
200
  //#endregion
184
201
  //#region src/errors.d.ts
package/dist/index.d.ts CHANGED
@@ -122,7 +122,7 @@ declare class JmapTransport implements Transport {
122
122
  */
123
123
  send(message: Message, options?: TransportOptions): Promise<Receipt>;
124
124
  /**
125
- * Sends multiple messages sequentially.
125
+ * Sends multiple messages in a single batched JMAP request.
126
126
  * @param messages The messages to send.
127
127
  * @param options Optional transport options.
128
128
  * @yields Receipts for each message.
@@ -162,6 +162,15 @@ declare class JmapTransport implements Transport {
162
162
  * @since 0.4.0
163
163
  */
164
164
  private getIdentityId;
165
+ /**
166
+ * Gets all identities and builds a map of email to identity ID.
167
+ * @param session The JMAP session.
168
+ * @param accountId The account ID.
169
+ * @param signal Optional abort signal.
170
+ * @returns Map of lowercase email to identity ID.
171
+ * @since 0.4.0
172
+ */
173
+ private getIdentityMap;
165
174
  /**
166
175
  * Uploads all attachments and returns a map of contentId to blobId.
167
176
  * @param session The JMAP session.
@@ -179,6 +188,14 @@ declare class JmapTransport implements Transport {
179
188
  * @since 0.4.0
180
189
  */
181
190
  private parseResponse;
191
+ /**
192
+ * Parses the JMAP batch response to extract receipt for a specific index.
193
+ * @param response The JMAP response.
194
+ * @param index The message index in the batch.
195
+ * @returns A receipt indicating success or failure for that message.
196
+ * @since 0.4.0
197
+ */
198
+ private parseBatchResponseForIndex;
182
199
  }
183
200
  //#endregion
184
201
  //#region src/errors.d.ts
package/dist/index.js CHANGED
@@ -113,10 +113,10 @@ function createJmapConfig(config) {
113
113
  basicAuth: config.basicAuth ?? null,
114
114
  accountId: config.accountId ?? null,
115
115
  identityId: config.identityId ?? null,
116
- timeout: config.timeout || 3e4,
117
- retries: config.retries || 3,
116
+ timeout: config.timeout ?? 3e4,
117
+ retries: config.retries ?? 3,
118
118
  headers: config.headers ?? {},
119
- sessionCacheTtl: config.sessionCacheTtl || 3e5,
119
+ sessionCacheTtl: config.sessionCacheTtl ?? 3e5,
120
120
  baseUrl: config.baseUrl ?? null
121
121
  };
122
122
  }
@@ -206,12 +206,18 @@ var JmapHttpClient = class {
206
206
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
207
207
  let combinedSignal = controller.signal;
208
208
  if (signal) {
209
- if (signal.aborted) {
210
- clearTimeout(timeoutId);
211
- signal.throwIfAborted();
209
+ const AbortSignalAny = AbortSignal.any;
210
+ if (typeof AbortSignalAny === "function") combinedSignal = AbortSignalAny([signal, controller.signal]);
211
+ else {
212
+ if (signal.aborted) {
213
+ clearTimeout(timeoutId);
214
+ signal.throwIfAborted();
215
+ }
216
+ signal.addEventListener("abort", () => {
217
+ controller.abort(signal.reason);
218
+ });
219
+ combinedSignal = controller.signal;
212
220
  }
213
- signal.addEventListener("abort", () => controller.abort());
214
- combinedSignal = controller.signal;
215
221
  }
216
222
  try {
217
223
  return await globalThis.fetch(url, {
@@ -515,14 +521,96 @@ var JmapTransport = class {
515
521
  }
516
522
  }
517
523
  /**
518
- * Sends multiple messages sequentially.
524
+ * Sends multiple messages in a single batched JMAP request.
519
525
  * @param messages The messages to send.
520
526
  * @param options Optional transport options.
521
527
  * @yields Receipts for each message.
522
528
  * @since 0.4.0
523
529
  */
524
530
  async *sendMany(messages, options) {
525
- for await (const message of messages) yield await this.send(message, options);
531
+ const signal = options?.signal;
532
+ const messageArray = [];
533
+ for await (const message of messages) messageArray.push(message);
534
+ if (messageArray.length === 0) return;
535
+ let processingStage = "initialization";
536
+ let attachmentsUploadedCount = 0;
537
+ try {
538
+ signal?.throwIfAborted();
539
+ processingStage = "session fetch";
540
+ const session = await this.getSession(signal);
541
+ signal?.throwIfAborted();
542
+ processingStage = "account discovery";
543
+ const accountId = this.config.accountId ?? findMailAccount(session);
544
+ 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
+ };
549
+ return;
550
+ }
551
+ processingStage = "mailbox discovery";
552
+ const draftsMailboxId = await this.getDraftsMailboxId(session, accountId, signal);
553
+ signal?.throwIfAborted();
554
+ processingStage = "identity resolution";
555
+ const identityMap = await this.getIdentityMap(session, accountId, signal);
556
+ signal?.throwIfAborted();
557
+ processingStage = "attachment upload";
558
+ const allUploadedBlobs = /* @__PURE__ */ new Map();
559
+ for (let i = 0; i < messageArray.length; i++) {
560
+ const message = messageArray[i];
561
+ const uploadedBlobs = await this.uploadAttachments(session, accountId, message.attachments, signal);
562
+ allUploadedBlobs.set(i, uploadedBlobs);
563
+ attachmentsUploadedCount = i + 1;
564
+ signal?.throwIfAborted();
565
+ }
566
+ processingStage = "message conversion";
567
+ const emailCreates = {};
568
+ const submissionCreates = {};
569
+ for (let i = 0; i < messageArray.length; i++) {
570
+ const message = messageArray[i];
571
+ const uploadedBlobs = allUploadedBlobs.get(i);
572
+ const emailCreate = convertMessage(message, draftsMailboxId, uploadedBlobs);
573
+ const senderEmail = message.sender.address.toLowerCase();
574
+ const identityId = identityMap.get(senderEmail) ?? identityMap.values().next().value;
575
+ emailCreates[`draft${i}`] = emailCreate;
576
+ submissionCreates[`sub${i}`] = {
577
+ identityId,
578
+ emailId: `#draft${i}`
579
+ };
580
+ }
581
+ processingStage = "batch request execution";
582
+ const response = await this.httpClient.executeRequest(session.apiUrl, {
583
+ using: [
584
+ JMAP_CAPABILITIES.core,
585
+ JMAP_CAPABILITIES.mail,
586
+ JMAP_CAPABILITIES.submission
587
+ ],
588
+ methodCalls: [[
589
+ "Email/set",
590
+ {
591
+ accountId,
592
+ create: emailCreates
593
+ },
594
+ "c0"
595
+ ], [
596
+ "EmailSubmission/set",
597
+ {
598
+ accountId,
599
+ create: submissionCreates
600
+ },
601
+ "c1"
602
+ ]]
603
+ }, signal);
604
+ for (let i = 0; i < messageArray.length; i++) yield this.parseBatchResponseForIndex(response, i);
605
+ } catch (error) {
606
+ 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
+ let detailedMessage = `Failed during ${processingStage}: ${baseMessage}`;
608
+ 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
+ };
613
+ }
526
614
  }
527
615
  /**
528
616
  * Gets or refreshes the JMAP session.
@@ -608,6 +696,21 @@ var JmapTransport = class {
608
696
  */
609
697
  async getIdentityId(session, accountId, senderEmail, signal) {
610
698
  if (this.config.identityId) return this.config.identityId;
699
+ const identityMap = await this.getIdentityMap(session, accountId, signal);
700
+ const matching = identityMap.get(senderEmail.toLowerCase());
701
+ if (matching) return matching;
702
+ return identityMap.values().next().value;
703
+ }
704
+ /**
705
+ * Gets all identities and builds a map of email to identity ID.
706
+ * @param session The JMAP session.
707
+ * @param accountId The account ID.
708
+ * @param signal Optional abort signal.
709
+ * @returns Map of lowercase email to identity ID.
710
+ * @since 0.4.0
711
+ */
712
+ async getIdentityMap(session, accountId, signal) {
713
+ if (this.config.identityId) return new Map([["*", this.config.identityId]]);
611
714
  const response = await this.httpClient.executeRequest(session.apiUrl, {
612
715
  using: [JMAP_CAPABILITIES.core, JMAP_CAPABILITIES.submission],
613
716
  methodCalls: [[
@@ -620,9 +723,9 @@ var JmapTransport = class {
620
723
  if (!identityResponse) throw new JmapApiError("No Identity/get response received");
621
724
  const identities = identityResponse[1].list;
622
725
  if (!identities || identities.length === 0) throw new JmapApiError("No identities found");
623
- const matching = identities.find((i) => i.email.toLowerCase() === senderEmail.toLowerCase());
624
- if (matching) return matching.id;
625
- return identities[0].id;
726
+ const identityMap = /* @__PURE__ */ new Map();
727
+ for (const identity of identities) identityMap.set(identity.email.toLowerCase(), identity.id);
728
+ return identityMap;
626
729
  }
627
730
  /**
628
731
  * Uploads all attachments and returns a map of contentId to blobId.
@@ -673,6 +776,43 @@ var JmapTransport = class {
673
776
  errorMessages: errors
674
777
  };
675
778
  }
779
+ /**
780
+ * Parses the JMAP batch response to extract receipt for a specific index.
781
+ * @param response The JMAP response.
782
+ * @param index The message index in the batch.
783
+ * @returns A receipt indicating success or failure for that message.
784
+ * @since 0.4.0
785
+ */
786
+ parseBatchResponseForIndex(response, index) {
787
+ const errors = [];
788
+ const draftKey = `draft${index}`;
789
+ const subKey = `sub${index}`;
790
+ const emailResponse = response.methodResponses.find((r) => r[0] === "Email/set");
791
+ if (emailResponse) {
792
+ const emailResult = emailResponse[1];
793
+ if (emailResult.notCreated?.[draftKey]) {
794
+ const error = emailResult.notCreated[draftKey];
795
+ errors.push(`Email creation failed: ${error.type}${error.description ? ` - ${error.description}` : ""}`);
796
+ }
797
+ }
798
+ const submissionResponse = response.methodResponses.find((r) => r[0] === "EmailSubmission/set");
799
+ if (submissionResponse) {
800
+ const submissionResult = submissionResponse[1];
801
+ if (submissionResult.notCreated?.[subKey]) {
802
+ const error = submissionResult.notCreated[subKey];
803
+ errors.push(`Email submission failed: ${error.type}${error.description ? ` - ${error.description}` : ""}`);
804
+ }
805
+ if (submissionResult.created?.[subKey]) return {
806
+ successful: true,
807
+ messageId: submissionResult.created[subKey].id
808
+ };
809
+ }
810
+ if (errors.length === 0) errors.push("Unknown error: No submission result received");
811
+ return {
812
+ successful: false,
813
+ errorMessages: errors
814
+ };
815
+ }
676
816
  };
677
817
 
678
818
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/jmap",
3
- "version": "0.4.0-dev.73+8cd7bd3d",
3
+ "version": "0.4.0-dev.77+40049302",
4
4
  "description": "JMAP transport for Upyo email library",
5
5
  "keywords": [
6
6
  "email",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "sideEffects": false,
55
55
  "peerDependencies": {
56
- "@upyo/core": "0.4.0-dev.73+8cd7bd3d"
56
+ "@upyo/core": "0.4.0-dev.77+40049302"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@dotenvx/dotenvx": "^1.47.3",