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