@upyo/jmap 0.4.0-dev.72 → 0.4.0-dev.75

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
@@ -516,14 +516,84 @@ var JmapTransport = class {
516
516
  }
517
517
  }
518
518
  /**
519
- * Sends multiple messages sequentially.
519
+ * Sends multiple messages in a single batched JMAP request.
520
520
  * @param messages The messages to send.
521
521
  * @param options Optional transport options.
522
522
  * @yields Receipts for each message.
523
523
  * @since 0.4.0
524
524
  */
525
525
  async *sendMany(messages, options) {
526
- for await (const message of messages) yield await this.send(message, options);
526
+ const signal = options?.signal;
527
+ const messageArray = [];
528
+ for await (const message of messages) messageArray.push(message);
529
+ if (messageArray.length === 0) return;
530
+ try {
531
+ signal?.throwIfAborted();
532
+ const session = await this.getSession(signal);
533
+ signal?.throwIfAborted();
534
+ const accountId = this.config.accountId ?? findMailAccount(session);
535
+ if (!accountId) {
536
+ for (let i = 0; i < messageArray.length; i++) yield {
537
+ successful: false,
538
+ errorMessages: ["No mail-capable account found in JMAP session"]
539
+ };
540
+ return;
541
+ }
542
+ const draftsMailboxId = await this.getDraftsMailboxId(session, accountId, signal);
543
+ signal?.throwIfAborted();
544
+ const identityMap = await this.getIdentityMap(session, accountId, signal);
545
+ signal?.throwIfAborted();
546
+ const allUploadedBlobs = /* @__PURE__ */ new Map();
547
+ for (let i = 0; i < messageArray.length; i++) {
548
+ const message = messageArray[i];
549
+ const uploadedBlobs = await this.uploadAttachments(session, accountId, message.attachments, signal);
550
+ allUploadedBlobs.set(i, uploadedBlobs);
551
+ signal?.throwIfAborted();
552
+ }
553
+ const emailCreates = {};
554
+ const submissionCreates = {};
555
+ for (let i = 0; i < messageArray.length; i++) {
556
+ const message = messageArray[i];
557
+ const uploadedBlobs = allUploadedBlobs.get(i);
558
+ const emailCreate = convertMessage(message, draftsMailboxId, uploadedBlobs);
559
+ const senderEmail = message.sender.address.toLowerCase();
560
+ const identityId = identityMap.get(senderEmail) ?? identityMap.values().next().value;
561
+ emailCreates[`draft${i}`] = emailCreate;
562
+ submissionCreates[`sub${i}`] = {
563
+ identityId,
564
+ emailId: `#draft${i}`
565
+ };
566
+ }
567
+ const response = await this.httpClient.executeRequest(session.apiUrl, {
568
+ using: [
569
+ JMAP_CAPABILITIES.core,
570
+ JMAP_CAPABILITIES.mail,
571
+ JMAP_CAPABILITIES.submission
572
+ ],
573
+ methodCalls: [[
574
+ "Email/set",
575
+ {
576
+ accountId,
577
+ create: emailCreates
578
+ },
579
+ "c0"
580
+ ], [
581
+ "EmailSubmission/set",
582
+ {
583
+ accountId,
584
+ create: submissionCreates
585
+ },
586
+ "c1"
587
+ ]]
588
+ }, signal);
589
+ for (let i = 0; i < messageArray.length; i++) yield this.parseBatchResponseForIndex(response, i);
590
+ } catch (error) {
591
+ const errorMessage = error instanceof Error && error.name === "AbortError" ? `Request aborted: ${error.message}` : error instanceof JmapApiError ? error.message : error instanceof Error ? error.message : String(error);
592
+ for (let i = 0; i < messageArray.length; i++) yield {
593
+ successful: false,
594
+ errorMessages: [errorMessage]
595
+ };
596
+ }
527
597
  }
528
598
  /**
529
599
  * Gets or refreshes the JMAP session.
@@ -609,6 +679,21 @@ var JmapTransport = class {
609
679
  */
