hypermail-mcp 0.4.1 → 0.4.3

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) {
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) {
563
589
  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
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);
570
594
  if (msg.forwardMessageId) {
571
595
  const draftId = await this.buildDraftFromReference(
572
596
  client,
573
597
  `/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) {
618
- 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
- const converted = convertInlineImages(msg.body);
625
- if (msg.forwardMessageId) {
626
- const draftId = await this.buildDraftFromReference(
627
- client,
628
- `/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,24 +611,40 @@ 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 = {};
@@ -757,91 +738,1685 @@ function mapFolder(f) {
757
738
  unreadItemCount: f.unreadItemCount
758
739
  };
759
740
  }
760
- function mapRecipient(r) {
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
+ });
761
2015
  return {
762
- name: r.emailAddress?.name,
763
- address: r.emailAddress?.address ?? ""
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
764
2023
  };
765
2024
  }
766
- function mapSummary(m, folder) {
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
2104
+ }
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 {
2121
+ }
2122
+ }
2123
+ const draftRes = await gmail.users.drafts.create({
2124
+ userId: "me",
2125
+ requestBody: {
2126
+ message: { raw, threadId }
2127
+ }
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
2166
+ }
2167
+ }
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
+ ]
2213
+ });
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
+ });
767
2229
  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
2230
+ id: updated.data.message?.id ?? updated.data.id ?? draftId,
2231
+ attachment: {
2232
+ id: randomUUID4(),
2233
+ name,
2234
+ contentType: contentType ?? "application/octet-stream"
2235
+ }
777
2236
  };
778
2237
  }
779
- function toRecipient(a) {
780
- return { emailAddress: { name: a.name, address: a.address } };
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
+ });
781
2248
  }
