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/README.md +14 -6
- package/dist/cli.js +1859 -285
- package/dist/cli.js.map +1 -1
- package/package.json +7 -2
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
|
|
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
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
658
|
-
ccRecipients
|
|
659
|
-
bccRecipients
|
|
625
|
+
toRecipients,
|
|
626
|
+
ccRecipients,
|
|
627
|
+
bccRecipients
|
|
660
628
|
};
|
|
661
629
|
if (converted.attachments.length > 0) {
|
|
662
|
-
|
|
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(
|
|
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
|
-
|
|
763
|
-
|
|
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
|
|
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:
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
|
780
|
-
|
|
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
|
|
783
|
-
|
|
784
|
-
|
|
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/
|
|
788
|
-
var
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
800
|
-
|
|
2365
|
+
// ── browse ──
|
|
2366
|
+
async listEmails(account, opts) {
|
|
2367
|
+
return listEmails2(this.clients, account, opts);
|
|
801
2368
|
}
|
|
802
|
-
async searchEmails(
|
|
803
|
-
|
|
2369
|
+
async searchEmails(account, query, opts) {
|
|
2370
|
+
return searchEmails2(this.clients, account, query, opts);
|
|
804
2371
|
}
|
|
805
|
-
async readEmail(
|
|
806
|
-
|
|
2372
|
+
async readEmail(account, id) {
|
|
2373
|
+
return readEmail2(this.clients, account, id);
|
|
807
2374
|
}
|
|
808
|
-
async readAttachment(
|
|
809
|
-
|
|
2375
|
+
async readAttachment(account, messageId, attachmentId) {
|
|
2376
|
+
return readAttachment2(this.clients, account, messageId, attachmentId);
|
|
810
2377
|
}
|
|
811
|
-
|
|
812
|
-
|
|
2378
|
+
// ── compose ──
|
|
2379
|
+
async sendEmail(account, msg) {
|
|
2380
|
+
return sendEmail2(this.clients, account, msg);
|
|
813
2381
|
}
|
|
814
|
-
async saveDraft(
|
|
815
|
-
|
|
2382
|
+
async saveDraft(account, msg) {
|
|
2383
|
+
return saveDraft2(this.clients, account, msg);
|
|
816
2384
|
}
|
|
817
|
-
async updateDraft(
|
|
818
|
-
|
|
2385
|
+
async updateDraft(account, id, update) {
|
|
2386
|
+
return updateDraft2(this.clients, account, id, update);
|
|
819
2387
|
}
|
|
820
|
-
async moveEmail(
|
|
821
|
-
|
|
2388
|
+
async moveEmail(account, id, destinationId) {
|
|
2389
|
+
return moveEmail2(this.clients, account, id, destinationId);
|
|
822
2390
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
throw new Error(NOT_IMPLEMENTED);
|
|
2391
|
+
async sendDraft(account, id) {
|
|
2392
|
+
return sendDraft2(this.clients, account, id);
|
|
826
2393
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
832
|
-
|
|
2404
|
+
// ── organize ──
|
|
2405
|
+
async markRead(account, id, isRead) {
|
|
2406
|
+
return markRead2(this.clients, account, id, isRead);
|
|
833
2407
|
}
|
|
834
|
-
|
|
835
|
-
|
|
2408
|
+
// ── folders ──
|
|
2409
|
+
async listFolders(account, opts) {
|
|
2410
|
+
return listFolders2(this.clients, account, opts);
|
|
836
2411
|
}
|
|
837
|
-
async createFolder(
|
|
838
|
-
|
|
2412
|
+
async createFolder(account, input) {
|
|
2413
|
+
return createFolder2(this.clients, account, input);
|
|
839
2414
|
}
|
|
840
|
-
async renameFolder(
|
|
841
|
-
|
|
2415
|
+
async renameFolder(account, folderId, newName) {
|
|
2416
|
+
return renameFolder2(this.clients, account, folderId, newName);
|
|
842
2417
|
}
|
|
843
|
-
async deleteFolder(
|
|
844
|
-
|
|
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:
|
|
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.
|
|
1016
|
-
z2.
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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.
|
|
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: () =>
|
|
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
|
`;
|