610
680
  async getIdentityId(session, accountId, senderEmail, signal) {
611
681
  if (this.config.identityId) return this.config.identityId;
682
+ const identityMap = await this.getIdentityMap(session, accountId, signal);
683
+ const matching = identityMap.get(senderEmail.toLowerCase());
684
+ if (matching) return matching;
685
+ return identityMap.values().next().value;
686
+ }
687
+ /**
688
+ * Gets all identities and builds a map of email to identity ID.
689
+ * @param session The JMAP session.
690
+ * @param accountId The account ID.
691
+ * @param signal Optional abort signal.
692
+ * @returns Map of lowercase email to identity ID.
693
+ * @since 0.4.0
694
+ */
695
+ async getIdentityMap(session, accountId, signal) {
696
+ if (this.config.identityId) return new Map([["*", this.config.identityId]]);
612
697
  const response = await this.httpClient.executeRequest(session.apiUrl, {
613
698
  using: [JMAP_CAPABILITIES.core, JMAP_CAPABILITIES.submission],
614
699
  methodCalls: [[
@@ -621,9 +706,9 @@ var JmapTransport = class {
621
706
  if (!identityResponse) throw new JmapApiError("No Identity/get response received");
622
707
  const identities = identityResponse[1].list;
623
708
  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;
709
+ const identityMap = /* @__PURE__ */ new Map();
710
+ for (const identity of identities) identityMap.set(identity.email.toLowerCase(), identity.id);
711
+ return identityMap;
627
712
  }
628
713
  /**
629
714
  * Uploads all attachments and returns a map of contentId to blobId.
@@ -674,6 +759,43 @@ var JmapTransport = class {
674
759
  errorMessages: errors
675
760
  };
676
761
  }
762
+ /**
763
+ * Parses the JMAP batch response to extract receipt for a specific index.
764
+ * @param response The JMAP response.
765
+ * @param index The message index in the batch.
766
+ * @returns A receipt indicating success or failure for that message.
767
+ * @since 0.4.0
768
+ */
769
+ parseBatchResponseForIndex(response, index) {
770
+ const errors = [];
771
+ const draftKey = `draft${index}`;
772
+ const subKey = `sub${index}`;
773
+ const emailResponse = response.methodResponses.find((r) => r[0] === "Email/set");
774
+ if (emailResponse) {
775
+ const emailResult = emailResponse[1];
776
+ if (emailResult.notCreated?.[draftKey]) {
777
+ const error = emailResult.notCreated[draftKey];
778
+ errors.push(`Email creation failed: ${error.type}${error.description ? ` - ${error.description}` : ""}`);
779
+ }
780
+ }
781
+ const submissionResponse = response.methodResponses.find((r) => r[0] === "EmailSubmission/set");
782
+ if (submissionResponse) {
783
+ const submissionResult = submissionResponse[1];
784
+ if (submissionResult.notCreated?.[subKey]) {
785
+ const error = submissionResult.notCreated[subKey];
786
+ errors.push(`Email submission failed: ${error.type}${error.description ? ` - ${error.description}` : ""}`);
787
+ }
788
+ if (submissionResult.created?.[subKey]) return {
789
+ successful: true,
790
+ messageId: submissionResult.created[subKey].id
791
+ };
792
+ }
793
+ if (errors.length === 0) errors.push("Unknown error: No submission result received");
794
+ return {
795
+ successful: false,
796
+ errorMessages: errors
797
+ };
798
+ }
677
799
  };
678
800
 
679
801
  //#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
@@ -515,14 +515,84 @@ var JmapTransport = class {
515
515
  }
516
516
  }
517
517
  /**
518
- * Sends multiple messages sequentially.
518
+ * Sends multiple messages in a single batched JMAP request.
519
519
  * @param messages The messages to send.
520
520
  * @param options Optional transport options.
521
521
  * @yields Receipts for each message.
522
522
  * @since 0.4.0
523
523
  */
524
524
  async *sendMany(messages, options) {
525
- for await (const message of messages) yield await this.send(message, options);
525
+ const signal = options?.signal;
526
+ const messageArray = [];
527
+ for await (const message of messages) messageArray.push(message);
528
+ if (messageArray.length === 0) return;
529
+ try {
530
+ signal?.throwIfAborted();
531
+ const session = await this.getSession(signal);
532
+ signal?.throwIfAborted();
533
+ const accountId = this.config.accountId ?? findMailAccount(session);
534
+ if (!accountId) {
535
+ for (let i = 0; i < messageArray.length; i++) yield {
536
+ successful: false,
537
+ errorMessages: ["No mail-capable account found in JMAP session"]
538
+ };
539
+ return;
540
+ }
541
+ const draftsMailboxId = await this.getDraftsMailboxId(session, accountId, signal);
542
+ signal?.throwIfAborted();
543
+ const identityMap = await this.getIdentityMap(session, accountId, signal);
544
+ signal?.throwIfAborted();
545
+ const allUploadedBlobs = /* @__PURE__ */ new Map();
546
+ for (let i = 0; i < messageArray.length; i++) {
547
+ const message = messageArray[i];
548
+ const uploadedBlobs = await this.uploadAttachments(session, accountId, message.attachments, signal);
549
+ allUploadedBlobs.set(i, uploadedBlobs);
550
+ signal?.throwIfAborted();
551
+ }
552
+ const emailCreates = {};
553
+ const submissionCreates = {};
554
+ for (let i = 0; i < messageArray.length; i++) {
555
+ const message = messageArray[i];
556
+ const uploadedBlobs = allUploadedBlobs.get(i);
557
+ const emailCreate = convertMessage(message, draftsMailboxId, uploadedBlobs);
558
+ const senderEmail = message.sender.address.toLowerCase();
559
+ const identityId = identityMap.get(senderEmail) ?? identityMap.values().next().value;
560
+ emailCreates[`draft${i}`] = emailCreate;
561
+ submissionCreates[`sub${i}`] = {
562
+ identityId,
563
+ emailId: `#draft${i}`
564
+ };
565
+ }
566
+ const response = await this.httpClient.executeRequest(session.apiUrl, {
567
+ using: [
568
+ JMAP_CAPABILITIES.core,
569
+ JMAP_CAPABILITIES.mail,
570
+ JMAP_CAPABILITIES.submission
571
+ ],
572
+ methodCalls: [[
573
+ "Email/set",
574
+ {
575
+ accountId,
576
+ create: emailCreates
577
+ },
578
+ "c0"
579
+ ], [
580
+ "EmailSubmission/set",
581
+ {
582
+ accountId,
583
+ create: submissionCreates
584
+ },
585
+ "c1"
586
+ ]]
587
+ }, signal);
588
+ for (let i = 0; i < messageArray.length; i++) yield this.parseBatchResponseForIndex(response, i);
589
+ } catch (error) {
590
+ const errorMessage = error instanceof Error && error.name === "AbortError" ? `Request aborted: ${error.message}` : error instanceof JmapApiError ? error.message : error instanceof Error ? error.message : String(error);
591
+ for (let i = 0; i < messageArray.length; i++) yield {
592
+ successful: false,
593
+ errorMessages: [errorMessage]
594
+ };
595
+ }
526
596
  }
527
597
  /**
528
598
  * Gets or refreshes the JMAP session.
@@ -608,6 +678,21 @@ var JmapTransport = class {
608
678
  */
609
679
  async getIdentityId(session, accountId, senderEmail, signal) {
610
680
  if (this.config.identityId) return this.config.identityId;
681
+ const identityMap = await this.getIdentityMap(session, accountId, signal);
682
+ const matching = identityMap.get(senderEmail.toLowerCase());
683
+ if (matching) return matching;
684
+ return identityMap.values().next().value;
685
+ }
686
+ /**
687
+ * Gets all identities and builds a map of email to identity ID.
688
+ * @param session The JMAP session.
689
+ * @param accountId The account ID.
690
+ * @param signal Optional abort signal.
691
+ * @returns Map of lowercase email to identity ID.
692
+ * @since 0.4.0
693
+ */
694
+ async getIdentityMap(session, accountId, signal) {
695
+ if (this.config.identityId) return new Map([["*", this.config.identityId]]);
611
696
  const response = await this.httpClient.executeRequest(session.apiUrl, {
612
697
  using: [JMAP_CAPABILITIES.core, JMAP_CAPABILITIES.submission],
613
698
  methodCalls: [[
@@ -620,9 +705,9 @@ var JmapTransport = class {
620
705
  if (!identityResponse) throw new JmapApiError("No Identity/get response received");
621
706
  const identities = identityResponse[1].list;
622
707
  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;
708
+ const identityMap = /* @__PURE__ */ new Map();
709
+ for (const identity of identities) identityMap.set(identity.email.toLowerCase(), identity.id);
710
+ return identityMap;
626
711
  }
627
712
  /**
628
713
  * Uploads all attachments and returns a map of contentId to blobId.
@@ -673,6 +758,43 @@ var JmapTransport = class {
673
758
  errorMessages: errors
674
759
  };
675
760
  }
761
+ /**
762
+ * Parses the JMAP batch response to extract receipt for a specific index.
763
+ * @param response The JMAP response.
764
+ * @param index The message index in the batch.
765
+ * @returns A receipt indicating success or failure for that message.
766
+ * @since 0.4.0
767
+ */
768
+ parseBatchResponseForIndex(response, index) {
769
+ const errors = [];
770
+ const draftKey = `draft${index}`;
771
+ const subKey = `sub${index}`;
772
+ const emailResponse = response.methodResponses.find((r) => r[0] === "Email/set");
773
+ if (emailResponse) {
774
+ const emailResult = emailResponse[1];
775
+ if (emailResult.notCreated?.[draftKey]) {
776
+ const error = emailResult.notCreated[draftKey];
777
+ errors.push(`Email creation failed: ${error.type}${error.description ? ` - ${error.description}` : ""}`);
778
+ }
779
+ }
780
+ const submissionResponse = response.methodResponses.find((r) => r[0] === "EmailSubmission/set");
781
+ if (submissionResponse) {
782
+ const submissionResult = submissionResponse[1];
783
+ if (submissionResult.notCreated?.[subKey]) {
784
+ const error = submissionResult.notCreated[subKey];
785
+ errors.push(`Email submission failed: ${error.type}${error.description ? ` - ${error.description}` : ""}`);
786
+ }
787
+ if (submissionResult.created?.[subKey]) return {
788
+ successful: true,
789
+ messageId: submissionResult.created[subKey].id
790
+ };
791
+ }
792
+ if (errors.length === 0) errors.push("Unknown error: No submission result received");
793
+ return {
794
+ successful: false,
795
+ errorMessages: errors
796
+ };
797
+ }
676
798
  };
677
799
 
678
800
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/jmap",
3
- "version": "0.4.0-dev.72+26b27bbe",
3
+ "version": "0.4.0-dev.75+4fff8d51",
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.72+26b27bbe"
56
+ "@upyo/core": "0.4.0-dev.75+4fff8d51"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@dotenvx/dotenvx": "^1.47.3",