hypermail-mcp 0.4.1 → 0.4.2

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/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
7
- import { randomUUID as randomUUID2 } from "crypto";
7
+ import { randomUUID as randomUUID6 } from "crypto";
8
8
  import { createServer as createHttpServer } from "http";
9
9
 
10
10
  // src/store/account-store.ts
@@ -159,12 +159,31 @@ async function tryKeytarSet(key) {
159
159
  }
160
160
 
161
161
  // src/providers/outlook/index.ts
162
- import { randomUUID } from "crypto";
162
+ import { randomUUID as randomUUID2 } from "crypto";
163
163
  import { writeFileSync } from "fs";
164
164
  import { tmpdir } from "os";
165
165
  import { join as pathJoin } from "path";
166
166
  import { ResponseType } from "@microsoft/microsoft-graph-client";
167
167
 
168
+ // src/providers/shared/inline-images.ts
169
+ import { randomUUID } from "crypto";
170
+ function parseInlineImages(html) {
171
+ const images = [];
172
+ const re = /src="data:image\/([\w+]+);base64,([^"]+)"/gi;
173
+ const transformed = html.replace(re, (_fullMatch, mimeSubtype, b64) => {
174
+ const contentId = `sig-img-${randomUUID()}`;
175
+ const ext = mimeSubtype.toLowerCase().replace(/\+/g, "-") === "svg-xml" ? "svg" : mimeSubtype.toLowerCase().replace(/\+/g, "-");
176
+ images.push({
177
+ cid: contentId,
178
+ contentBytes: b64,
179
+ contentType: `image/${mimeSubtype}`,
180
+ filename: `signature-image.${ext}`
181
+ });
182
+ return `src="cid:${contentId}"`;
183
+ });
184
+ return { body: transformed, images };
185
+ }
186
+
168
187
  // src/providers/outlook/client.ts
169
188
  import "isomorphic-fetch";