782
- function clampLimit(v, dflt, max) {
783
- if (!v || v <= 0) return dflt;
784
- return Math.min(v, max);
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);
2260
+ }
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);
2269
+ }
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.string(), 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(),
@@ -987,15 +2578,15 @@ function shouldRegister(name, tools) {
987
2578
  import { z as z2 } from "zod";
988
2579
  function registerAccountTools(server, ctx) {
989
2580
  const { store, registry, tools } = ctx;
990
- const listAccountsOutputSchema = {
2581
+ const listAccountsOutputSchema = z2.object({
991
2582
  accounts: z2.array(accountSummaryOutputSchema)
992
- };
2583
+ });
993
2584
  if (shouldRegister("list_accounts", tools)) {
994
2585
  server.registerTool(
995
2586
  "list_accounts",
996
2587
  {
997
2588
  description: "List all email accounts known to this server (no secrets). Use the returned `email` value as the `account` argument to other tools.",
998
- inputSchema: {},
2589
+ inputSchema: z2.object({}),
999
2590
  outputSchema: listAccountsOutputSchema
1000
2591
  },
1001
2592
  async () => {
@@ -1012,44 +2603,31 @@ function registerAccountTools(server, ctx) {
1012
2603
  }
1013
2604
  );
1014
2605
  }
1015
- const addAccountOutputSchema = z2.discriminatedUnion("status", [
1016
- z2.object({
1017
- status: z2.literal("pending"),
1018
- handle: z2.string(),
1019
- verification: z2.object({
1020
- userCode: z2.string(),
1021
- verificationUri: z2.string(),
1022
- expiresAt: z2.string(),
1023
- message: z2.string()
1024
- })
1025
- }),
1026
- z2.object({
1027
- 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
- })
1037
- })
1038
- ]);
2606
+ const addAccountOutputSchema = z2.object({
2607
+ status: z2.enum(["pending", "ready"]),
2608
+ handle: z2.string().optional(),
2609
+ verification: z2.object({
2610
+ userCode: z2.string(),
2611
+ verificationUri: z2.string(),
2612
+ expiresAt: z2.string(),
2613
+ message: z2.string()
2614
+ }).optional(),
2615
+ account: accountFullOutputSchema.optional()
2616
+ });
1039
2617
  if (shouldRegister("add_account", tools)) {
1040
2618
  server.registerTool(
1041
2619
  "add_account",
1042
2620
  {
1043
2621
  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
- inputSchema: {
1045
- provider: z2.enum(["outlook", "imap", "gmail"]).describe("Email backend. v1 only fully implements 'outlook'."),
2622
+ inputSchema: z2.object({
2623
+ provider: providerIdEnum.describe("Email backend. 'outlook' (Microsoft Graph) and 'imap' are fully implemented."),
1046
2624
  email: z2.string().email().optional().describe(
1047
2625
  "Optional hint \u2014 the provider will verify it against the auth result."
1048
2626
  ),
1049
- config: z2.record(z2.unknown()).optional().describe(
2627
+ config: z2.record(z2.string(), z2.unknown()).optional().describe(
1050
2628
  "Provider-specific config (e.g. IMAP host/port). Unused for Outlook."
1051
2629
  )
1052
- },
2630
+ }),
1053
2631
  outputSchema: addAccountOutputSchema
1054
2632
  },
1055
2633
  async (args) => {
@@ -1068,15 +2646,7 @@ function registerAccountTools(server, ctx) {
1068
2646
  }
1069
2647
  const completeAddAccountOutputSchema = z2.object({
1070
2648
  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(),
2649
+ account: accountFullOutputSchema.optional(),
1080
2650
  error: z2.string().optional()
1081
2651
  });
1082
2652
  if (shouldRegister("complete_add_account", tools)) {
@@ -1084,10 +2654,10 @@ function registerAccountTools(server, ctx) {
1084
2654
  "complete_add_account",
1085
2655
  {
1086
2656
  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
- inputSchema: {
1088
- provider: z2.enum(["outlook", "imap", "gmail"]),
2657
+ inputSchema: z2.object({
2658
+ provider: providerIdEnum,
1089
2659
  handle: z2.string().min(1)
1090
- },
2660
+ }),
1091
2661
  outputSchema: completeAddAccountOutputSchema
1092
2662
  },
1093
2663
  async (args) => {
@@ -1106,16 +2676,16 @@ function registerAccountTools(server, ctx) {
1106
2676
  }
1107
2677
  );
1108
2678
  }
1109
- const accountSettingsOutputSchema = {
2679
+ const accountSettingsOutputSchema = z2.object({
1110
2680
  signature: z2.string().nullable(),
1111
2681
  style: styleOutputSchema.nullable()
1112
- };
2682
+ });
1113
2683
  if (shouldRegister("get_account_settings", tools)) {
1114
2684
  server.registerTool(
1115
2685
  "get_account_settings",
1116
2686
  {
1117
2687
  description: "Get signature (HTML) and style preferences for an account.",
1118
- inputSchema: { account: z2.string().email() },
2688
+ inputSchema: z2.object({ account: z2.string().email() }),
1119
2689
  outputSchema: accountSettingsOutputSchema
1120
2690
  },
1121
2691
  async (args) => {
@@ -1139,7 +2709,7 @@ function registerAccountTools(server, ctx) {
1139
2709
  "set_account_settings",
1140
2710
  {
1141
2711
  description: "Set signature (HTML snippet) and/or style preferences for an account. Disabled in --read-only mode.",
1142
- inputSchema: {
2712
+ inputSchema: z2.object({
1143
2713
  account: z2.string().email(),
1144
2714
  signature: z2.string().optional().describe(
1145
2715
  "HTML snippet \u2014 may contain formatting, images, links. Pass null to clear."
@@ -1151,7 +2721,7 @@ function registerAccountTools(server, ctx) {
1151
2721
  }).optional().describe(
1152
2722
  "Font preferences applied to outgoing HTML emails. Pass null to clear."
1153
2723
  )
1154
- },
2724
+ }),
1155
2725
  outputSchema: accountSettingsOutputSchema
1156
2726
  },
1157
2727
  async (args) => {
@@ -1175,16 +2745,16 @@ function registerAccountTools(server, ctx) {
1175
2745
  }
1176
2746
  );
1177
2747
  }
1178
- const removeAccountOutputSchema = {
2748
+ const removeAccountOutputSchema = z2.object({
1179
2749
  removed: z2.boolean(),
1180
2750
  email: z2.string()
1181
- };
2751
+ });
1182
2752
  if (shouldRegister("remove_account", tools)) {
1183
2753
  server.registerTool(
1184
2754
  "remove_account",
1185
2755
  {
1186
2756
  description: "Forget an account and delete its stored tokens. Disabled in --read-only mode.",
1187
- inputSchema: { email: z2.string().email() },
2757
+ inputSchema: z2.object({ email: z2.string().email() }),
1188
2758
  outputSchema: removeAccountOutputSchema
1189
2759
  },
1190
2760
  async (args) => {
@@ -1228,25 +2798,30 @@ function selectBody(msg, format) {
1228
2798
  // src/tools/browse.ts
1229
2799
  function registerBrowseTools(server, ctx) {
1230
2800
  const { registry, tools } = ctx;
1231
- const emailListOutputSchema = {
2801
+ const emailListOutputSchema = z3.object({
1232
2802
  account: z3.string(),
1233
2803
  count: z3.number(),
1234
2804
  items: z3.array(emailSummaryOutputSchema),
1235
2805
  skip: z3.number(),
1236
2806
  hasMore: z3.boolean()
1237
- };
2807
+ });
2808
+ const searchEmailsOutputSchema = z3.object({
2809
+ account: z3.string(),
2810
+ count: z3.number(),
2811
+ items: z3.array(emailSummaryOutputSchema)
2812
+ });
1238
2813
  if (shouldRegister("list_emails", tools)) {
1239
2814
  server.registerTool(
1240
2815
  "list_emails",
1241
2816
  {
1242
2817
  description: "List recent emails in a folder of the given account. Pass the user's email address as `account`; the server routes to the correct backend automatically.",
1243
- inputSchema: {
2818
+ inputSchema: z3.object({
1244
2819
  account: z3.string().email(),
1245
2820
  folder: z3.string().default("inbox").optional(),
1246
2821
  limit: z3.number().int().positive().max(100).optional(),
1247
2822
  unreadOnly: z3.boolean().optional(),
1248
2823
  skip: z3.number().int().min(0).optional()
1249
- },
2824
+ }),
1250
2825
  outputSchema: emailListOutputSchema
1251
2826
  },
1252
2827
  async (args) => {
@@ -1277,12 +2852,12 @@ function registerBrowseTools(server, ctx) {
1277
2852
  "search_emails",
1278
2853
  {
1279
2854
  description: "Search emails by free-text query (KQL on Outlook). Returns lightweight summaries.",
1280
- inputSchema: {
2855
+ inputSchema: z3.object({
1281
2856
  account: z3.string().email(),
1282
2857
  query: z3.string().min(1),
1283
2858
  limit: z3.number().int().positive().max(100).optional()
1284
- },
1285
- outputSchema: emailListOutputSchema
2859
+ }),
2860
+ outputSchema: searchEmailsOutputSchema
1286
2861
  },
1287
2862
  async (args) => {
1288
2863
  try {
@@ -1302,7 +2877,7 @@ function registerBrowseTools(server, ctx) {
1302
2877
  }
1303
2878
  );
1304
2879
  }
1305
- const readEmailOutputSchema = {
2880
+ const readEmailOutputSchema = z3.object({
1306
2881
  id: z3.string(),
1307
2882
  subject: z3.string(),
1308
2883
  from: emailAddrOutputSchema.optional(),
@@ -1317,19 +2892,19 @@ function registerBrowseTools(server, ctx) {
1317
2892
  attachments: z3.array(attachmentMetaOutputSchema).optional(),
1318
2893
  body: z3.string(),
1319
2894
  bodyFormat: z3.enum(["markdown", "html", "text"])
1320
- };
2895
+ });
1321
2896
  if (shouldRegister("read_email", tools)) {
1322
2897
  server.registerTool(
1323
2898
  "read_email",
1324
2899
  {
1325
2900
  description: "Fetch a single email with full body and recipients by id. Body is returned as `body` with `bodyFormat` indicating the format. Default format is 'markdown' \u2014 HTML is automatically converted to save context tokens.",
1326
- inputSchema: {
2901
+ inputSchema: z3.object({
1327
2902
  account: z3.string().email(),
1328
2903
  id: z3.string().min(1),
1329
2904
  format: z3.enum(["markdown", "html", "text"]).default("markdown").optional().describe(
1330
2905
  "Output body format. 'markdown' converts HTML to Markdown (default), 'html' returns the raw HTML, 'text' returns plain text."
1331
2906
  )
1332
- },
2907
+ }),
1333
2908
  outputSchema: readEmailOutputSchema
1334
2909
  },
1335
2910
  async (args) => {
@@ -1361,21 +2936,21 @@ function registerBrowseTools(server, ctx) {
1361
2936
  }
1362
2937
  );
1363
2938
  }
1364
- const readAttachmentOutputSchema = {
2939
+ const readAttachmentOutputSchema = z3.object({
1365
2940
  name: z3.string(),
1366
2941
  contentType: z3.string().optional(),
1367
2942
  path: z3.string()
1368
- };
2943
+ });
1369
2944
  if (shouldRegister("read_attachment", tools)) {
1370
2945
  server.registerTool(
1371
2946
  "read_attachment",
1372
2947
  {
1373
2948
  description: "Download an email attachment to a temporary file and return its path. Use messageId and attachmentId from a prior read_email call.",
1374
- inputSchema: {
2949
+ inputSchema: z3.object({
1375
2950
  account: z3.string().email(),
1376
2951
  messageId: z3.string().min(1),
1377
2952
  attachmentId: z3.string().min(1)
1378
- },
2953
+ }),
1379
2954
  outputSchema: readAttachmentOutputSchema
1380
2955
  },
1381
2956
  async (args) => {
@@ -1399,22 +2974,22 @@ function registerBrowseTools(server, ctx) {
1399
2974
  import { z as z4 } from "zod";
1400
2975
  function registerFolderTools(server, ctx) {
1401
2976
  const { registry, tools } = ctx;
1402
- const listFoldersOutputSchema = {
2977
+ const listFoldersOutputSchema = z4.object({
1403
2978
  account: z4.string(),
1404
2979
  count: z4.number(),
1405
2980
  items: z4.array(folderInfoOutputSchema)
1406
- };
2981
+ });
1407
2982
  if (shouldRegister("list_folders", tools)) {
1408
2983
  server.registerTool(
1409
2984
  "list_folders",
1410
2985
  {
1411
2986
  description: "List available mail folders. Returns top-level folders by default, or child folders of the given parent when `parentFolderId` is provided.",
1412
- inputSchema: {
2987
+ inputSchema: z4.object({
1413
2988
  account: z4.string().email(),
1414
2989
  parentFolderId: z4.string().optional().describe(
1415
2990
  "When provided, lists child folders of this folder. When omitted, lists top-level folders (children of the root)."
1416
2991
  )
1417
- },
2992
+ }),
1418
2993
  outputSchema: listFoldersOutputSchema
1419
2994
  },
1420
2995
  async (args) => {
@@ -1435,22 +3010,22 @@ function registerFolderTools(server, ctx) {
1435
3010
  }
1436
3011
  );
1437
3012
  }
1438
- const createFolderOutputSchema = {
3013
+ const createFolderOutputSchema = z4.object({
1439
3014
  created: z4.literal(true),
1440
3015
  folder: folderInfoOutputSchema
1441
- };
3016
+ });
1442
3017
  if (shouldRegister("create_folder", tools)) {
1443
3018
  server.registerTool(
1444
3019
  "create_folder",
1445
3020
  {
1446
3021
  description: "Create a new mail folder. Creates under the root folder by default, or under the specified parent when `parentFolderId` is provided. Disabled in --read-only mode.",
1447
- inputSchema: {
3022
+ inputSchema: z4.object({
1448
3023
  account: z4.string().email(),
1449
3024
  displayName: z4.string().min(1).describe("Name of the new folder"),
1450
3025
  parentFolderId: z4.string().optional().describe(
1451
3026
  "When provided, creates the folder as a child of this folder. When omitted, creates under the root folder."
1452
3027
  )
1453
- },
3028
+ }),
1454
3029
  outputSchema: createFolderOutputSchema
1455
3030
  },
1456
3031
  async (args) => {
@@ -1468,19 +3043,19 @@ function registerFolderTools(server, ctx) {
1468
3043
  }
1469
3044
  );
1470
3045
  }
1471
- const deleteFolderOutputSchema = {
3046
+ const deleteFolderOutputSchema = z4.object({
1472
3047
  deleted: z4.literal(true),
1473
3048
  id: z4.string()
1474
- };
3049
+ });
1475
3050
  if (shouldRegister("delete_folder", tools)) {
1476
3051
  server.registerTool(
1477
3052
  "delete_folder",
1478
3053
  {
1479
3054
  description: "Delete a mail folder by ID. Disabled in --read-only mode.",
1480
- inputSchema: {
3055
+ inputSchema: z4.object({
1481
3056
  account: z4.string().email(),
1482
3057
  folderId: z4.string().min(1).describe("ID of the folder to delete")
1483
- },
3058
+ }),
1484
3059
  outputSchema: deleteFolderOutputSchema
1485
3060
  },
1486
3061
  async (args) => {
@@ -1495,20 +3070,20 @@ function registerFolderTools(server, ctx) {
1495
3070
  }
1496
3071
  );
1497
3072
  }
1498
- const renameFolderOutputSchema = {
3073
+ const renameFolderOutputSchema = z4.object({
1499
3074
  renamed: z4.literal(true),
1500
3075
  folder: folderInfoOutputSchema
1501
- };
3076
+ });
1502
3077
  if (shouldRegister("rename_folder", tools)) {
1503
3078
  server.registerTool(
1504
3079
  "rename_folder",
1505
3080
  {
1506
3081
  description: "Rename an existing mail folder. Disabled in --read-only mode.",
1507
- inputSchema: {
3082
+ inputSchema: z4.object({
1508
3083
  account: z4.string().email(),
1509
3084
  folderId: z4.string().min(1).describe("ID of the folder to rename"),
1510
3085
  newName: z4.string().min(1).describe("New display name for the folder")
1511
- },
3086
+ }),
1512
3087
  outputSchema: renameFolderOutputSchema
1513
3088
  },
1514
3089
  async (args) => {
@@ -1533,14 +3108,27 @@ function registerFolderTools(server, ctx) {
1533
3108
  import { z as z5 } from "zod";
1534
3109
  function registerOrganizeTools(server, ctx) {
1535
3110
  const { registry, tools } = ctx;
1536
- const archiveMoveSchema = {
3111
+ async function moveToWellKnown(args, destination, resultKey) {
3112
+ const { provider, account } = registry.resolveByEmail(args.account);
3113
+ await provider.moveEmail(account, args.id, destination);
3114
+ const data = { id: args.id };
3115
+ data[resultKey] = true;
3116
+ return ok(data, data);
3117
+ }
3118
+ async function markReadState(args, isRead) {
3119
+ const { provider, account } = registry.resolveByEmail(args.account);
3120
+ await provider.markRead(account, args.id, isRead);
3121
+ const data = { marked: true, id: args.id, isRead };
3122
+ return ok(data, data);
3123
+ }
3124
+ const archiveMoveSchema = z5.object({
1537
3125
  account: z5.string().email(),
1538
3126
  id: z5.string().min(1).describe("Message ID to move")
1539
- };
1540
- const archiveOutputSchema = {
3127
+ });
3128
+ const archiveOutputSchema = z5.object({
1541
3129
  archived: z5.literal(true),
1542
3130
  id: z5.string()
1543
- };
3131
+ });
1544
3132
  if (shouldRegister("archive_email", tools)) {
1545
3133
  server.registerTool(
1546
3134
  "archive_email",
@@ -1551,20 +3139,17 @@ function registerOrganizeTools(server, ctx) {
1551
3139
  },
1552
3140
  async (args) => {
1553
3141
  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);
3142
+ return await moveToWellKnown(args, "archive", "archived");
1558
3143
  } catch (err) {
1559
3144
  return fail(errMsg(err));
1560
3145
  }
1561
3146
  }
1562
3147
  );
1563
3148
  }
1564
- const trashOutputSchema = {
3149
+ const trashOutputSchema = z5.object({
1565
3150
  trashed: z5.literal(true),
1566
3151
  id: z5.string()
1567
- };
3152
+ });
1568
3153
  if (shouldRegister("trash_email", tools)) {
1569
3154
  server.registerTool(
1570
3155
  "trash_email",
@@ -1575,33 +3160,30 @@ function registerOrganizeTools(server, ctx) {
1575
3160
  },
1576
3161
  async (args) => {
1577
3162
  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);
3163
+ return await moveToWellKnown(args, "deleteditems", "trashed");
1582
3164
  } catch (err) {
1583
3165
  return fail(errMsg(err));
1584
3166
  }
1585
3167
  }
1586
3168
  );
1587
3169
  }
1588
- const moveEmailOutputSchema = {
3170
+ const moveEmailOutputSchema = z5.object({
1589
3171
  moved: z5.literal(true),
1590
3172
  id: z5.string(),
1591
3173
  destination: z5.string()
1592
- };
3174
+ });
1593
3175
  if (shouldRegister("move_email", tools)) {
1594
3176
  server.registerTool(
1595
3177
  "move_email",
1596
3178
  {
1597
3179
  description: "Move a message to any folder by well-known name (e.g. 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or custom folder ID. Disabled in --read-only mode.",
1598
- inputSchema: {
3180
+ inputSchema: z5.object({
1599
3181
  account: z5.string().email(),
1600
3182
  id: z5.string().min(1).describe("Message ID to move"),
1601
3183
  destination: z5.string().min(1).describe(
1602
3184
  "Destination folder \u2014 a well-known folder name ('archive', 'deleteditems', 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or a raw folder ID."
1603
3185
  )
1604
- },
3186
+ }),
1605
3187
  outputSchema: moveEmailOutputSchema
1606
3188
  },
1607
3189
  async (args) => {
@@ -1620,15 +3202,15 @@ function registerOrganizeTools(server, ctx) {
1620
3202
  }
1621
3203
  );
1622
3204
  }
1623
- const markReadInputSchema = {
3205
+ const markReadInputSchema = z5.object({
1624
3206
  account: z5.string().email(),
1625
3207
  id: z5.string().min(1).describe("Message ID to mark as read")
1626
- };
1627
- const markReadOutputSchema = {
3208
+ });
3209
+ const markReadOutputSchema = z5.object({
1628
3210
  marked: z5.literal(true),
1629
3211
  id: z5.string(),
1630
3212
  isRead: z5.boolean()
1631
- };
3213
+ });
1632
3214
  if (shouldRegister("mark_read", tools)) {
1633
3215
  server.registerTool(
1634
3216
  "mark_read",
@@ -1639,14 +3221,7 @@ function registerOrganizeTools(server, ctx) {
1639
3221
  },
1640
3222
  async (args) => {
1641
3223
  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);
3224
+ return await markReadState(args, true);
1650
3225
  } catch (err) {
1651
3226
  return fail(errMsg(err));
1652
3227
  }
@@ -1663,14 +3238,7 @@ function registerOrganizeTools(server, ctx) {
1663
3238
  },
1664
3239
  async (args) => {
1665
3240
  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);
3241
+ return await markReadState(args, false);
1674
3242
  } catch (err) {
1675
3243
  return fail(errMsg(err));
1676
3244
  }
@@ -1933,14 +3501,14 @@ function registerComposeTools(server, ctx) {
1933
3501
  function registerTools(server, opts) {
1934
3502
  const { store, registry, tools } = opts;
1935
3503
  registerAccountTools(server, { store, registry, tools });
1936
- registerBrowseTools(server, { store, registry, tools });
3504
+ registerBrowseTools(server, { registry, tools });
1937
3505
  registerFolderTools(server, { registry, tools });
1938
3506
  registerOrganizeTools(server, { registry, tools });
1939
3507
  registerComposeTools(server, { store, registry, tools });
1940
3508
  }
1941
3509
 
1942
3510
  // src/version.ts
1943
- var VERSION = "0.3.0";
3511
+ var VERSION = "0.4.1";
1944
3512
 
1945
3513
  // src/config.ts
1946
3514
  import { readFileSync } from "fs";
@@ -1958,8 +3526,13 @@ var outlookProviderSchema = z7.object({
1958
3526
  clientId: z7.string().optional(),
1959
3527
  tenantId: z7.string().optional()
1960
3528
  });
3529
+ var gmailProviderSchema = z7.object({
3530
+ clientId: z7.string().optional(),
3531
+ clientSecret: z7.string().optional()
3532
+ });
1961
3533
  var providersConfigSchema = z7.object({
1962
- outlook: outlookProviderSchema.optional()
3534
+ outlook: outlookProviderSchema.optional(),
3535
+ gmail: gmailProviderSchema.optional()
1963
3536
  });
1964
3537
  var rawConfigSchema = z7.object({
1965
3538
  dataDir: z7.string().optional(),
@@ -2101,7 +3674,7 @@ async function startHttp(server, host, port) {
2101
3674
  let transport = sessionId ? sessions.get(sessionId) : void 0;
2102
3675
  if (!transport) {
2103
3676
  transport = new StreamableHTTPServerTransport({
2104
- sessionIdGenerator: () => randomUUID2(),
3677
+ sessionIdGenerator: () => randomUUID6(),
2105
3678
  onsessioninitialized: (sid) => {
2106
3679
  sessions.set(sid, transport);
2107
3680
  }
@@ -2193,7 +3766,8 @@ Example hypermail-config.json:
2193
3766
  "http": { "enabled": false },
2194
3767
  "tools": { "disabled": ["send_email"] },
2195
3768
  "providers": {
2196
- "outlook": { "clientId": "\${MS_CLIENT_ID}", "tenantId": "\${MS_TENANT_ID}" }
3769
+ "outlook": { "clientId": "\${MS_CLIENT_ID}", "tenantId": "\${MS_TENANT_ID}" },
3770
+ "gmail": { "clientId": "\${GOOGLE_CLIENT_ID}", "clientSecret": "\${GOOGLE_CLIENT_SECRET}" }
2197
3771
  }
2198
3772
  }
2199
3773
  `;