170
189
  import {
@@ -182,6 +201,11 @@ var DEFAULT_SCOPES = [
182
201
  "Mail.ReadWrite",
183
202
  "Mail.Send"
184
203
  ];
204
+ function isSerializedTokens(obj) {
205
+ if (typeof obj !== "object" || obj === null) return false;
206
+ const o = obj;
207
+ return typeof o.msalCache === "string" && typeof o.homeAccountId === "string" && typeof o.tenantId === "string" && typeof o.username === "string" && Array.isArray(o.scopes);
208
+ }
185
209
  function makeConfig(prevCacheJson, clientIdOverride, tenantOverride) {
186
210
  const clientId = clientIdOverride || process.env.MS_CLIENT_ID || DEFAULT_CLIENT_ID;
187
211
  const tenant = tenantOverride || process.env.MS_TENANT_ID || "common";
@@ -322,6 +346,11 @@ var OutlookClientFactory = class {
322
346
  const provider = {
323
347
  getAccessToken: async () => {
324
348
  const fresh = store.getAccount(account.email) ?? account;
349
+ if (!isSerializedTokens(fresh.tokens)) {
350
+ throw new Error(
351
+ "Outlook account tokens are missing or corrupted \u2014 re-run add_account"
352
+ );
353
+ }
325
354
  const tokens = fresh.tokens;
326
355
  const { accessToken, tokens: nextTokens } = await acquireAccessToken(
327
356
  tokens,
@@ -351,21 +380,15 @@ var OutlookClientFactory = class {
351
380
 
352
381
  // src/providers/outlook/index.ts
353
382
  function convertInlineImages(body) {
354
- const attachments = [];
355
- const re = /src="data:image\/([\w+]+);base64,([^"]+)"/gi;
356
- const transformed = body.replace(re, (_fullMatch, mimeSubtype, b64) => {
357
- const contentId = `sig-img-${randomUUID()}`;
358
- const ext = mimeSubtype.toLowerCase().replace(/\+/g, "-") === "svg-xml" ? "svg" : mimeSubtype.toLowerCase().replace(/\+/g, "-");
359
- attachments.push({
360
- "@odata.type": "#microsoft.graph.fileAttachment",
361
- name: `signature-image.${ext}`,
362
- contentType: `image/${mimeSubtype}`,
363
- contentId,
364
- contentBytes: b64,
365
- isInline: true
366
- });
367
- return `src="cid:${contentId}"`;
368
- });
383
+ const { body: transformed, images } = parseInlineImages(body);
384
+ const attachments = images.map((img) => ({
385
+ "@odata.type": "#microsoft.graph.fileAttachment",
386
+ name: img.filename,
387
+ contentType: img.contentType,
388
+ contentId: img.cid,
389
+ contentBytes: img.contentBytes,
390
+ isInline: true
391
+ }));
369
392
  return { body: transformed, attachments };
370
393
  }
371
394
  var OutlookProvider = class {
@@ -385,7 +408,7 @@ var OutlookProvider = class {
385
408
  async addAccount(input) {
386
409
  const begin = beginDeviceCode(void 0, this.clientId, this.tenantId);
387
410
  await awaitDeviceCodeReady(begin);
388
- const handle = randomUUID();
411
+ const handle = randomUUID2();
389
412
  const flow = {
390
413
  begin,
391
414
  emailHint: input.email,
@@ -559,83 +582,25 @@ var OutlookProvider = class {
559
582
  }
560
583
  return draft.id;
561
584
  }
562
- async sendEmail(account, msg) {
563
- const client = this.clients.get(account);
564
- if (msg.inReplyTo && msg.forwardMessageId) {
565
- throw new Error(
566
- "inReplyTo and forwardMessageId are mutually exclusive \u2014 use one or the other"
567
- );
568
- }
569
- const converted = convertInlineImages(msg.body);
570
- if (msg.forwardMessageId) {
571
- const draftId = await this.buildDraftFromReference(
572
- client,
573
- `/me/messages/${encodeURIComponent(msg.forwardMessageId)}/createForward`,
574
- {
575
- message: {
576
- toRecipients: msg.to.map(toRecipient),
577
- ccRecipients: (msg.cc ?? []).map(toRecipient),
578
- bccRecipients: (msg.bcc ?? []).map(toRecipient)
579
- },
580
- comment: ""
581
- },
582
- converted
583
- );
584
- await client.api(`/me/messages/${draftId}/send`).post({});
585
- return { id: draftId };
586
- }
587
- if (msg.inReplyTo) {
588
- const createEndpoint = msg.replyAll ? `/me/messages/${encodeURIComponent(msg.inReplyTo)}/createReplyAll` : `/me/messages/${encodeURIComponent(msg.inReplyTo)}/createReply`;
589
- const draftId = await this.buildDraftFromReference(
590
- client,
591
- createEndpoint,
592
- {},
593
- converted
594
- );
595
- await client.api(`/me/messages/${draftId}/send`).post({});
596
- return { id: draftId };
597
- }
598
- const payload = {
599
- message: {
600
- subject: msg.subject,
601
- body: {
602
- contentType: msg.isHtml ? "HTML" : "Text",
603
- content: converted.body
604
- },
605
- toRecipients: msg.to.map(toRecipient),
606
- ccRecipients: (msg.cc ?? []).map(toRecipient),
607
- bccRecipients: (msg.bcc ?? []).map(toRecipient)
608
- },
609
- saveToSentItems: true
610
- };
611
- if (converted.attachments.length > 0) {
612
- payload.message.attachments = converted.attachments;
613
- }
614
- await client.api("/me/sendMail").post(payload);
615
- return { id: "" };
616
- }
617
- async saveDraft(account, msg) {
585
+ // Shared backend for sendEmail and saveDraft — handles forward, reply, and
586
+ // new-message paths. The `mode` controls whether the message is sent
587
+ // immediately or saved as a draft.
588
+ async sendOrSave(account, msg, mode) {
618
589
  const client = this.clients.get(account);
619
- if (msg.inReplyTo && msg.forwardMessageId) {
620
- throw new Error(
621
- "inReplyTo and forwardMessageId are mutually exclusive \u2014 use one or the other"
622
- );
623
- }
624
590
  const converted = convertInlineImages(msg.body);
591
+ const toRecipients = msg.to.map(toRecipient);
592
+ const ccRecipients = (msg.cc ?? []).map(toRecipient);
593
+ const bccRecipients = (msg.bcc ?? []).map(toRecipient);
625
594
  if (msg.forwardMessageId) {
626
595
  const draftId = await this.buildDraftFromReference(
627
596
  client,
628
597
  `/me/messages/${encodeURIComponent(msg.forwardMessageId)}/createForward`,
629
- {
630
- message: {
631
- toRecipients: msg.to.map(toRecipient),
632
- ccRecipients: (msg.cc ?? []).map(toRecipient),
633
- bccRecipients: (msg.bcc ?? []).map(toRecipient)
634
- },
635
- comment: ""
636
- },
598
+ { message: { toRecipients, ccRecipients, bccRecipients }, comment: "" },
637
599
  converted
638
600
  );
601
+ if (mode === "send") {
602
+ await client.api(`/me/messages/${draftId}/send`).post({});
603
+ }
639
604
  return { id: draftId };
640
605
  }
641
606
  if (msg.inReplyTo) {
@@ -646,202 +611,1812 @@ var OutlookProvider = class {
646
611
  {},
647
612
  converted
648
613
  );
614
+ if (mode === "send") {
615
+ await client.api(`/me/messages/${draftId}/send`).post({});
616
+ }
649
617
  return { id: draftId };
650
618
  }
651
- const draftPayload = {
619
+ const messagePayload = {
652
620
  subject: msg.subject,
653
621
  body: {
654
622
  contentType: msg.isHtml ? "HTML" : "Text",
655
623
  content: converted.body
656
624
  },
657
- toRecipients: msg.to.map(toRecipient),
658
- ccRecipients: (msg.cc ?? []).map(toRecipient),
659
- bccRecipients: (msg.bcc ?? []).map(toRecipient)
625
+ toRecipients,
626
+ ccRecipients,
627
+ bccRecipients
660
628
  };
661
629
  if (converted.attachments.length > 0) {
662
- draftPayload.attachments = converted.attachments;
630
+ messagePayload.attachments = converted.attachments;
631
+ }
632
+ if (mode === "send") {
633
+ await client.api("/me/sendMail").post({
634
+ message: messagePayload,
635
+ saveToSentItems: true
636
+ });
637
+ return { id: "" };
663
638
  }
664
- const draft = await client.api("/me/messages").post(draftPayload);
639
+ const draft = await client.api("/me/messages").post(messagePayload);
665
640
  return { id: draft.id };
666
641
  }
642
+ async sendEmail(account, msg) {
643
+ return this.sendOrSave(account, msg, "send");
644
+ }
645
+ async saveDraft(account, msg) {
646
+ return this.sendOrSave(account, msg, "draft");
647
+ }
667
648
  async updateDraft(account, id, update) {
668
649
  const client = this.clients.get(account);
669
650
  const payload = {};
670
651
  if (update.subject !== void 0) {
671
652
  payload.subject = update.subject;
672
653
  }
673
- if (update.to !== void 0) {
674
- payload.toRecipients = update.to.map(toRecipient);
654
+ if (update.to !== void 0) {
655
+ payload.toRecipients = update.to.map(toRecipient);
656
+ }
657
+ if (update.cc !== void 0) {
658
+ payload.ccRecipients = update.cc.map(toRecipient);
659
+ }
660
+ if (update.bcc !== void 0) {
661
+ payload.bccRecipients = update.bcc.map(toRecipient);
662
+ }
663
+ if (update.body !== void 0) {
664
+ const converted = convertInlineImages(update.body);
665
+ payload.body = {
666
+ contentType: update.isHtml ? "HTML" : "Text",
667
+ content: converted.body
668
+ };
669
+ if (converted.attachments.length > 0) {
670
+ payload.attachments = converted.attachments;
671
+ }
672
+ }
673
+ await client.api(`/me/messages/${encodeURIComponent(id)}`).patch(payload);
674
+ return { id };
675
+ }
676
+ async moveEmail(account, id, destinationId) {
677
+ const client = this.clients.get(account);
678
+ await client.api(`/me/messages/${encodeURIComponent(id)}/move`).post({ destinationId });
679
+ }
680
+ async sendDraft(account, id) {
681
+ const client = this.clients.get(account);
682
+ await client.api(`/me/messages/${encodeURIComponent(id)}/send`).post({});
683
+ return { id };
684
+ }
685
+ async addAttachmentToDraft(account, draftId, name, contentBytes, contentType) {
686
+ const client = this.clients.get(account);
687
+ const att = await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post({
688
+ "@odata.type": "#microsoft.graph.fileAttachment",
689
+ name,
690
+ contentType: contentType ?? "application/octet-stream",
691
+ contentBytes
692
+ });
693
+ return {
694
+ id: draftId,
695
+ attachment: { id: att.id, name: att.name, contentType: att.contentType }
696
+ };
697
+ }
698
+ async markRead(account, id, isRead) {
699
+ const client = this.clients.get(account);
700
+ await client.api(`/me/messages/${encodeURIComponent(id)}`).patch({ isRead });
701
+ }
702
+ async listFolders(account, opts) {
703
+ const client = this.clients.get(account);
704
+ const endpoint = opts.parentFolderId ? `/me/mailFolders/${encodeURIComponent(opts.parentFolderId)}/childFolders` : "/me/mailFolders";
705
+ const res = await client.api(endpoint).select([
706
+ "id",
707
+ "displayName",
708
+ "parentFolderId",
709
+ "childFolderCount",
710
+ "totalItemCount",
711
+ "unreadItemCount"
712
+ ].join(",")).get();
713
+ return (res.value ?? []).map(mapFolder);
714
+ }
715
+ async createFolder(account, input) {
716
+ const client = this.clients.get(account);
717
+ const parentId = input.parentFolderId ?? "msgfolderroot";
718
+ const created = await client.api(`/me/mailFolders/${encodeURIComponent(parentId)}/childFolders`).post({ displayName: input.displayName });
719
+ return mapFolder(created);
720
+ }
721
+ async renameFolder(account, folderId, newName) {
722
+ const client = this.clients.get(account);
723
+ const updated = await client.api(`/me/mailFolders/${encodeURIComponent(folderId)}`).patch({ displayName: newName });
724
+ return mapFolder(updated);
725
+ }
726
+ async deleteFolder(account, folderId) {
727
+ const client = this.clients.get(account);
728
+ await client.api(`/me/mailFolders/${encodeURIComponent(folderId)}`).delete();
729
+ }
730
+ };
731
+ function mapFolder(f) {
732
+ return {
733
+ id: f.id,
734
+ displayName: f.displayName,
735
+ parentFolderId: f.parentFolderId,
736
+ childFolderCount: f.childFolderCount,
737
+ totalItemCount: f.totalItemCount,
738
+ unreadItemCount: f.unreadItemCount
739
+ };
740
+ }
741
+ function mapRecipient(r) {
742
+ return {
743
+ name: r.emailAddress?.name,
744
+ address: r.emailAddress?.address ?? ""
745
+ };
746
+ }
747
+ function mapSummary(m, folder) {
748
+ return {
749
+ id: m.id,
750
+ subject: m.subject ?? "",
751
+ from: m.from ? mapRecipient(m.from) : void 0,
752
+ to: (m.toRecipients ?? []).map(mapRecipient),
753
+ receivedAt: m.receivedDateTime,
754
+ preview: m.bodyPreview,
755
+ isRead: m.isRead,
756
+ hasAttachments: m.hasAttachments,
757
+ folder
758
+ };
759
+ }
760
+ function toRecipient(a) {
761
+ return { emailAddress: { name: a.name, address: a.address } };
762
+ }
763
+ function clampLimit(v, dflt, max) {
764
+ if (!v || v <= 0) return dflt;
765
+ return Math.min(v, max);
766
+ }
767
+
768
+ // src/providers/imap/client.ts
769
+ import { ImapFlow } from "imapflow";
770
+ import nodemailer from "nodemailer";
771
+ function isImapTokens(obj) {
772
+ if (typeof obj !== "object" || obj === null) return false;
773
+ const o = obj;
774
+ return typeof o.host === "string" && typeof o.port === "number" && typeof o.user === "string" && typeof o.password === "string" && typeof o.smtpHost === "string" && typeof o.smtpPort === "number";
775
+ }
776
+ function extractTokens(account) {
777
+ if (!isImapTokens(account.tokens)) {
778
+ throw new Error(
779
+ "IMAP account tokens are missing or corrupted \u2014 re-run add_account"
780
+ );
781
+ }
782
+ return account.tokens;
783
+ }
784
+ var ImapClient = class {
785
+ constructor(tokens) {
786
+ this.tokens = tokens;
787
+ }
788
+ tokens;
789
+ imap = null;
790
+ transporter = null;
791
+ connecting = null;
792
+ /** Get (or create) the ImapFlow instance. */
793
+ async getImap() {
794
+ if (this.imap) return this.imap;
795
+ this.imap = new ImapFlow({
796
+ host: this.tokens.host,
797
+ port: this.tokens.port,
798
+ secure: this.tokens.secure,
799
+ auth: {
800
+ user: this.tokens.user,
801
+ pass: this.tokens.password
802
+ },
803
+ logger: false
804
+ });
805
+ if (!this.connecting) {
806
+ this.connecting = this.imap.connect().catch((err) => {
807
+ this.imap = null;
808
+ this.connecting = null;
809
+ throw err;
810
+ });
811
+ }
812
+ await this.connecting;
813
+ return this.imap;
814
+ }
815
+ /** Get (or create) a nodemailer SMTP transporter. */
816
+ getTransporter() {
817
+ if (this.transporter) return this.transporter;
818
+ this.transporter = nodemailer.createTransport({
819
+ host: this.tokens.smtpHost,
820
+ port: this.tokens.smtpPort,
821
+ secure: this.tokens.smtpSecure,
822
+ auth: {
823
+ user: this.tokens.user,
824
+ pass: this.tokens.password
825
+ }
826
+ });
827
+ return this.transporter;
828
+ }
829
+ /**
830
+ * Acquire a mailbox lock and run `fn` with the mailbox selected.
831
+ * Releases the lock automatically after `fn` completes.
832
+ */
833
+ async withMailbox(mailbox, fn) {
834
+ const imap = await this.getImap();
835
+ const lock = await imap.getMailboxLock(mailbox);
836
+ try {
837
+ return await fn(imap);
838
+ } finally {
839
+ lock.release();
840
+ }
841
+ }
842
+ /** Disconnect IMAP and close the SMTP pool. */
843
+ async disconnect() {
844
+ if (this.imap) {
845
+ try {
846
+ await this.imap.logout();
847
+ } catch {
848
+ }
849
+ this.imap = null;
850
+ this.connecting = null;
851
+ }
852
+ if (this.transporter) {
853
+ this.transporter.close();
854
+ this.transporter = null;
855
+ }
856
+ }
857
+ };
858
+ var ImapClientFactory = class {
859
+ cache = /* @__PURE__ */ new Map();
860
+ get(account) {
861
+ const key = account.email.toLowerCase();
862
+ const existing = this.cache.get(key);
863
+ if (existing) return existing;
864
+ const tokens = extractTokens(account);
865
+ const client = new ImapClient(tokens);
866
+ this.cache.set(key, client);
867
+ return client;
868
+ }
869
+ /** Drop a cached client (e.g. after removeAccount). */
870
+ invalidate(email) {
871
+ const key = email.toLowerCase();
872
+ const existing = this.cache.get(key);
873
+ if (existing) {
874
+ existing.disconnect().catch(() => {
875
+ });
876
+ this.cache.delete(key);
877
+ }
878
+ }
879
+ };
880
+
881
+ // src/providers/imap/read-ops.ts
882
+ import { createWriteStream } from "fs";
883
+ import { tmpdir as tmpdir2 } from "os";
884
+ import { join as pathJoin2 } from "path";
885
+ import { pipeline } from "stream/promises";
886
+
887
+ // src/providers/imap/helpers.ts
888
+ var WELL_KNOWN_TO_IMAP = {
889
+ archive: "Archive",
890
+ deleteditems: "Trash",
891
+ inbox: "INBOX",
892
+ drafts: "Drafts",
893
+ sentitems: "Sent",
894
+ junkemail: "Junk",
895
+ outbox: "Outbox"
896
+ };
897
+ function resolveFolder(wellKnownOrPath) {
898
+ return WELL_KNOWN_TO_IMAP[wellKnownOrPath.toLowerCase()] ?? wellKnownOrPath;
899
+ }
900
+ function clampLimit2(v, dflt, max) {
901
+ if (!v || v <= 0) return dflt;
902
+ return Math.min(v, max);
903
+ }
904
+ function encodeId(folder, uid) {
905
+ return `${folder}/${uid}`;
906
+ }
907
+ function decodeId(id) {
908
+ const idx = id.lastIndexOf("/");
909
+ if (idx === -1) throw new Error(`invalid message ID: ${id}`);
910
+ const folder = id.slice(0, idx);
911
+ const uid = Number(id.slice(idx + 1));
912
+ if (!Number.isFinite(uid) || uid <= 0) {
913
+ throw new Error(`invalid message UID in ID: ${id}`);
914
+ }
915
+ return { folder, uid };
916
+ }
917
+ function findAttachments(node) {
918
+ const attachments = [];
919
+ const topType = (node.type ?? "").split("/")[0];
920
+ const isAttachment = node.disposition === "attachment" || !!node.type && topType !== "text" && topType !== "multipart" && !node.disposition;
921
+ if (isAttachment) {
922
+ attachments.push({
923
+ part: node.part ?? "1",
924
+ name: node.dispositionParameters?.filename ?? node.parameters?.name ?? "attachment",
925
+ contentType: node.type,
926
+ size: node.size
927
+ });
928
+ }
929
+ if (node.childNodes) {
930
+ for (const child of node.childNodes) {
931
+ attachments.push(...findAttachments(child));
932
+ }
933
+ }
934
+ return attachments;
935
+ }
936
+ function findPartByType(node, contentType) {
937
+ if (node.type === contentType) return node.part ?? "1";
938
+ if (node.childNodes) {
939
+ for (const child of node.childNodes) {
940
+ const found = findPartByType(child, contentType);
941
+ if (found) return found;
942
+ }
943
+ }
944
+ return void 0;
945
+ }
946
+ function mapEnvelopeAddr(a) {
947
+ return { name: a.name, address: a.address ?? "" };
948
+ }
949
+ function mapSummary2(uid, folder, envelope, flags = /* @__PURE__ */ new Set()) {
950
+ const fromAddr = envelope.from && envelope.from.length > 0 && envelope.from[0] ? mapEnvelopeAddr(envelope.from[0]) : void 0;
951
+ return {
952
+ id: encodeId(folder, uid),
953
+ subject: envelope.subject ?? "",
954
+ from: fromAddr,
955
+ to: (envelope.to ?? []).map(mapEnvelopeAddr),
956
+ receivedAt: envelope.date ? envelope.date.toISOString() : void 0,
957
+ isRead: flags.has("\\Seen"),
958
+ folder
959
+ };
960
+ }
961
+ function mapMailboxToListEntry(m) {
962
+ const lastSep = m.path.lastIndexOf("/");
963
+ const parentFolderId = lastSep === -1 ? void 0 : m.path.slice(0, lastSep);
964
+ return {
965
+ id: m.path,
966
+ displayName: m.path,
967
+ parentFolderId,
968
+ childFolderCount: 0,
969
+ totalItemCount: m.status?.messages ?? 0,
970
+ unreadItemCount: m.status?.unseen ?? 0
971
+ };
972
+ }
973
+
974
+ // src/providers/imap/read-ops.ts
975
+ async function listEmails(clients, account, opts) {
976
+ const client = clients.get(account);
977
+ const folder = resolveFolder(opts.folder ?? "INBOX");
978
+ const limit = clampLimit2(opts.limit, 25, 100);
979
+ const skip = opts.skip ?? 0;
980
+ return client.withMailbox(folder, async (imap) => {
981
+ const searchCriteria = {};
982
+ if (opts.unreadOnly) searchCriteria.seen = false;
983
+ const allUids = Object.keys(searchCriteria).length > 0 ? await imap.search(searchCriteria, { uid: true }) : await imap.search({ all: true }, { uid: true });
984
+ allUids.sort((a, b) => b - a);
985
+ const pageUids = allUids.slice(skip, skip + limit);
986
+ const hasMore = skip + limit < allUids.length;
987
+ if (pageUids.length === 0) {
988
+ return { items: [], hasMore };
989
+ }
990
+ const messages = await imap.fetchAll(
991
+ pageUids,
992
+ { envelope: true, flags: true },
993
+ { uid: true }
994
+ );
995
+ const items = [];
996
+ for (const msg of messages) {
997
+ items.push(
998
+ mapSummary2(
999
+ msg.uid,
1000
+ folder,
1001
+ msg.envelope,
1002
+ msg.flags
1003
+ )
1004
+ );
1005
+ }
1006
+ return { items, hasMore };
1007
+ });
1008
+ }
1009
+ async function searchEmails(clients, account, query, opts) {
1010
+ const client = clients.get(account);
1011
+ const limit = clampLimit2(opts.limit, 25, 100);
1012
+ return client.withMailbox("INBOX", async (imap) => {
1013
+ const uids = await imap.search({ text: query }, { uid: true });
1014
+ uids.sort((a, b) => b - a);
1015
+ const pageUids = uids.slice(0, limit);
1016
+ if (pageUids.length === 0) return [];
1017
+ const messages = await imap.fetchAll(
1018
+ pageUids,
1019
+ { envelope: true, flags: true },
1020
+ { uid: true }
1021
+ );
1022
+ return messages.map(
1023
+ (msg) => mapSummary2(
1024
+ msg.uid,
1025
+ "INBOX",
1026
+ msg.envelope,
1027
+ msg.flags
1028
+ )
1029
+ );
1030
+ });
1031
+ }
1032
+ async function readEmail(clients, account, id) {
1033
+ const client = clients.get(account);
1034
+ const { folder, uid } = decodeId(id);
1035
+ return client.withMailbox(folder, async (imap) => {
1036
+ const msg = await imap.fetchOne(
1037
+ uid,
1038
+ { bodyStructure: true, envelope: true, flags: true },
1039
+ { uid: true }
1040
+ );
1041
+ if (!msg || !msg.envelope) {
1042
+ throw new Error(`message not found: ${id}`);
1043
+ }
1044
+ const envelope = msg.envelope;
1045
+ const structure = msg.bodyStructure;
1046
+ const flags = msg.flags ?? /* @__PURE__ */ new Set();
1047
+ let bodyText;
1048
+ let bodyHtml;
1049
+ const textPart = findPartByType(structure, "text/plain");
1050
+ const htmlPart = findPartByType(structure, "text/html");
1051
+ if (textPart) {
1052
+ const { content } = await imap.download(uid, textPart, { uid: true });
1053
+ const chunks = [];
1054
+ for await (const chunk of content) {
1055
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1056
+ }
1057
+ bodyText = Buffer.concat(chunks).toString("utf-8");
1058
+ }
1059
+ if (htmlPart) {
1060
+ const { content } = await imap.download(uid, htmlPart, { uid: true });
1061
+ const chunks = [];
1062
+ for await (const chunk of content) {
1063
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1064
+ }
1065
+ bodyHtml = Buffer.concat(chunks).toString("utf-8");
1066
+ }
1067
+ const attachments = findAttachments(
1068
+ structure
1069
+ ).map((a) => ({
1070
+ id: a.part,
1071
+ name: a.name,
1072
+ contentType: a.contentType,
1073
+ size: a.size
1074
+ }));
1075
+ const summary = mapSummary2(uid, folder, envelope, flags);
1076
+ return {
1077
+ ...summary,
1078
+ cc: (envelope.cc ?? []).map(mapEnvelopeAddr),
1079
+ bcc: (envelope.bcc ?? []).map(mapEnvelopeAddr),
1080
+ bodyText,
1081
+ bodyHtml,
1082
+ attachments: attachments.length > 0 ? attachments : void 0,
1083
+ hasAttachments: attachments.length > 0
1084
+ };
1085
+ });
1086
+ }
1087
+ async function readAttachment(clients, account, messageId, attachmentId) {
1088
+ const client = clients.get(account);
1089
+ const { folder, uid } = decodeId(messageId);
1090
+ return client.withMailbox(folder, async (imap) => {
1091
+ const msg = await imap.fetchOne(
1092
+ uid,
1093
+ { bodyStructure: true },
1094
+ { uid: true }
1095
+ );
1096
+ let name = "attachment";
1097
+ let contentType;
1098
+ if (msg && msg.bodyStructure) {
1099
+ const attachments = findAttachments(msg.bodyStructure);
1100
+ const match = attachments.find((a) => a.part === attachmentId);
1101
+ if (match) {
1102
+ name = match.name;
1103
+ contentType = match.contentType;
1104
+ }
1105
+ }
1106
+ const { meta, content } = await imap.download(uid, attachmentId, {
1107
+ uid: true
1108
+ });
1109
+ const outPath = pathJoin2(tmpdir2(), name);
1110
+ await pipeline(
1111
+ content,
1112
+ createWriteStream(outPath)
1113
+ );
1114
+ return {
1115
+ name,
1116
+ contentType: contentType ?? meta.contentType,
1117
+ path: outPath
1118
+ };
1119
+ });
1120
+ }
1121
+ async function listFolders(clients, account, opts) {
1122
+ const client = clients.get(account);
1123
+ const imap = await client.getImap();
1124
+ const mailboxes = await imap.list({
1125
+ statusQuery: { messages: true, unseen: true, uidNext: true }
1126
+ });
1127
+ let results = mailboxes.map(mapMailboxToListEntry);
1128
+ if (opts.parentFolderId) {
1129
+ const parentPath = opts.parentFolderId;
1130
+ results = results.filter(
1131
+ (f) => f.parentFolderId === parentPath || parentPath === "INBOX" && f.displayName === "INBOX"
1132
+ );
1133
+ } else {
1134
+ const allPaths = new Set(results.map((f) => f.displayName));
1135
+ results = results.filter((f) => {
1136
+ const lastSep = f.displayName.lastIndexOf("/");
1137
+ if (lastSep === -1) return true;
1138
+ const parent = f.displayName.slice(0, lastSep);
1139
+ return !allPaths.has(parent);
1140
+ });
1141
+ }
1142
+ return results;
1143
+ }
1144
+
1145
+ // src/providers/imap/write-ops.ts
1146
+ import { randomUUID as randomUUID3 } from "crypto";
1147
+ import MailComposer from "nodemailer/lib/mail-composer/index.js";
1148
+ async function addAccount(clients, store, input) {
1149
+ const cfg = input.config ?? {};
1150
+ const host = String(cfg.host ?? "");
1151
+ const port = Number(cfg.port ?? 993);
1152
+ const secure = cfg.secure !== false;
1153
+ const user = String(cfg.user ?? input.email ?? "");
1154
+ const password = String(cfg.password ?? "");
1155
+ const smtpHost = String(cfg.smtpHost ?? host);
1156
+ const smtpPort = Number(cfg.smtpPort ?? 587);
1157
+ const smtpSecure = cfg.smtpSecure === true;
1158
+ if (!host || !user || !password) {
1159
+ throw new Error(
1160
+ "IMAP requires config: { host, port?, secure?, user, password, smtpHost?, smtpPort?, smtpSecure? }"
1161
+ );
1162
+ }
1163
+ const tokens = {
1164
+ host,
1165
+ port,
1166
+ secure,
1167
+ user,
1168
+ password,
1169
+ smtpHost: smtpHost || host,
1170
+ smtpPort: smtpPort || 587,
1171
+ smtpSecure
1172
+ };
1173
+ const client = clients.get({
1174
+ email: user.toLowerCase(),
1175
+ provider: "imap",
1176
+ tokens,
1177
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
1178
+ });
1179
+ try {
1180
+ await client.getImap();
1181
+ } finally {
1182
+ clients.invalidate(user.toLowerCase());
1183
+ }
1184
+ try {
1185
+ const t = client.getTransporter();
1186
+ await t.verify();
1187
+ } catch {
1188
+ }
1189
+ const email = user.toLowerCase();
1190
+ const rec = {
1191
+ email,
1192
+ provider: "imap",
1193
+ displayName: input.email ?? user,
1194
+ tokens,
1195
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
1196
+ };
1197
+ const saved = await store.upsertAccount(rec);
1198
+ return { status: "ready", account: saved };
1199
+ }
1200
+ function completeAddAccount() {
1201
+ return {
1202
+ status: "error",
1203
+ error: "IMAP accounts are set up synchronously \u2014 no polling needed. Call add_account with IMAP config to create the account directly."
1204
+ };
1205
+ }
1206
+ async function sendEmail(clients, account, msg) {
1207
+ const client = clients.get(account);
1208
+ const transporter = client.getTransporter();
1209
+ const mailOptions = {
1210
+ from: `${account.displayName ?? ""} <${account.email}>`,
1211
+ to: msg.to.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", "),
1212
+ subject: msg.subject
1213
+ };
1214
+ if (msg.isHtml) {
1215
+ mailOptions.html = msg.body;
1216
+ } else {
1217
+ mailOptions.text = msg.body;
1218
+ }
1219
+ mailOptions.attachDataUrls = true;
1220
+ if (msg.cc && msg.cc.length > 0) {
1221
+ mailOptions.cc = msg.cc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
1222
+ }
1223
+ if (msg.bcc && msg.bcc.length > 0) {
1224
+ mailOptions.bcc = msg.bcc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
1225
+ }
1226
+ if (msg.inReplyTo || msg.forwardMessageId) {
1227
+ const refId = msg.inReplyTo ?? msg.forwardMessageId;
1228
+ if (refId) {
1229
+ try {
1230
+ const { folder: refFolder, uid: refUid } = decodeId(refId);
1231
+ const refMsg = await client.withMailbox(refFolder, async (imap) => {
1232
+ return imap.fetchOne(
1233
+ refUid,
1234
+ { envelope: true, source: true },
1235
+ { uid: true }
1236
+ );
1237
+ });
1238
+ if (refMsg?.envelope) {
1239
+ const env = refMsg.envelope;
1240
+ if (msg.inReplyTo && env.messageId && !msg.forwardMessageId) {
1241
+ mailOptions.inReplyTo = env.messageId;
1242
+ mailOptions.references = env.messageId;
1243
+ }
1244
+ }
1245
+ if (msg.forwardMessageId && refMsg?.source) {
1246
+ const sourceStr = typeof refMsg.source === "string" ? refMsg.source : Buffer.from(refMsg.source).toString("utf-8");
1247
+ const divider = '\n\n<div style="line-height:12px"><br></div>\n\n<div style="border-left:2px solid #ccc; padding-left:8px; margin-left:0; color:#666">\n---------- Forwarded message ---------<br>' + sourceStr + "\n</div>";
1248
+ if (mailOptions.html) {
1249
+ mailOptions.html += divider;
1250
+ } else if (mailOptions.text) {
1251
+ mailOptions.text += "\n\n---------- Forwarded message ---------\n" + sourceStr;
1252
+ }
1253
+ }
1254
+ } catch {
1255
+ }
1256
+ }
1257
+ }
1258
+ const info = await transporter.sendMail(mailOptions);
1259
+ try {
1260
+ const rawMsg = await buildRawMessage(account, msg, info.messageId);
1261
+ await client.withMailbox("Sent", async (imap) => {
1262
+ await imap.append("Sent", rawMsg, ["\\Seen"]);
1263
+ });
1264
+ } catch {
1265
+ }
1266
+ return { id: info.messageId };
1267
+ }
1268
+ async function saveDraft(clients, account, msg) {
1269
+ const client = clients.get(account);
1270
+ const rawMsg = await buildRawMessage(account, msg);
1271
+ return client.withMailbox("Drafts", async (imap) => {
1272
+ const result = await imap.append("Drafts", rawMsg, ["\\Draft"]);
1273
+ return { id: encodeId("Drafts", result.uid) };
1274
+ });
1275
+ }
1276
+ async function updateDraft(clients, account, id, update) {
1277
+ const client = clients.get(account);
1278
+ const { folder, uid } = decodeId(id);
1279
+ return client.withMailbox(folder, async (imap) => {
1280
+ const existing = await imap.fetchOne(
1281
+ uid,
1282
+ { source: true, envelope: true },
1283
+ { uid: true }
1284
+ );
1285
+ if (!existing?.source) {
1286
+ throw new Error(`draft not found: ${id}`);
1287
+ }
1288
+ const origSubject = existing.envelope ? existing.envelope.subject ?? "" : "";
1289
+ const updatedMsg = {
1290
+ from: `${account.displayName ?? ""} <${account.email}>`,
1291
+ subject: update.subject ?? origSubject,
1292
+ attachDataUrls: true
1293
+ };
1294
+ if (update.body !== void 0) {
1295
+ if (update.isHtml) {
1296
+ updatedMsg.html = update.body;
1297
+ } else {
1298
+ updatedMsg.text = update.body;
1299
+ }
1300
+ }
1301
+ const raw = await new Promise((resolve, reject) => {
1302
+ const mc = new MailComposer(updatedMsg);
1303
+ mc.compile().build((err, buf) => {
1304
+ if (err) reject(err);
1305
+ else resolve(buf);
1306
+ });
1307
+ });
1308
+ await imap.messageDelete(uid, { uid: true });
1309
+ const result = await imap.append(folder, raw, ["\\Draft"]);
1310
+ return { id: encodeId(folder, result.uid) };
1311
+ });
1312
+ }
1313
+ async function moveEmail(clients, account, id, destinationId) {
1314
+ const client = clients.get(account);
1315
+ const { folder, uid } = decodeId(id);
1316
+ const dest = resolveFolder(destinationId);
1317
+ return client.withMailbox(folder, async (imap) => {
1318
+ await imap.messageMove(uid, dest, { uid: true });
1319
+ });
1320
+ }
1321
+ async function sendDraft(clients, account, id) {
1322
+ const client = clients.get(account);
1323
+ const { folder, uid } = decodeId(id);
1324
+ return client.withMailbox(folder, async (imap) => {
1325
+ const draft = await imap.fetchOne(
1326
+ uid,
1327
+ { source: true },
1328
+ { uid: true }
1329
+ );
1330
+ if (!draft?.source) {
1331
+ throw new Error(`draft not found: ${id}`);
1332
+ }
1333
+ const sourceStr = typeof draft.source === "string" ? draft.source : Buffer.from(draft.source).toString("utf-8");
1334
+ const transporter = client.getTransporter();
1335
+ const info = await transporter.sendMail({ raw: sourceStr });
1336
+ try {
1337
+ await imap.messageMove(uid, "Sent", { uid: true });
1338
+ } catch {
1339
+ }
1340
+ return { id: info.messageId };
1341
+ });
1342
+ }
1343
+ async function addAttachmentToDraft(clients, account, draftId, name, contentBytes, contentType) {
1344
+ const client = clients.get(account);
1345
+ const { folder, uid } = decodeId(draftId);
1346
+ return client.withMailbox(folder, async (imap) => {
1347
+ const existing = await imap.fetchOne(
1348
+ uid,
1349
+ { source: true },
1350
+ { uid: true }
1351
+ );
1352
+ if (!existing?.source) {
1353
+ throw new Error(`draft not found: ${draftId}`);
1354
+ }
1355
+ const sourceStr = typeof existing.source === "string" ? existing.source : Buffer.from(existing.source).toString("utf-8");
1356
+ const built = await new Promise((resolve, reject) => {
1357
+ const mc = new MailComposer({
1358
+ raw: sourceStr,
1359
+ attachments: [
1360
+ {
1361
+ filename: name,
1362
+ content: Buffer.from(contentBytes, "base64"),
1363
+ contentType: contentType ?? "application/octet-stream"
1364
+ }
1365
+ ]
1366
+ });
1367
+ mc.compile().build((err, buf) => {
1368
+ if (err) reject(err);
1369
+ else resolve(buf);
1370
+ });
1371
+ });
1372
+ await imap.messageDelete(uid, { uid: true });
1373
+ const result = await imap.append(folder, built, ["\\Draft"]);
1374
+ return {
1375
+ id: encodeId(folder, result.uid),
1376
+ attachment: {
1377
+ id: randomUUID3(),
1378
+ name,
1379
+ contentType: contentType ?? "application/octet-stream"
1380
+ }
1381
+ };
1382
+ });
1383
+ }
1384
+ async function markRead(clients, account, id, isRead) {
1385
+ const client = clients.get(account);
1386
+ const { folder, uid } = decodeId(id);
1387
+ return client.withMailbox(folder, async (imap) => {
1388
+ if (isRead) {
1389
+ await imap.messageFlagsAdd(uid, ["\\Seen"], { uid: true });
1390
+ } else {
1391
+ await imap.messageFlagsRemove(uid, ["\\Seen"], { uid: true });
1392
+ }
1393
+ });
1394
+ }
1395
+ async function createFolder(clients, account, input) {
1396
+ const client = clients.get(account);
1397
+ const imap = await client.getImap();
1398
+ const path2 = input.parentFolderId ? `${input.parentFolderId}/${input.displayName}` : input.displayName;
1399
+ const result = await imap.mailboxCreate(path2);
1400
+ return {
1401
+ id: result.path,
1402
+ displayName: result.path,
1403
+ parentFolderId: input.parentFolderId,
1404
+ childFolderCount: 0,
1405
+ totalItemCount: 0,
1406
+ unreadItemCount: 0
1407
+ };
1408
+ }
1409
+ async function renameFolder(clients, account, folderId, newName) {
1410
+ const client = clients.get(account);
1411
+ const imap = await client.getImap();
1412
+ const lastSep = folderId.lastIndexOf("/");
1413
+ const newPath = lastSep === -1 ? newName : folderId.slice(0, lastSep + 1) + newName;
1414
+ const result = await imap.mailboxRename(folderId, newPath);
1415
+ return {
1416
+ id: result.path,
1417
+ displayName: result.path,
1418
+ parentFolderId: lastSep === -1 ? void 0 : folderId.slice(0, lastSep),
1419
+ childFolderCount: 0,
1420
+ totalItemCount: 0,
1421
+ unreadItemCount: 0
1422
+ };
1423
+ }
1424
+ async function deleteFolder(clients, account, folderId) {
1425
+ const client = clients.get(account);
1426
+ const imap = await client.getImap();
1427
+ await imap.mailboxDelete(folderId);
1428
+ }
1429
+ async function buildRawMessage(account, msg, messageId) {
1430
+ const mailOptions = {
1431
+ from: `${account.displayName ?? ""} <${account.email}>`,
1432
+ to: msg.to.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", "),
1433
+ subject: msg.subject,
1434
+ attachDataUrls: true
1435
+ };
1436
+ if (msg.isHtml) {
1437
+ mailOptions.html = msg.body;
1438
+ } else {
1439
+ mailOptions.text = msg.body;
1440
+ }
1441
+ if (msg.cc && msg.cc.length > 0) {
1442
+ mailOptions.cc = msg.cc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
1443
+ }
1444
+ if (msg.bcc && msg.bcc.length > 0) {
1445
+ mailOptions.bcc = msg.bcc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
1446
+ }
1447
+ if (messageId) {
1448
+ mailOptions.messageId = messageId;
1449
+ }
1450
+ return new Promise((resolve, reject) => {
1451
+ const mc = new MailComposer(mailOptions);
1452
+ mc.compile().build((err, buf) => {
1453
+ if (err) reject(err);
1454
+ else resolve(buf.toString("utf-8"));
1455
+ });
1456
+ });
1457
+ }
1458
+
1459
+ // src/providers/imap/index.ts
1460
+ var ImapProvider = class {
1461
+ constructor(store) {
1462
+ this.store = store;
1463
+ }
1464
+ store;
1465
+ id = "imap";
1466
+ clients = new ImapClientFactory();
1467
+ // ---------- account lifecycle ----------
1468
+ async addAccount(input) {
1469
+ if (!this.store) throw new Error("IMAP provider requires an AccountStore");
1470
+ return addAccount(this.clients, this.store, input);
1471
+ }
1472
+ async completeAddAccount(_handle) {
1473
+ return completeAddAccount();
1474
+ }
1475
+ // ---------- browse ----------
1476
+ async listEmails(account, opts) {
1477
+ return listEmails(this.clients, account, opts);
1478
+ }
1479
+ async searchEmails(account, query, opts) {
1480
+ return searchEmails(this.clients, account, query, opts);
1481
+ }
1482
+ async readEmail(account, id) {
1483
+ return readEmail(this.clients, account, id);
1484
+ }
1485
+ async readAttachment(account, messageId, attachmentId) {
1486
+ return readAttachment(this.clients, account, messageId, attachmentId);
1487
+ }
1488
+ // ---------- compose ----------
1489
+ async sendEmail(account, msg) {
1490
+ return sendEmail(this.clients, account, msg);
1491
+ }
1492
+ async saveDraft(account, msg) {
1493
+ return saveDraft(this.clients, account, msg);
1494
+ }
1495
+ async updateDraft(account, id, update) {
1496
+ return updateDraft(this.clients, account, id, update);
1497
+ }
1498
+ async moveEmail(account, id, destinationId) {
1499
+ return moveEmail(this.clients, account, id, destinationId);
1500
+ }
1501
+ async sendDraft(account, id) {
1502
+ return sendDraft(this.clients, account, id);
1503
+ }
1504
+ async addAttachmentToDraft(account, draftId, name, contentBytes, contentType) {
1505
+ return addAttachmentToDraft(this.clients, account, draftId, name, contentBytes, contentType);
1506
+ }
1507
+ // ---------- organize ----------
1508
+ async markRead(account, id, isRead) {
1509
+ return markRead(this.clients, account, id, isRead);
1510
+ }
1511
+ // ---------- folders ----------
1512
+ async listFolders(account, opts) {
1513
+ return listFolders(this.clients, account, opts);
1514
+ }
1515
+ async createFolder(account, input) {
1516
+ return createFolder(this.clients, account, input);
1517
+ }
1518
+ async renameFolder(account, folderId, newName) {
1519
+ return renameFolder(this.clients, account, folderId, newName);
1520
+ }
1521
+ async deleteFolder(account, folderId) {
1522
+ return deleteFolder(this.clients, account, folderId);
1523
+ }
1524
+ };
1525
+
1526
+ // src/providers/gmail/index.ts
1527
+ import { randomUUID as randomUUID5 } from "crypto";
1528
+
1529
+ // src/providers/gmail/auth.ts
1530
+ import { OAuth2Client } from "google-auth-library";
1531
+ var GOOGLE_DEVICE_CODE_URL = "https://oauth2.googleapis.com/device/code";
1532
+ var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
1533
+ var DEFAULT_SCOPES2 = [
1534
+ "https://www.googleapis.com/auth/gmail.modify"
1535
+ ];
1536
+ function isSerializedGmailTokens(obj) {
1537
+ if (typeof obj !== "object" || obj === null) return false;
1538
+ const o = obj;
1539
+ return typeof o.clientId === "string" && (o.clientSecret === void 0 || typeof o.clientSecret === "string") && typeof o.accessToken === "string" && typeof o.refreshToken === "string" && typeof o.expiryDate === "number" && Array.isArray(o.scopes) && typeof o.email === "string";
1540
+ }
1541
+ function buildOAuth2Client(tokens) {
1542
+ const client = new OAuth2Client({
1543
+ clientId: tokens?.clientId,
1544
+ clientSecret: tokens?.clientSecret
1545
+ });
1546
+ if (tokens) {
1547
+ client.setCredentials({
1548
+ access_token: tokens.accessToken,
1549
+ refresh_token: tokens.refreshToken,
1550
+ expiry_date: tokens.expiryDate,
1551
+ scope: tokens.scopes.join(" ")
1552
+ });
1553
+ }
1554
+ return client;
1555
+ }
1556
+ async function getEmailFromToken(accessToken) {
1557
+ const res = await fetch(
1558
+ "https://gmail.googleapis.com/gmail/v1/users/me/profile",
1559
+ {
1560
+ headers: {
1561
+ Authorization: `Bearer ${accessToken}`
1562
+ }
1563
+ }
1564
+ );
1565
+ if (!res.ok) {
1566
+ const body = await res.text().catch(() => "");
1567
+ throw new Error(
1568
+ `Failed to get Gmail profile (${res.status}): ${body}`
1569
+ );
1570
+ }
1571
+ const data = await res.json();
1572
+ return data.emailAddress;
1573
+ }
1574
+ function beginDeviceCode2(scopes = DEFAULT_SCOPES2, clientIdOverride, clientSecretOverride) {
1575
+ const clientId = clientIdOverride || process.env.GOOGLE_CLIENT_ID;
1576
+ if (!clientId) {
1577
+ throw new Error(
1578
+ "GOOGLE_CLIENT_ID is required for Gmail OAuth \u2014 set it in env or provider config"
1579
+ );
1580
+ }
1581
+ const clientSecret = clientSecretOverride || process.env.GOOGLE_CLIENT_SECRET || void 0;
1582
+ let resolve;
1583
+ let reject;
1584
+ const result = new Promise(
1585
+ (res, rej) => {
1586
+ resolve = res;
1587
+ reject = rej;
1588
+ }
1589
+ );
1590
+ let userCode = "";
1591
+ let verificationUri = "";
1592
+ let message = "";
1593
+ let expiresAt = new Date(Date.now() + 15 * 6e4).toISOString();
1594
+ let aborted = false;
1595
+ const ready = (async () => {
1596
+ try {
1597
+ const dcParams = new URLSearchParams();
1598
+ dcParams.set("client_id", clientId);
1599
+ if (clientSecret) dcParams.set("client_secret", clientSecret);
1600
+ dcParams.set("scope", scopes.join(" "));
1601
+ const dcRes = await fetch(GOOGLE_DEVICE_CODE_URL, {
1602
+ method: "POST",
1603
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1604
+ body: dcParams.toString()
1605
+ });
1606
+ if (!dcRes.ok) {
1607
+ const errBody = await dcRes.json().catch(() => ({}));
1608
+ throw new Error(
1609
+ `Google device-code request failed: ${errBody.error_description ?? errBody.error ?? dcRes.statusText}`
1610
+ );
1611
+ }
1612
+ const dcData = await dcRes.json();
1613
+ userCode = dcData.user_code;
1614
+ verificationUri = dcData.verification_url;
1615
+ const deviceCode = dcData.device_code;
1616
+ let interval = dcData.interval ?? 5;
1617
+ if (dcData.expires_in) {
1618
+ expiresAt = new Date(
1619
+ Date.now() + dcData.expires_in * 1e3
1620
+ ).toISOString();
1621
+ }
1622
+ message = `Go to ${verificationUri} and enter code: ${userCode}`;
1623
+ const tokenParams = new URLSearchParams();
1624
+ tokenParams.set("client_id", clientId);
1625
+ if (clientSecret) tokenParams.set("client_secret", clientSecret);
1626
+ tokenParams.set("device_code", deviceCode);
1627
+ tokenParams.set(
1628
+ "grant_type",
1629
+ "urn:ietf:params:oauth:grant-type:device_code"
1630
+ );
1631
+ const deadline = Date.now() + dcData.expires_in * 1e3;
1632
+ while (Date.now() < deadline && !aborted) {
1633
+ await new Promise((r) => setTimeout(r, interval * 1e3));
1634
+ if (aborted) return;
1635
+ const tokenRes = await fetch(GOOGLE_TOKEN_URL, {
1636
+ method: "POST",
1637
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1638
+ body: tokenParams.toString()
1639
+ });
1640
+ const tokenData = await tokenRes.json();
1641
+ if (tokenData.access_token) {
1642
+ const email = await getEmailFromToken(tokenData.access_token);
1643
+ const tokens = {
1644
+ clientId,
1645
+ clientSecret,
1646
+ accessToken: tokenData.access_token,
1647
+ refreshToken: tokenData.refresh_token ?? "",
1648
+ expiryDate: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1e3 : Date.now() + 36e5,
1649
+ scopes: tokenData.scope ? tokenData.scope.split(" ") : scopes,
1650
+ email
1651
+ };
1652
+ resolve({ tokens, email });
1653
+ return;
1654
+ }
1655
+ switch (tokenData.error) {
1656
+ case "authorization_pending":
1657
+ break;
1658
+ // keep polling
1659
+ case "slow_down":
1660
+ interval += 1;
1661
+ break;
1662
+ case "expired_token":
1663
+ throw new Error("Device code expired \u2014 please try again");
1664
+ case "access_denied":
1665
+ throw new Error("User denied access");
1666
+ default:
1667
+ throw new Error(
1668
+ `Token request failed: ${tokenData.error ?? "unknown error"}`
1669
+ );
1670
+ }
1671
+ }
1672
+ if (!aborted) {
1673
+ throw new Error("Device code expired \u2014 please try again");
1674
+ }
1675
+ } catch (err) {
1676
+ if (!aborted) reject(err);
1677
+ }
1678
+ })();
1679
+ return {
1680
+ get userCode() {
1681
+ return userCode;
1682
+ },
1683
+ get verificationUri() {
1684
+ return verificationUri;
1685
+ },
1686
+ get message() {
1687
+ return message;
1688
+ },
1689
+ get expiresAt() {
1690
+ return expiresAt;
1691
+ },
1692
+ result,
1693
+ cancel() {
1694
+ aborted = true;
1695
+ },
1696
+ ...{ _ready: ready }
1697
+ };
1698
+ }
1699
+ async function awaitDeviceCodeReady2(b) {
1700
+ const r = b._ready;
1701
+ await r;
1702
+ }
1703
+
1704
+ // src/providers/gmail/client.ts
1705
+ import { google } from "googleapis";
1706
+ var GmailClientFactory = class {
1707
+ constructor(store, clientId, clientSecret) {
1708
+ this.store = store;
1709
+ this.clientId = clientId;
1710
+ this.clientSecret = clientSecret;
1711
+ }
1712
+ store;
1713
+ clientId;
1714
+ clientSecret;
1715
+ cache = /* @__PURE__ */ new Map();
1716
+ get(account) {
1717
+ const key = account.email.toLowerCase();
1718
+ const existing = this.cache.get(key);
1719
+ if (existing) return existing;
1720
+ if (!isSerializedGmailTokens(account.tokens)) {
1721
+ throw new Error(
1722
+ "Gmail account tokens are missing or corrupted \u2014 re-run add_account"
1723
+ );
1724
+ }
1725
+ const tokens = account.tokens;
1726
+ const resolvedClientId = tokens.clientId || this.clientId;
1727
+ const resolvedSecret = tokens.clientSecret || this.clientSecret;
1728
+ const auth = buildOAuth2Client({
1729
+ ...tokens,
1730
+ clientId: resolvedClientId ?? tokens.clientId,
1731
+ clientSecret: resolvedSecret
1732
+ });
1733
+ const store = this.store;
1734
+ auth.on("tokens", (updated) => {
1735
+ if (!updated.refresh_token && !updated.access_token) return;
1736
+ const fresh = store.getAccount(account.email) ?? account;
1737
+ const currentTokens = isSerializedGmailTokens(fresh.tokens) ? fresh.tokens : tokens;
1738
+ const nextTokens = {
1739
+ ...currentTokens,
1740
+ accessToken: updated.access_token ?? currentTokens.accessToken,
1741
+ refreshToken: updated.refresh_token ?? currentTokens.refreshToken,
1742
+ expiryDate: updated.expiry_date ?? currentTokens.expiryDate,
1743
+ scopes: updated.scope ? updated.scope.split(" ") : currentTokens.scopes
1744
+ };
1745
+ store.upsertAccount({
1746
+ ...fresh,
1747
+ tokens: nextTokens
1748
+ }).catch(() => {
1749
+ });
1750
+ });
1751
+ const gmail = google.gmail({ version: "v1", auth });
1752
+ const entry = { auth, gmail };
1753
+ this.cache.set(key, entry);
1754
+ return entry;
1755
+ }
1756
+ /** Drop a cached client (e.g. after removeAccount). */
1757
+ invalidate(email) {
1758
+ this.cache.delete(email.toLowerCase());
1759
+ }
1760
+ };
1761
+
1762
+ // src/providers/gmail/read-ops.ts
1763
+ import { writeFileSync as writeFileSync2 } from "fs";
1764
+ import { tmpdir as tmpdir3 } from "os";
1765
+ import { join as pathJoin3 } from "path";
1766
+
1767
+ // src/providers/gmail/helpers.ts
1768
+ import MailComposer2 from "nodemailer/lib/mail-composer/index.js";
1769
+ var WELL_KNOWN_TO_LABEL = {
1770
+ inbox: "INBOX",
1771
+ sentitems: "SENT",
1772
+ drafts: "DRAFT",
1773
+ deleteditems: "TRASH",
1774
+ junkemail: "SPAM",
1775
+ outbox: ""
1776
+ // Gmail has no outbox; sendEmail handles this.
1777
+ };
1778
+ function resolveLabel(wellKnownOrId) {
1779
+ const lower = wellKnownOrId.toLowerCase();
1780
+ return WELL_KNOWN_TO_LABEL[lower] ?? wellKnownOrId;
1781
+ }
1782
+ function resolveLabelsForMove(destinationId) {
1783
+ if (destinationId.toLowerCase() === "archive") {
1784
+ return { addLabelIds: [], removeLabelIds: ["INBOX"] };
1785
+ }
1786
+ return { addLabelIds: [resolveLabel(destinationId)], removeLabelIds: [] };
1787
+ }
1788
+ function mapHeaderAddr(raw) {
1789
+ if (!raw) return [];
1790
+ const addrs = raw.split(",");
1791
+ return addrs.map((a) => {
1792
+ const trimmed = a.trim();
1793
+ const match = trimmed.match(/^(.+?)\s*<(.+@.+)>$/);
1794
+ if (match) return { name: match[1].trim(), address: match[2] };
1795
+ return { address: trimmed };
1796
+ });
1797
+ }
1798
+ function findHeader(headers, name) {
1799
+ if (!headers) return void 0;
1800
+ const h = headers.find(
1801
+ (h2) => h2.name?.toLowerCase() === name.toLowerCase()
1802
+ );
1803
+ return h?.value ?? void 0;
1804
+ }
1805
+ function decodeBody(body) {
1806
+ if (!body?.data) return "";
1807
+ return Buffer.from(
1808
+ body.data.replace(/-/g, "+").replace(/_/g, "/"),
1809
+ "base64"
1810
+ ).toString("utf-8");
1811
+ }
1812
+ function parsePayload(payload, prefix = "") {
1813
+ let bodyText;
1814
+ let bodyHtml;
1815
+ const attachments = [];
1816
+ function walk(part, partPrefix) {
1817
+ const mime = part.mimeType ?? "";
1818
+ if (mime === "text/plain" && bodyText === void 0) {
1819
+ bodyText = decodeBody(part.body ?? {});
1820
+ return;
1821
+ }
1822
+ if (mime === "text/html" && bodyHtml === void 0) {
1823
+ bodyHtml = decodeBody(part.body ?? {});
1824
+ return;
1825
+ }
1826
+ if (part.parts) {
1827
+ for (let i = 0; i < part.parts.length; i++) {
1828
+ walk(part.parts[i], (partPrefix ? `${partPrefix}.` : "") + String(i));
1829
+ }
1830
+ return;
1831
+ }
1832
+ const topType = mime.split("/")[0] ?? "";
1833
+ const hasFilename = !!part.filename || !!part.body?.attachmentId;
1834
+ const isAttachment = hasFilename || !!mime && topType !== "text" && topType !== "multipart";
1835
+ if (isAttachment && part.body?.attachmentId) {
1836
+ attachments.push({
1837
+ id: part.body.attachmentId,
1838
+ name: part.filename ?? part.partId ?? "attachment",
1839
+ contentType: mime || void 0,
1840
+ size: part.body.size != null ? Number(part.body.size) : void 0
1841
+ });
1842
+ }
1843
+ }
1844
+ walk(payload, prefix);
1845
+ return { bodyText, bodyHtml, attachments };
1846
+ }
1847
+ function mapSummary3(id, headers, flags) {
1848
+ return {
1849
+ id,
1850
+ subject: findHeader(headers, "Subject") ?? "",
1851
+ from: mapHeaderAddr(findHeader(headers, "From"))[0],
1852
+ to: mapHeaderAddr(findHeader(headers, "To")),
1853
+ receivedAt: flags.internalDate ? new Date(Number(flags.internalDate)).toISOString() : void 0,
1854
+ preview: void 0,
1855
+ isRead: !(flags.labelIds?.includes("UNREAD") ?? false),
1856
+ hasAttachments: false
1857
+ };
1858
+ }
1859
+ function mapFolder2(label) {
1860
+ return {
1861
+ id: label.id ?? "",
1862
+ displayName: label.name ?? "",
1863
+ parentFolderId: void 0,
1864
+ childFolderCount: 0,
1865
+ totalItemCount: label.messagesTotal ?? 0,
1866
+ unreadItemCount: label.messagesUnread ?? 0
1867
+ };
1868
+ }
1869
+ function clampLimit3(v, dflt, max) {
1870
+ if (!v || v <= 0) return dflt;
1871
+ return Math.min(v, max);
1872
+ }
1873
+ async function pool(items, concurrency, fn) {
1874
+ const results = new Array(items.length);
1875
+ let idx = 0;
1876
+ async function worker() {
1877
+ while (idx < items.length) {
1878
+ const i = idx++;
1879
+ results[i] = await fn(items[i]);
1880
+ }
1881
+ }
1882
+ const workers = Array.from(
1883
+ { length: Math.min(concurrency, items.length) },
1884
+ () => worker()
1885
+ );
1886
+ await Promise.all(workers);
1887
+ return results;
1888
+ }
1889
+ function base64urlEncode(buf) {
1890
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1891
+ }
1892
+ async function buildRawMessage2(account, msg, messageId) {
1893
+ const { body: transformed, images } = parseInlineImages(msg.body);
1894
+ const mailOptions = {
1895
+ from: `${account.displayName ?? ""} <${account.email}>`,
1896
+ to: msg.to.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", "),
1897
+ subject: msg.subject,
1898
+ attachDataUrls: true
1899
+ };
1900
+ if (msg.isHtml) {
1901
+ mailOptions.html = transformed;
1902
+ } else {
1903
+ mailOptions.text = transformed;
1904
+ }
1905
+ if (msg.cc && msg.cc.length > 0) {
1906
+ mailOptions.cc = msg.cc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
1907
+ }
1908
+ if (msg.bcc && msg.bcc.length > 0) {
1909
+ mailOptions.bcc = msg.bcc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
1910
+ }
1911
+ if (images.length > 0) {
1912
+ mailOptions.attachments = images.map((img) => ({
1913
+ filename: img.filename,
1914
+ content: Buffer.from(img.contentBytes, "base64"),
1915
+ contentType: img.contentType,
1916
+ cid: img.cid
1917
+ }));
1918
+ }
1919
+ if (messageId) {
1920
+ mailOptions.messageId = messageId;
1921
+ }
1922
+ const rawStr = await new Promise((resolve, reject) => {
1923
+ const mc = new MailComposer2(mailOptions);
1924
+ mc.compile().build((err, buf) => {
1925
+ if (err) reject(err);
1926
+ else resolve(buf.toString("utf-8"));
1927
+ });
1928
+ });
1929
+ return { raw: base64urlEncode(Buffer.from(rawStr, "utf-8")) };
1930
+ }
1931
+
1932
+ // src/providers/gmail/read-ops.ts
1933
+ async function listEmails2(clients, account, opts) {
1934
+ const { gmail } = clients.get(account);
1935
+ const limit = clampLimit3(opts.limit, 25, 100);
1936
+ const label = resolveLabel(opts.folder ?? "inbox");
1937
+ const params = {
1938
+ userId: "me",
1939
+ labelIds: [label],
1940
+ maxResults: limit
1941
+ };
1942
+ if (opts.unreadOnly) {
1943
+ params.q = "is:unread";
1944
+ }
1945
+ const allIds = [];
1946
+ let pageToken;
1947
+ do {
1948
+ const res = await gmail.users.messages.list({ ...params, pageToken });
1949
+ if (res.data.messages) allIds.push(...res.data.messages);
1950
+ pageToken = res.data.nextPageToken ?? void 0;
1951
+ } while (pageToken && allIds.length < (opts.skip ?? 0) + limit);
1952
+ const skip = opts.skip ?? 0;
1953
+ const pageIds = allIds.slice(skip, skip + limit);
1954
+ const hasMore = skip + limit < allIds.length;
1955
+ if (pageIds.length === 0) {
1956
+ return { items: [], hasMore };
1957
+ }
1958
+ const items = await pool(pageIds, 10, async (entry) => {
1959
+ const msgId = entry.id ?? "";
1960
+ const msgRes = await gmail.users.messages.get({
1961
+ userId: "me",
1962
+ id: msgId,
1963
+ format: "metadata",
1964
+ metadataHeaders: ["From", "Subject", "To", "Date"]
1965
+ });
1966
+ const msg = msgRes.data;
1967
+ return mapSummary3(msgId, msg.payload?.headers ?? [], {
1968
+ labelIds: msg.labelIds,
1969
+ internalDate: msg.internalDate
1970
+ });
1971
+ });
1972
+ return { items, hasMore };
1973
+ }
1974
+ async function searchEmails2(clients, account, query, opts) {
1975
+ const { gmail } = clients.get(account);
1976
+ const limit = clampLimit3(opts.limit, 25, 100);
1977
+ const res = await gmail.users.messages.list({
1978
+ userId: "me",
1979
+ q: query,
1980
+ maxResults: limit
1981
+ });
1982
+ const ids = res.data.messages ?? [];
1983
+ if (ids.length === 0) return [];
1984
+ const items = await pool(ids, 10, async (entry) => {
1985
+ const msgId = entry.id ?? "";
1986
+ const msgRes = await gmail.users.messages.get({
1987
+ userId: "me",
1988
+ id: msgId,
1989
+ format: "metadata",
1990
+ metadataHeaders: ["From", "Subject", "To", "Date"]
1991
+ });
1992
+ const msg = msgRes.data;
1993
+ return mapSummary3(msgId, msg.payload?.headers ?? [], {
1994
+ labelIds: msg.labelIds,
1995
+ internalDate: msg.internalDate
1996
+ });
1997
+ });
1998
+ return items;
1999
+ }
2000
+ async function readEmail2(clients, account, id) {
2001
+ const { gmail } = clients.get(account);
2002
+ const res = await gmail.users.messages.get({
2003
+ userId: "me",
2004
+ id,
2005
+ format: "full"
2006
+ });
2007
+ const msg = res.data;
2008
+ if (!msg) throw new Error(`message not found: ${id}`);
2009
+ const headers = msg.payload?.headers ?? [];
2010
+ const { bodyText, bodyHtml, attachments } = parsePayload(msg.payload ?? {});
2011
+ const summary = mapSummary3(id, headers, {
2012
+ labelIds: msg.labelIds,
2013
+ internalDate: msg.internalDate
2014
+ });
2015
+ return {
2016
+ ...summary,
2017
+ cc: mapHeaderAddr(findHeader(headers, "Cc")),
2018
+ bcc: mapHeaderAddr(findHeader(headers, "Bcc")),
2019
+ bodyText,
2020
+ bodyHtml,
2021
+ attachments: attachments.length > 0 ? attachments : void 0,
2022
+ hasAttachments: attachments.length > 0
2023
+ };
2024
+ }
2025
+ async function readAttachment2(clients, account, messageId, attachmentId) {
2026
+ const { gmail } = clients.get(account);
2027
+ const msgRes = await gmail.users.messages.get({
2028
+ userId: "me",
2029
+ id: messageId,
2030
+ format: "full"
2031
+ });
2032
+ const { attachments } = parsePayload(msgRes.data.payload ?? {});
2033
+ const match = attachments.find((a) => a.id === attachmentId);
2034
+ const name = match?.name ?? "attachment";
2035
+ const contentType = match?.contentType;
2036
+ const attRes = await gmail.users.messages.attachments.get({
2037
+ userId: "me",
2038
+ messageId,
2039
+ id: attachmentId
2040
+ });
2041
+ const data = attRes.data.data;
2042
+ if (!data) throw new Error("attachment data is empty");
2043
+ const buf = Buffer.from(
2044
+ data.replace(/-/g, "+").replace(/_/g, "/"),
2045
+ "base64"
2046
+ );
2047
+ const outPath = pathJoin3(tmpdir3(), name);
2048
+ writeFileSync2(outPath, buf);
2049
+ return { name, contentType, path: outPath };
2050
+ }
2051
+ async function listFolders2(clients, account, _opts) {
2052
+ const { gmail } = clients.get(account);
2053
+ const res = await gmail.users.labels.list({ userId: "me" });
2054
+ const labels = res.data.labels ?? [];
2055
+ return labels.map(mapFolder2);
2056
+ }
2057
+
2058
+ // src/providers/gmail/write-ops.ts
2059
+ import { randomUUID as randomUUID4 } from "crypto";
2060
+ import { Buffer as Buffer2 } from "buffer";
2061
+ import MailComposer3 from "nodemailer/lib/mail-composer/index.js";
2062
+ async function sendEmail2(clients, account, msg) {
2063
+ const { gmail } = clients.get(account);
2064
+ let threadId;
2065
+ let rawBody;
2066
+ if (msg.forwardMessageId) {
2067
+ const fwdRes = await gmail.users.messages.get({
2068
+ userId: "me",
2069
+ id: msg.forwardMessageId,
2070
+ format: "raw"
2071
+ });
2072
+ threadId = fwdRes.data.threadId ?? void 0;
2073
+ const fwdRaw = fwdRes.data.raw;
2074
+ if (fwdRaw) {
2075
+ const fwdStr = Buffer2.from(
2076
+ fwdRaw.replace(/-/g, "+").replace(/_/g, "/"),
2077
+ "base64"
2078
+ ).toString("utf-8");
2079
+ const divider = '\n\n<div style="line-height:12px"><br></div>\n\n<div style="border-left:2px solid #ccc; padding-left:8px; margin-left:0; color:#666">\n---------- Forwarded message ---------<br>' + fwdStr + "\n</div>";
2080
+ const combinedMsg = { ...msg, body: msg.body + divider };
2081
+ rawBody = await buildRawMessage2(account, combinedMsg);
2082
+ } else {
2083
+ rawBody = await buildRawMessage2(account, msg);
2084
+ }
2085
+ } else {
2086
+ rawBody = await buildRawMessage2(account, msg);
2087
+ if (msg.inReplyTo) {
2088
+ try {
2089
+ const refRes = await gmail.users.messages.get({
2090
+ userId: "me",
2091
+ id: msg.inReplyTo,
2092
+ format: "minimal"
2093
+ });
2094
+ threadId = refRes.data.threadId ?? void 0;
2095
+ } catch {
2096
+ }
2097
+ }
2098
+ }
2099
+ const sendRes = await gmail.users.messages.send({
2100
+ userId: "me",
2101
+ requestBody: {
2102
+ raw: rawBody.raw,
2103
+ threadId
675
2104
  }
676
- if (update.cc !== void 0) {
677
- payload.ccRecipients = update.cc.map(toRecipient);
2105
+ });
2106
+ return { id: sendRes.data.id ?? "" };
2107
+ }
2108
+ async function saveDraft2(clients, account, msg) {
2109
+ const { gmail } = clients.get(account);
2110
+ const { raw } = await buildRawMessage2(account, msg);
2111
+ let threadId;
2112
+ if (msg.inReplyTo) {
2113
+ try {
2114
+ const refRes = await gmail.users.messages.get({
2115
+ userId: "me",
2116
+ id: msg.inReplyTo,
2117
+ format: "minimal"
2118
+ });
2119
+ threadId = refRes.data.threadId ?? void 0;
2120
+ } catch {
678
2121
  }
679
- if (update.bcc !== void 0) {
680
- payload.bccRecipients = update.bcc.map(toRecipient);
2122
+ }
2123
+ const draftRes = await gmail.users.drafts.create({
2124
+ userId: "me",
2125
+ requestBody: {
2126
+ message: { raw, threadId }
681
2127
  }
682
- if (update.body !== void 0) {
683
- const converted = convertInlineImages(update.body);
684
- payload.body = {
685
- contentType: update.isHtml ? "HTML" : "Text",
686
- content: converted.body
687
- };
688
- if (converted.attachments.length > 0) {
689
- payload.attachments = converted.attachments;
2128
+ });
2129
+ return { id: draftRes.data.message?.id ?? draftRes.data.id ?? "" };
2130
+ }
2131
+ async function updateDraft2(clients, account, id, update) {
2132
+ const { gmail } = clients.get(account);
2133
+ const draftRes = await gmail.users.drafts.get({
2134
+ userId: "me",
2135
+ id,
2136
+ format: "raw"
2137
+ });
2138
+ const existingMessage = draftRes.data.message;
2139
+ if (!existingMessage?.raw) {
2140
+ throw new Error(`draft not found: ${id}`);
2141
+ }
2142
+ const existingHeaders = existingMessage.payload?.headers ?? [];
2143
+ const origSubject = findHeader(existingHeaders, "Subject") ?? "";
2144
+ const rawStr = Buffer2.from(
2145
+ existingMessage.raw.replace(/-/g, "+").replace(/_/g, "/"),
2146
+ "base64"
2147
+ ).toString("utf-8");
2148
+ const existingTo = update.to ?? mapHeaderAddr(findHeader(existingHeaders, "To"));
2149
+ const existingCc = update.cc ?? mapHeaderAddr(findHeader(existingHeaders, "Cc"));
2150
+ const existingBcc = update.bcc ?? mapHeaderAddr(findHeader(existingHeaders, "Bcc"));
2151
+ const { raw } = await buildRawMessage2(account, {
2152
+ to: existingTo,
2153
+ subject: update.subject ?? origSubject,
2154
+ body: update.body ?? "",
2155
+ isHtml: update.isHtml,
2156
+ cc: existingCc.length > 0 ? existingCc : void 0,
2157
+ bcc: existingBcc.length > 0 ? existingBcc : void 0
2158
+ });
2159
+ const updated = await gmail.users.drafts.update({
2160
+ userId: "me",
2161
+ id,
2162
+ requestBody: {
2163
+ message: {
2164
+ raw,
2165
+ threadId: existingMessage.threadId ?? void 0
690
2166
  }
691
2167
  }
692
- await client.api(`/me/messages/${encodeURIComponent(id)}`).patch(payload);
693
- return { id };
694
- }
695
- async moveEmail(account, id, destinationId) {
696
- const client = this.clients.get(account);
697
- await client.api(`/me/messages/${encodeURIComponent(id)}/move`).post({ destinationId });
698
- }
699
- async sendDraft(account, id) {
700
- const client = this.clients.get(account);
701
- await client.api(`/me/messages/${encodeURIComponent(id)}/send`).post({});
702
- return { id };
703
- }
704
- async addAttachmentToDraft(account, draftId, name, contentBytes, contentType) {
705
- const client = this.clients.get(account);
706
- const att = await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post({
707
- "@odata.type": "#microsoft.graph.fileAttachment",
708
- name,
709
- contentType: contentType ?? "application/octet-stream",
710
- contentBytes
2168
+ });
2169
+ return { id: updated.data.message?.id ?? updated.data.id ?? id };
2170
+ }
2171
+ async function moveEmail2(clients, account, id, destinationId) {
2172
+ const { gmail } = clients.get(account);
2173
+ const { addLabelIds, removeLabelIds } = resolveLabelsForMove(destinationId);
2174
+ await gmail.users.messages.modify({
2175
+ userId: "me",
2176
+ id,
2177
+ requestBody: { addLabelIds, removeLabelIds }
2178
+ });
2179
+ }
2180
+ async function sendDraft2(clients, account, id) {
2181
+ const { gmail } = clients.get(account);
2182
+ const res = await gmail.users.drafts.send({
2183
+ userId: "me",
2184
+ requestBody: { id }
2185
+ });
2186
+ return { id: res.data.id ?? id };
2187
+ }
2188
+ async function addAttachmentToDraft2(clients, account, draftId, name, contentBytes, contentType) {
2189
+ const { gmail } = clients.get(account);
2190
+ const draftRes = await gmail.users.drafts.get({
2191
+ userId: "me",
2192
+ id: draftId,
2193
+ format: "raw"
2194
+ });
2195
+ const existingMessage = draftRes.data.message;
2196
+ if (!existingMessage?.raw) {
2197
+ throw new Error(`draft not found: ${draftId}`);
2198
+ }
2199
+ const rawStr = Buffer2.from(
2200
+ existingMessage.raw.replace(/-/g, "+").replace(/_/g, "/"),
2201
+ "base64"
2202
+ ).toString("utf-8");
2203
+ const newRawStr = await new Promise((resolve, reject) => {
2204
+ const mc = new MailComposer3({
2205
+ raw: rawStr,
2206
+ attachments: [
2207
+ {
2208
+ filename: name,
2209
+ content: Buffer2.from(contentBytes, "base64"),
2210
+ contentType: contentType ?? "application/octet-stream"
2211
+ }
2212
+ ]
711
2213
  });
712
- return {
713
- id: draftId,
714
- attachment: { id: att.id, name: att.name, contentType: att.contentType }
715
- };
716
- }
717
- async markRead(account, id, isRead) {
718
- const client = this.clients.get(account);
719
- await client.api(`/me/messages/${encodeURIComponent(id)}`).patch({ isRead });
720
- }
721
- async listFolders(account, opts) {
722
- const client = this.clients.get(account);
723
- const endpoint = opts.parentFolderId ? `/me/mailFolders/${encodeURIComponent(opts.parentFolderId)}/childFolders` : "/me/mailFolders";
724
- const res = await client.api(endpoint).select([
725
- "id",
726
- "displayName",
727
- "parentFolderId",
728
- "childFolderCount",
729
- "totalItemCount",
730
- "unreadItemCount"
731
- ].join(",")).get();
732
- return (res.value ?? []).map(mapFolder);
733
- }
734
- async createFolder(account, input) {
735
- const client = this.clients.get(account);
736
- const parentId = input.parentFolderId ?? "msgfolderroot";
737
- const created = await client.api(`/me/mailFolders/${encodeURIComponent(parentId)}/childFolders`).post({ displayName: input.displayName });
738
- return mapFolder(created);
739
- }
740
- async renameFolder(account, folderId, newName) {
741
- const client = this.clients.get(account);
742
- const updated = await client.api(`/me/mailFolders/${encodeURIComponent(folderId)}`).patch({ displayName: newName });
743
- return mapFolder(updated);
744
- }
745
- async deleteFolder(account, folderId) {
746
- const client = this.clients.get(account);
747
- await client.api(`/me/mailFolders/${encodeURIComponent(folderId)}`).delete();
748
- }
749
- };
750
- function mapFolder(f) {
2214
+ mc.compile().build((err, buf) => {
2215
+ if (err) reject(err);
2216
+ else resolve(buf.toString("utf-8"));
2217
+ });
2218
+ });
2219
+ const updated = await gmail.users.drafts.update({
2220
+ userId: "me",
2221
+ id: draftId,
2222
+ requestBody: {
2223
+ message: {
2224
+ raw: base64urlEncode(Buffer2.from(newRawStr, "utf-8")),
2225
+ threadId: existingMessage.threadId ?? void 0
2226
+ }
2227
+ }
2228
+ });
751
2229
  return {
752
- id: f.id,
753
- displayName: f.displayName,
754
- parentFolderId: f.parentFolderId,
755
- childFolderCount: f.childFolderCount,
756
- totalItemCount: f.totalItemCount,
757
- unreadItemCount: f.unreadItemCount
2230
+ id: updated.data.message?.id ?? updated.data.id ?? draftId,
2231
+ attachment: {
2232
+ id: randomUUID4(),
2233
+ name,
2234
+ contentType: contentType ?? "application/octet-stream"
2235
+ }
758
2236
  };
759
2237
  }
760
- function mapRecipient(r) {
761
- return {
762
- name: r.emailAddress?.name,
763
- address: r.emailAddress?.address ?? ""
764
- };
2238
+ async function markRead2(clients, account, id, isRead) {
2239
+ const { gmail } = clients.get(account);
2240
+ await gmail.users.messages.modify({
2241
+ userId: "me",
2242
+ id,
2243
+ requestBody: {
2244
+ removeLabelIds: isRead ? ["UNREAD"] : void 0,
2245
+ addLabelIds: isRead ? void 0 : ["UNREAD"]
2246
+ }
2247
+ });
765
2248
  }
766
- function mapSummary(m, folder) {
767
- return {
768
- id: m.id,
769
- subject: m.subject ?? "",
770
- from: m.from ? mapRecipient(m.from) : void 0,
771
- to: (m.toRecipients ?? []).map(mapRecipient),
772
- receivedAt: m.receivedDateTime,
773
- preview: m.bodyPreview,
774
- isRead: m.isRead,
775
- hasAttachments: m.hasAttachments,
776
- folder
777
- };
2249
+ async function createFolder2(clients, account, input) {
2250
+ const { gmail } = clients.get(account);
2251
+ const created = await gmail.users.labels.create({
2252
+ userId: "me",
2253
+ requestBody: {
2254
+ name: input.displayName,
2255
+ messageListVisibility: "show",
2256
+ labelListVisibility: "labelShow"
2257
+ }
2258
+ });
2259
+ return mapFolder2(created.data);
778
2260
  }
779
- function toRecipient(a) {
780
- return { emailAddress: { name: a.name, address: a.address } };
2261
+ async function renameFolder2(clients, account, folderId, newName) {
2262
+ const { gmail } = clients.get(account);
2263
+ const updated = await gmail.users.labels.patch({
2264
+ userId: "me",
2265
+ id: folderId,
2266
+ requestBody: { name: newName }
2267
+ });
2268
+ return mapFolder2(updated.data);
781
2269
  }
782
- function clampLimit(v, dflt, max) {
783
- if (!v || v <= 0) return dflt;
784
- return Math.min(v, max);
2270
+ async function deleteFolder2(clients, account, folderId) {
2271
+ const { gmail } = clients.get(account);
2272
+ await gmail.users.labels.delete({
2273
+ userId: "me",
2274
+ id: folderId
2275
+ });
785
2276
  }
786
2277
 
787
- // src/providers/imap/index.ts
788
- var NOT_IMPLEMENTED = "IMAP provider is not yet implemented in v1. Tracked at src/providers/imap/index.ts \u2014 see src/providers/types.ts for the contract.";
789
- var ImapProvider = class {
790
- id = "imap";
791
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
792
- async addAccount(_input) {
793
- throw new Error(NOT_IMPLEMENTED);
2278
+ // src/providers/gmail/index.ts
2279
+ var GmailProvider = class {
2280
+ constructor(opts) {
2281
+ this.opts = opts;
2282
+ this.clientId = opts.clientId;
2283
+ this.clientSecret = opts.clientSecret;
2284
+ this.clients = new GmailClientFactory(
2285
+ opts.store,
2286
+ opts.clientId,
2287
+ opts.clientSecret
2288
+ );
794
2289
  }
795
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
796
- async completeAddAccount(_handle) {
797
- return { status: "error", error: NOT_IMPLEMENTED };
2290
+ opts;
2291
+ id = "gmail";
2292
+ clients;
2293
+ pending = /* @__PURE__ */ new Map();
2294
+ clientId;
2295
+ clientSecret;
2296
+ // ── account lifecycle ──
2297
+ async addAccount(input) {
2298
+ const begin = beginDeviceCode2(
2299
+ void 0,
2300
+ this.clientId,
2301
+ this.clientSecret
2302
+ );
2303
+ await awaitDeviceCodeReady2(begin);
2304
+ const handle = randomUUID5();
2305
+ const flow = {
2306
+ begin,
2307
+ emailHint: input.email,
2308
+ startedAt: Date.now(),
2309
+ settled: "pending"
2310
+ };
2311
+ this.pending.set(handle, flow);
2312
+ begin.result.then(async ({ tokens, email }) => {
2313
+ const resolvedEmail = (email || input.email || "").toLowerCase();
2314
+ if (!resolvedEmail) {
2315
+ flow.settled = "error";
2316
+ flow.error = "no email returned from Google account";
2317
+ return;
2318
+ }
2319
+ const rec = {
2320
+ email: resolvedEmail,
2321
+ provider: "gmail",
2322
+ displayName: resolvedEmail,
2323
+ tokens,
2324
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
2325
+ };
2326
+ const saved = await this.opts.store.upsertAccount(rec);
2327
+ flow.account = saved;
2328
+ flow.settled = "ready";
2329
+ }).catch((err) => {
2330
+ flow.settled = "error";
2331
+ flow.error = err instanceof Error ? err.message : String(err);
2332
+ });
2333
+ return {
2334
+ status: "pending",
2335
+ handle,
2336
+ verification: {
2337
+ userCode: begin.userCode,
2338
+ verificationUri: begin.verificationUri,
2339
+ expiresAt: begin.expiresAt,
2340
+ message: begin.message
2341
+ }
2342
+ };
2343
+ }
2344
+ async completeAddAccount(handle) {
2345
+ const flow = this.pending.get(handle);
2346
+ if (!flow) return { status: "error", error: "unknown handle" };
2347
+ if (Date.now() - flow.startedAt > 20 * 6e4 && flow.settled === "pending") {
2348
+ flow.settled = "expired";
2349
+ flow.begin.cancel();
2350
+ }
2351
+ if (flow.settled === "ready" && flow.account) {
2352
+ this.pending.delete(handle);
2353
+ return { status: "ready", account: flow.account };
2354
+ }
2355
+ if (flow.settled === "error") {
2356
+ this.pending.delete(handle);
2357
+ return { status: "error", error: flow.error ?? "unknown error" };
2358
+ }
2359
+ if (flow.settled === "expired") {
2360
+ this.pending.delete(handle);
2361
+ return { status: "expired" };
2362
+ }
2363
+ return { status: "pending" };
798
2364
  }
799
- async listEmails(_account, _opts) {
800
- throw new Error(NOT_IMPLEMENTED);
2365
+ // ── browse ──
2366
+ async listEmails(account, opts) {
2367
+ return listEmails2(this.clients, account, opts);
801
2368
  }
802
- async searchEmails(_account, _query, _opts) {
803
- throw new Error(NOT_IMPLEMENTED);
2369
+ async searchEmails(account, query, opts) {
2370
+ return searchEmails2(this.clients, account, query, opts);
804
2371
  }
805
- async readEmail(_account, _id) {
806
- throw new Error(NOT_IMPLEMENTED);
2372
+ async readEmail(account, id) {
2373
+ return readEmail2(this.clients, account, id);
807
2374
  }
808
- async readAttachment(_account, _messageId, _attachmentId) {
809
- throw new Error(NOT_IMPLEMENTED);
2375
+ async readAttachment(account, messageId, attachmentId) {
2376
+ return readAttachment2(this.clients, account, messageId, attachmentId);
810
2377
  }
811
- async sendEmail(_account, _msg) {
812
- throw new Error(NOT_IMPLEMENTED);
2378
+ // ── compose ──
2379
+ async sendEmail(account, msg) {
2380
+ return sendEmail2(this.clients, account, msg);
813
2381
  }
814
- async saveDraft(_account, _msg) {
815
- throw new Error(NOT_IMPLEMENTED);
2382
+ async saveDraft(account, msg) {
2383
+ return saveDraft2(this.clients, account, msg);
816
2384
  }
817
- async updateDraft(_account, _id, _update) {
818
- throw new Error(NOT_IMPLEMENTED);
2385
+ async updateDraft(account, id, update) {
2386
+ return updateDraft2(this.clients, account, id, update);
819
2387
  }
820
- async moveEmail(_account, _id, _destinationId) {
821
- throw new Error(NOT_IMPLEMENTED);
2388
+ async moveEmail(account, id, destinationId) {
2389
+ return moveEmail2(this.clients, account, id, destinationId);
822
2390
  }
823
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
824
- async sendDraft(_account, _id) {
825
- throw new Error(NOT_IMPLEMENTED);
2391
+ async sendDraft(account, id) {
2392
+ return sendDraft2(this.clients, account, id);
826
2393
  }
827
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
828
- async addAttachmentToDraft(_account, _draftId, _name, _contentBytes, _contentType) {
829
- throw new Error(NOT_IMPLEMENTED);
2394
+ async addAttachmentToDraft(account, draftId, name, contentBytes, contentType) {
2395
+ return addAttachmentToDraft2(
2396
+ this.clients,
2397
+ account,
2398
+ draftId,
2399
+ name,
2400
+ contentBytes,
2401
+ contentType
2402
+ );
830
2403
  }
831
- async markRead(_account, _id, _isRead) {
832
- throw new Error(NOT_IMPLEMENTED);
2404
+ // ── organize ──
2405
+ async markRead(account, id, isRead) {
2406
+ return markRead2(this.clients, account, id, isRead);
833
2407
  }
834
- async listFolders(_account, _opts) {
835
- throw new Error(NOT_IMPLEMENTED);
2408
+ // ── folders ──
2409
+ async listFolders(account, opts) {
2410
+ return listFolders2(this.clients, account, opts);
836
2411
  }
837
- async createFolder(_account, _input) {
838
- throw new Error(NOT_IMPLEMENTED);
2412
+ async createFolder(account, input) {
2413
+ return createFolder2(this.clients, account, input);
839
2414
  }
840
- async renameFolder(_account, _folderId, _newName) {
841
- throw new Error(NOT_IMPLEMENTED);
2415
+ async renameFolder(account, folderId, newName) {
2416
+ return renameFolder2(this.clients, account, folderId, newName);
842
2417
  }
843
- async deleteFolder(_account, _folderId) {
844
- throw new Error(NOT_IMPLEMENTED);
2418
+ async deleteFolder(account, folderId) {
2419
+ return deleteFolder2(this.clients, account, folderId);
845
2420
  }
846
2421
  };
847
2422
 
@@ -854,7 +2429,13 @@ function buildRegistry(opts) {
854
2429
  clientId: outlookCfg?.clientId,
855
2430
  tenantId: outlookCfg?.tenantId
856
2431
  }));
857
- providers.set("imap", new ImapProvider());
2432
+ providers.set("imap", new ImapProvider(opts.store));
2433
+ const gmailCfg = opts.providers?.gmail;
2434
+ providers.set("gmail", new GmailProvider({
2435
+ store: opts.store,
2436
+ clientId: gmailCfg?.clientId,
2437
+ clientSecret: gmailCfg?.clientSecret
2438
+ }));
858
2439
  function get(id) {
859
2440
  const p = providers.get(id);
860
2441
  if (!p) throw new Error(`unknown provider: ${id}`);
@@ -897,6 +2478,7 @@ function errMsg(err) {
897
2478
  if (err instanceof Error) return err.message;
898
2479
  return String(err);
899
2480
  }
2481
+ var providerIdEnum = z.enum(["outlook", "imap", "gmail"]);
900
2482
  var emailAddrSchema = z.object({
901
2483
  address: z.string().email(),
902
2484
  name: z.string().optional()
@@ -907,12 +2489,26 @@ var emailAddrOutputSchema = z.object({
907
2489
  });
908
2490
  var accountSummaryOutputSchema = z.object({
909
2491
  email: z.string(),
910
- provider: z.enum(["outlook", "imap", "gmail"]),
2492
+ provider: providerIdEnum,
911
2493
  displayName: z.string().optional(),
912
2494
  addedAt: z.string(),
913
2495
  hasSignature: z.boolean(),
914
2496
  hasStyle: z.boolean()
915
2497
  });
2498
+ var styleOutputSchema = z.object({
2499
+ fontFamily: z.string().optional(),
2500
+ fontSize: z.string().optional(),
2501
+ fontColor: z.string().optional()
2502
+ });
2503
+ var accountFullOutputSchema = z.object({
2504
+ email: z.string(),
2505
+ provider: providerIdEnum,
2506
+ displayName: z.string().optional(),
2507
+ tokens: z.record(z.unknown()),
2508
+ addedAt: z.string(),
2509
+ signature: z.string().optional(),
2510
+ style: styleOutputSchema.optional()
2511
+ });
916
2512
  var emailSummaryOutputSchema = z.object({
917
2513
  id: z.string(),
918
2514
  subject: z.string(),
@@ -930,11 +2526,6 @@ var attachmentMetaOutputSchema = z.object({
930
2526
  contentType: z.string().optional(),
931
2527
  size: z.number().optional()
932
2528
  });
933
- var styleOutputSchema = z.object({
934
- fontFamily: z.string().optional(),
935
- fontSize: z.string().optional(),
936
- fontColor: z.string().optional()
937
- });
938
2529
  var folderInfoOutputSchema = z.object({
939
2530
  id: z.string(),
940
2531
  displayName: z.string(),
@@ -1025,15 +2616,7 @@ function registerAccountTools(server, ctx) {
1025
2616
  }),
1026
2617
  z2.object({
1027
2618
  status: z2.literal("ready"),
1028
- account: z2.object({
1029
- email: z2.string(),
1030
- provider: z2.enum(["outlook", "imap", "gmail"]),
1031
- displayName: z2.string().optional(),
1032
- tokens: z2.record(z2.unknown()),
1033
- addedAt: z2.string(),
1034
- signature: z2.string().optional(),
1035
- style: styleOutputSchema.optional()
1036
- })
2619
+ account: accountFullOutputSchema
1037
2620
  })
1038
2621
  ]);
1039
2622
  if (shouldRegister("add_account", tools)) {
@@ -1042,7 +2625,7 @@ function registerAccountTools(server, ctx) {
1042
2625
  {
1043
2626
  description: "Start adding an email account. For Outlook this returns a device code the user must enter at the verification URL; then call `complete_add_account` with the returned `handle` to finalize. Disabled in --read-only mode.",
1044
2627
  inputSchema: {
1045
- provider: z2.enum(["outlook", "imap", "gmail"]).describe("Email backend. v1 only fully implements 'outlook'."),
2628
+ provider: providerIdEnum.describe("Email backend. 'outlook' (Microsoft Graph) and 'imap' are fully implemented."),
1046
2629
  email: z2.string().email().optional().describe(
1047
2630
  "Optional hint \u2014 the provider will verify it against the auth result."
1048
2631
  ),
@@ -1068,15 +2651,7 @@ function registerAccountTools(server, ctx) {
1068
2651
  }
1069
2652
  const completeAddAccountOutputSchema = z2.object({
1070
2653
  status: z2.enum(["pending", "ready", "expired", "error"]),
1071
- account: z2.object({
1072
- email: z2.string(),
1073
- provider: z2.enum(["outlook", "imap", "gmail"]),
1074
- displayName: z2.string().optional(),
1075
- tokens: z2.record(z2.unknown()),
1076
- addedAt: z2.string(),
1077
- signature: z2.string().optional(),
1078
- style: styleOutputSchema.optional()
1079
- }).optional(),
2654
+ account: accountFullOutputSchema.optional(),
1080
2655
  error: z2.string().optional()
1081
2656
  });
1082
2657
  if (shouldRegister("complete_add_account", tools)) {
@@ -1085,7 +2660,7 @@ function registerAccountTools(server, ctx) {
1085
2660
  {
1086
2661
  description: "Poll/finalize a pending add_account flow. Returns `pending` until the user completes the device-code step, then `ready` with the persisted account.",
1087
2662
  inputSchema: {
1088
- provider: z2.enum(["outlook", "imap", "gmail"]),
2663
+ provider: providerIdEnum,
1089
2664
  handle: z2.string().min(1)
1090
2665
  },
1091
2666
  outputSchema: completeAddAccountOutputSchema
@@ -1235,6 +2810,11 @@ function registerBrowseTools(server, ctx) {
1235
2810
  skip: z3.number(),
1236
2811
  hasMore: z3.boolean()
1237
2812
  };
2813
+ const searchEmailsOutputSchema = {
2814
+ account: z3.string(),
2815
+ count: z3.number(),
2816
+ items: z3.array(emailSummaryOutputSchema)
2817
+ };
1238
2818
  if (shouldRegister("list_emails", tools)) {
1239
2819
  server.registerTool(
1240
2820
  "list_emails",
@@ -1282,7 +2862,7 @@ function registerBrowseTools(server, ctx) {
1282
2862
  query: z3.string().min(1),
1283
2863
  limit: z3.number().int().positive().max(100).optional()
1284
2864
  },
1285
- outputSchema: emailListOutputSchema
2865
+ outputSchema: searchEmailsOutputSchema
1286
2866
  },
1287
2867
  async (args) => {
1288
2868
  try {
@@ -1533,6 +3113,19 @@ function registerFolderTools(server, ctx) {
1533
3113
  import { z as z5 } from "zod";
1534
3114
  function registerOrganizeTools(server, ctx) {
1535
3115
  const { registry, tools } = ctx;
3116
+ async function moveToWellKnown(args, destination, resultKey) {
3117
+ const { provider, account } = registry.resolveByEmail(args.account);
3118
+ await provider.moveEmail(account, args.id, destination);
3119
+ const data = { id: args.id };
3120
+ data[resultKey] = true;
3121
+ return ok(data, data);
3122
+ }
3123
+ async function markReadState(args, isRead) {
3124
+ const { provider, account } = registry.resolveByEmail(args.account);
3125
+ await provider.markRead(account, args.id, isRead);
3126
+ const data = { marked: true, id: args.id, isRead };
3127
+ return ok(data, data);
3128
+ }
1536
3129
  const archiveMoveSchema = {
1537
3130
  account: z5.string().email(),
1538
3131
  id: z5.string().min(1).describe("Message ID to move")
@@ -1551,10 +3144,7 @@ function registerOrganizeTools(server, ctx) {
1551
3144
  },
1552
3145
  async (args) => {
1553
3146
  try {
1554
- const { provider, account } = registry.resolveByEmail(args.account);
1555
- await provider.moveEmail(account, args.id, "archive");
1556
- const data = { archived: true, id: args.id };
1557
- return ok(data, data);
3147
+ return await moveToWellKnown(args, "archive", "archived");
1558
3148
  } catch (err) {
1559
3149
  return fail(errMsg(err));
1560
3150
  }
@@ -1575,10 +3165,7 @@ function registerOrganizeTools(server, ctx) {
1575
3165
  },
1576
3166
  async (args) => {
1577
3167
  try {
1578
- const { provider, account } = registry.resolveByEmail(args.account);
1579
- await provider.moveEmail(account, args.id, "deleteditems");
1580
- const data = { trashed: true, id: args.id };
1581
- return ok(data, data);
3168
+ return await moveToWellKnown(args, "deleteditems", "trashed");
1582
3169
  } catch (err) {
1583
3170
  return fail(errMsg(err));
1584
3171
  }
@@ -1639,14 +3226,7 @@ function registerOrganizeTools(server, ctx) {
1639
3226
  },
1640
3227
  async (args) => {
1641
3228
  try {
1642
- const { provider, account } = registry.resolveByEmail(args.account);
1643
- await provider.markRead(account, args.id, true);
1644
- const data = {
1645
- marked: true,
1646
- id: args.id,
1647
- isRead: true
1648
- };
1649
- return ok(data, data);
3229
+ return await markReadState(args, true);
1650
3230
  } catch (err) {
1651
3231
  return fail(errMsg(err));
1652
3232
  }
@@ -1663,14 +3243,7 @@ function registerOrganizeTools(server, ctx) {
1663
3243
  },
1664
3244
  async (args) => {
1665
3245
  try {
1666
- const { provider, account } = registry.resolveByEmail(args.account);
1667
- await provider.markRead(account, args.id, false);
1668
- const data = {
1669
- marked: true,
1670
- id: args.id,
1671
- isRead: false
1672
- };
1673
- return ok(data, data);
3246
+ return await markReadState(args, false);
1674
3247
  } catch (err) {
1675
3248
  return fail(errMsg(err));
1676
3249
  }
@@ -1933,14 +3506,14 @@ function registerComposeTools(server, ctx) {
1933
3506
  function registerTools(server, opts) {
1934
3507
  const { store, registry, tools } = opts;
1935
3508
  registerAccountTools(server, { store, registry, tools });
1936
- registerBrowseTools(server, { store, registry, tools });
3509
+ registerBrowseTools(server, { registry, tools });
1937
3510
  registerFolderTools(server, { registry, tools });
1938
3511
  registerOrganizeTools(server, { registry, tools });
1939
3512
  registerComposeTools(server, { store, registry, tools });
1940
3513
  }
1941
3514
 
1942
3515
  // src/version.ts
1943
- var VERSION = "0.3.0";
3516
+ var VERSION = "0.4.1";
1944
3517
 
1945
3518
  // src/config.ts
1946
3519
  import { readFileSync } from "fs";
@@ -1958,8 +3531,13 @@ var outlookProviderSchema = z7.object({
1958
3531
  clientId: z7.string().optional(),
1959
3532
  tenantId: z7.string().optional()
1960
3533
  });
3534
+ var gmailProviderSchema = z7.object({
3535
+ clientId: z7.string().optional(),
3536
+ clientSecret: z7.string().optional()
3537
+ });
1961
3538
  var providersConfigSchema = z7.object({
1962
- outlook: outlookProviderSchema.optional()
3539
+ outlook: outlookProviderSchema.optional(),
3540
+ gmail: gmailProviderSchema.optional()
1963
3541
  });
1964
3542
  var rawConfigSchema = z7.object({
1965
3543
  dataDir: z7.string().optional(),
@@ -2101,7 +3679,7 @@ async function startHttp(server, host, port) {
2101
3679
  let transport = sessionId ? sessions.get(sessionId) : void 0;
2102
3680
  if (!transport) {
2103
3681
  transport = new StreamableHTTPServerTransport({
2104
- sessionIdGenerator: () => randomUUID2(),
3682
+ sessionIdGenerator: () => randomUUID6(),
2105
3683
  onsessioninitialized: (sid) => {
2106
3684
  sessions.set(sid, transport);
2107
3685
  }
@@ -2193,7 +3771,8 @@ Example hypermail-config.json:
2193
3771
  "http": { "enabled": false },
2194
3772
  "tools": { "disabled": ["send_email"] },
2195
3773
  "providers": {
2196
- "outlook": { "clientId": "\${MS_CLIENT_ID}", "tenantId": "\${MS_TENANT_ID}" }
3774
+ "outlook": { "clientId": "\${MS_CLIENT_ID}", "tenantId": "\${MS_TENANT_ID}" },
3775
+ "gmail": { "clientId": "\${GOOGLE_CLIENT_ID}", "clientSecret": "\${GOOGLE_CLIENT_SECRET}" }
2197
3776
  }
2198
3777
  }
2199
3778
  `;