hypermail-mcp 0.4.0 → 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 +45 -8
- package/dist/cli.js +2873 -683
- 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,9 +201,14 @@ var DEFAULT_SCOPES = [
|
|
|
182
201
|
"Mail.ReadWrite",
|
|
183
202
|
"Mail.Send"
|
|
184
203
|
];
|
|
185
|
-
function
|
|
186
|
-
|
|
187
|
-
const
|
|
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
|
+
}
|
|
209
|
+
function makeConfig(prevCacheJson, clientIdOverride, tenantOverride) {
|
|
210
|
+
const clientId = clientIdOverride || process.env.MS_CLIENT_ID || DEFAULT_CLIENT_ID;
|
|
211
|
+
const tenant = tenantOverride || process.env.MS_TENANT_ID || "common";
|
|
188
212
|
return {
|
|
189
213
|
auth: {
|
|
190
214
|
clientId,
|
|
@@ -196,15 +220,15 @@ function makeConfig(prevCacheJson) {
|
|
|
196
220
|
} : void 0
|
|
197
221
|
};
|
|
198
222
|
}
|
|
199
|
-
function buildPca(prevCacheJson) {
|
|
200
|
-
const pca = new PublicClientApplication(makeConfig(prevCacheJson));
|
|
223
|
+
function buildPca(prevCacheJson, clientIdOverride, tenantOverride) {
|
|
224
|
+
const pca = new PublicClientApplication(makeConfig(prevCacheJson, clientIdOverride, tenantOverride));
|
|
201
225
|
if (prevCacheJson) {
|
|
202
226
|
pca.getTokenCache().deserialize(prevCacheJson);
|
|
203
227
|
}
|
|
204
228
|
return pca;
|
|
205
229
|
}
|
|
206
|
-
function beginDeviceCode(scopes = DEFAULT_SCOPES) {
|
|
207
|
-
const pca = buildPca();
|
|
230
|
+
function beginDeviceCode(scopes = DEFAULT_SCOPES, clientIdOverride, tenantOverride) {
|
|
231
|
+
const pca = buildPca(void 0, clientIdOverride, tenantOverride);
|
|
208
232
|
let resolve;
|
|
209
233
|
let reject;
|
|
210
234
|
const result = new Promise(
|
|
@@ -284,8 +308,8 @@ async function awaitDeviceCodeReady(b) {
|
|
|
284
308
|
const r = b._ready;
|
|
285
309
|
await r;
|
|
286
310
|
}
|
|
287
|
-
async function acquireAccessToken(tokens, scopes = DEFAULT_SCOPES) {
|
|
288
|
-
const pca = buildPca(tokens.msalCache);
|
|
311
|
+
async function acquireAccessToken(tokens, scopes = DEFAULT_SCOPES, clientIdOverride, tenantOverride) {
|
|
312
|
+
const pca = buildPca(tokens.msalCache, clientIdOverride, tenantOverride);
|
|
289
313
|
const cache = pca.getTokenCache();
|
|
290
314
|
const account = await cache.getAccountByHomeId(tokens.homeAccountId) ?? (await cache.getAllAccounts()).find((a) => a.username === tokens.username);
|
|
291
315
|
if (!account) {
|
|
@@ -305,10 +329,14 @@ async function acquireAccessToken(tokens, scopes = DEFAULT_SCOPES) {
|
|
|
305
329
|
|
|
306
330
|
// src/providers/outlook/client.ts
|
|
307
331
|
var OutlookClientFactory = class {
|
|
308
|
-
constructor(store) {
|
|
332
|
+
constructor(store, clientId, tenantId) {
|
|
309
333
|
this.store = store;
|
|
334
|
+
this.clientId = clientId;
|
|
335
|
+
this.tenantId = tenantId;
|
|
310
336
|
}
|
|
311
337
|
store;
|
|
338
|
+
clientId;
|
|
339
|
+
tenantId;
|
|
312
340
|
cache = /* @__PURE__ */ new Map();
|
|
313
341
|
get(account) {
|
|
314
342
|
const key = account.email.toLowerCase();
|
|
@@ -318,8 +346,18 @@ var OutlookClientFactory = class {
|
|
|
318
346
|
const provider = {
|
|
319
347
|
getAccessToken: async () => {
|
|
320
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
|
+
}
|
|
321
354
|
const tokens = fresh.tokens;
|
|
322
|
-
const { accessToken, tokens: nextTokens } = await acquireAccessToken(
|
|
355
|
+
const { accessToken, tokens: nextTokens } = await acquireAccessToken(
|
|
356
|
+
tokens,
|
|
357
|
+
void 0,
|
|
358
|
+
this.clientId,
|
|
359
|
+
this.tenantId
|
|
360
|
+
);
|
|
323
361
|
if (nextTokens.msalCache !== tokens.msalCache) {
|
|
324
362
|
store.upsertAccount({
|
|
325
363
|
...fresh,
|
|
@@ -342,37 +380,35 @@ var OutlookClientFactory = class {
|
|
|
342
380
|
|
|
343
381
|
// src/providers/outlook/index.ts
|
|
344
382
|
function convertInlineImages(body) {
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
contentId,
|
|
355
|
-
contentBytes: b64,
|
|
356
|
-
isInline: true
|
|
357
|
-
});
|
|
358
|
-
return `src="cid:${contentId}"`;
|
|
359
|
-
});
|
|
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
|
+
}));
|
|
360
392
|
return { body: transformed, attachments };
|
|
361
393
|
}
|
|
362
394
|
var OutlookProvider = class {
|
|
363
395
|
constructor(opts) {
|
|
364
396
|
this.opts = opts;
|
|
365
|
-
this.
|
|
397
|
+
this.clientId = opts.clientId;
|
|
398
|
+
this.tenantId = opts.tenantId;
|
|
399
|
+
this.clients = new OutlookClientFactory(opts.store, opts.clientId, opts.tenantId);
|
|
366
400
|
}
|
|
367
401
|
opts;
|
|
368
402
|
id = "outlook";
|
|
369
403
|
clients;
|
|
370
404
|
pending = /* @__PURE__ */ new Map();
|
|
405
|
+
clientId;
|
|
406
|
+
tenantId;
|
|
371
407
|
// ---------- account lifecycle ----------
|
|
372
408
|
async addAccount(input) {
|
|
373
|
-
const begin = beginDeviceCode();
|
|
409
|
+
const begin = beginDeviceCode(void 0, this.clientId, this.tenantId);
|
|
374
410
|
await awaitDeviceCodeReady(begin);
|
|
375
|
-
const handle =
|
|
411
|
+
const handle = randomUUID2();
|
|
376
412
|
const flow = {
|
|
377
413
|
begin,
|
|
378
414
|
emailHint: input.email,
|
|
@@ -440,7 +476,7 @@ var OutlookProvider = class {
|
|
|
440
476
|
const folder = opts.folder ?? "inbox";
|
|
441
477
|
const filterParts = [];
|
|
442
478
|
if (opts.unreadOnly) filterParts.push("isRead eq false");
|
|
443
|
-
let req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).top(limit).select([
|
|
479
|
+
let req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).top(limit).skip(opts.skip ?? 0).select([
|
|
444
480
|
"id",
|
|
445
481
|
"subject",
|
|
446
482
|
"from",
|
|
@@ -452,7 +488,10 @@ var OutlookProvider = class {
|
|
|
452
488
|
].join(",")).orderby("receivedDateTime DESC");
|
|
453
489
|
if (filterParts.length > 0) req = req.filter(filterParts.join(" and "));
|
|
454
490
|
const res = await req.get();
|
|
455
|
-
return
|
|
491
|
+
return {
|
|
492
|
+
items: res.value.map((m) => mapSummary(m, folder)),
|
|
493
|
+
hasMore: !!res["@odata.nextLink"]
|
|
494
|
+
};
|
|
456
495
|
}
|
|
457
496
|
async searchEmails(account, query, opts) {
|
|
458
497
|
const client = this.clients.get(account);
|
|
@@ -543,83 +582,25 @@ var OutlookProvider = class {
|
|
|
543
582
|
}
|
|
544
583
|
return draft.id;
|
|
545
584
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
"inReplyTo and forwardMessageId are mutually exclusive \u2014 use one or the other"
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
const converted = convertInlineImages(msg.body);
|
|
554
|
-
if (msg.forwardMessageId) {
|
|
555
|
-
const draftId = await this.buildDraftFromReference(
|
|
556
|
-
client,
|
|
557
|
-
`/me/messages/${encodeURIComponent(msg.forwardMessageId)}/createForward`,
|
|
558
|
-
{
|
|
559
|
-
message: {
|
|
560
|
-
toRecipients: msg.to.map(toRecipient),
|
|
561
|
-
ccRecipients: (msg.cc ?? []).map(toRecipient),
|
|
562
|
-
bccRecipients: (msg.bcc ?? []).map(toRecipient)
|
|
563
|
-
},
|
|
564
|
-
comment: ""
|
|
565
|
-
},
|
|
566
|
-
converted
|
|
567
|
-
);
|
|
568
|
-
await client.api(`/me/messages/${draftId}/send`).post({});
|
|
569
|
-
return { id: draftId };
|
|
570
|
-
}
|
|
571
|
-
if (msg.inReplyTo) {
|
|
572
|
-
const createEndpoint = msg.replyAll ? `/me/messages/${encodeURIComponent(msg.inReplyTo)}/createReplyAll` : `/me/messages/${encodeURIComponent(msg.inReplyTo)}/createReply`;
|
|
573
|
-
const draftId = await this.buildDraftFromReference(
|
|
574
|
-
client,
|
|
575
|
-
createEndpoint,
|
|
576
|
-
{},
|
|
577
|
-
converted
|
|
578
|
-
);
|
|
579
|
-
await client.api(`/me/messages/${draftId}/send`).post({});
|
|
580
|
-
return { id: draftId };
|
|
581
|
-
}
|
|
582
|
-
const payload = {
|
|
583
|
-
message: {
|
|
584
|
-
subject: msg.subject,
|
|
585
|
-
body: {
|
|
586
|
-
contentType: msg.isHtml ? "HTML" : "Text",
|
|
587
|
-
content: converted.body
|
|
588
|
-
},
|
|
589
|
-
toRecipients: msg.to.map(toRecipient),
|
|
590
|
-
ccRecipients: (msg.cc ?? []).map(toRecipient),
|
|
591
|
-
bccRecipients: (msg.bcc ?? []).map(toRecipient)
|
|
592
|
-
},
|
|
593
|
-
saveToSentItems: true
|
|
594
|
-
};
|
|
595
|
-
if (converted.attachments.length > 0) {
|
|
596
|
-
payload.message.attachments = converted.attachments;
|
|
597
|
-
}
|
|
598
|
-
await client.api("/me/sendMail").post(payload);
|
|
599
|
-
return { id: "" };
|
|
600
|
-
}
|
|
601
|
-
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) {
|
|
602
589
|
const client = this.clients.get(account);
|
|
603
|
-
if (msg.inReplyTo && msg.forwardMessageId) {
|
|
604
|
-
throw new Error(
|
|
605
|
-
"inReplyTo and forwardMessageId are mutually exclusive \u2014 use one or the other"
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
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);
|
|
609
594
|
if (msg.forwardMessageId) {
|
|
610
595
|
const draftId = await this.buildDraftFromReference(
|
|
611
596
|
client,
|
|
612
597
|
`/me/messages/${encodeURIComponent(msg.forwardMessageId)}/createForward`,
|
|
613
|
-
{
|
|
614
|
-
message: {
|
|
615
|
-
toRecipients: msg.to.map(toRecipient),
|
|
616
|
-
ccRecipients: (msg.cc ?? []).map(toRecipient),
|
|
617
|
-
bccRecipients: (msg.bcc ?? []).map(toRecipient)
|
|
618
|
-
},
|
|
619
|
-
comment: ""
|
|
620
|
-
},
|
|
598
|
+
{ message: { toRecipients, ccRecipients, bccRecipients }, comment: "" },
|
|
621
599
|
converted
|
|
622
600
|
);
|
|
601
|
+
if (mode === "send") {
|
|
602
|
+
await client.api(`/me/messages/${draftId}/send`).post({});
|
|
603
|
+
}
|
|
623
604
|
return { id: draftId };
|
|
624
605
|
}
|
|
625
606
|
if (msg.inReplyTo) {
|
|
@@ -630,24 +611,40 @@ var OutlookProvider = class {
|
|
|
630
611
|
{},
|
|
631
612
|
converted
|
|
632
613
|
);
|
|
614
|
+
if (mode === "send") {
|
|
615
|
+
await client.api(`/me/messages/${draftId}/send`).post({});
|
|
616
|
+
}
|
|
633
617
|
return { id: draftId };
|
|
634
618
|
}
|
|
635
|
-
const
|
|
619
|
+
const messagePayload = {
|
|
636
620
|
subject: msg.subject,
|
|
637
621
|
body: {
|
|
638
622
|
contentType: msg.isHtml ? "HTML" : "Text",
|
|
639
623
|
content: converted.body
|
|
640
624
|
},
|
|
641
|
-
toRecipients
|
|
642
|
-
ccRecipients
|
|
643
|
-
bccRecipients
|
|
625
|
+
toRecipients,
|
|
626
|
+
ccRecipients,
|
|
627
|
+
bccRecipients
|
|
644
628
|
};
|
|
645
629
|
if (converted.attachments.length > 0) {
|
|
646
|
-
|
|
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: "" };
|
|
647
638
|
}
|
|
648
|
-
const draft = await client.api("/me/messages").post(
|
|
639
|
+
const draft = await client.api("/me/messages").post(messagePayload);
|
|
649
640
|
return { id: draft.id };
|
|
650
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
|
+
}
|
|
651
648
|
async updateDraft(account, id, update) {
|
|
652
649
|
const client = this.clients.get(account);
|
|
653
650
|
const payload = {};
|
|
@@ -680,7 +677,67 @@ var OutlookProvider = class {
|
|
|
680
677
|
const client = this.clients.get(account);
|
|
681
678
|
await client.api(`/me/messages/${encodeURIComponent(id)}/move`).post({ destinationId });
|
|
682
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
|
+
}
|
|
683
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
|
+
}
|
|
684
741
|
function mapRecipient(r) {
|
|
685
742
|
return {
|
|
686
743
|
name: r.emailAddress?.name,
|
|
@@ -708,133 +765,1750 @@ function clampLimit(v, dflt, max) {
|
|
|
708
765
|
return Math.min(v, max);
|
|
709
766
|
}
|
|
710
767
|
|
|
711
|
-
// src/providers/imap/
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
throw new Error(NOT_IMPLEMENTED);
|
|
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
|
+
);
|
|
725
781
|
}
|
|
726
|
-
|
|
727
|
-
|
|
782
|
+
return account.tokens;
|
|
783
|
+
}
|
|
784
|
+
var ImapClient = class {
|
|
785
|
+
constructor(tokens) {
|
|
786
|
+
this.tokens = tokens;
|
|
728
787
|
}
|
|
729
|
-
|
|
730
|
-
|
|
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;
|
|
731
814
|
}
|
|
732
|
-
|
|
733
|
-
|
|
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;
|
|
734
828
|
}
|
|
735
|
-
|
|
736
|
-
|
|
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
|
+
}
|
|
737
841
|
}
|
|
738
|
-
|
|
739
|
-
|
|
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
|
+
}
|
|
740
856
|
}
|
|
741
|
-
|
|
742
|
-
|
|
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;
|
|
743
868
|
}
|
|
744
|
-
|
|
745
|
-
|
|
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
|
+
}
|
|
746
878
|
}
|
|
747
879
|
};
|
|
748
880
|
|
|
749
|
-
// src/providers/
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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}`);
|
|
758
914
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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));
|
|
765
932
|
}
|
|
766
|
-
return { provider: get(account.provider), account };
|
|
767
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;
|
|
768
951
|
return {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
|
772
971
|
};
|
|
773
972
|
}
|
|
774
973
|
|
|
775
|
-
// src/
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
+
});
|
|
783
1008
|
}
|
|
784
|
-
function
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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}`);
|
|
790
1043
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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");
|
|
795
1058
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
+
}
|
|
800
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
|
+
});
|
|
801
1141
|
}
|
|
1142
|
+
return results;
|
|
802
1143
|
}
|
|
803
1144
|
|
|
804
|
-
// src/
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
|
810
1172
|
};
|
|
811
|
-
|
|
812
|
-
|
|
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());
|
|
813
1183
|
}
|
|
814
|
-
|
|
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 };
|
|
815
1199
|
}
|
|
816
|
-
function
|
|
1200
|
+
function completeAddAccount() {
|
|
817
1201
|
return {
|
|
818
|
-
|
|
819
|
-
|
|
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."
|
|
820
1204
|
};
|
|
821
1205
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
|
2104
|
+
}
|
|
2105
|
+
});
|
|
2106
|
+
return { id: sendRes.data.id ?? "" };
|
|
2107
|
+
}
|
|
2108
|
+
async function saveDraft2(clients, account, msg) {
|
|
2109
|
+
const { gmail } = clients.get(account);
|
|
2110
|
+
const { raw } = await buildRawMessage2(account, msg);
|
|
2111
|
+
let threadId;
|
|
2112
|
+
if (msg.inReplyTo) {
|
|
2113
|
+
try {
|
|
2114
|
+
const refRes = await gmail.users.messages.get({
|
|
2115
|
+
userId: "me",
|
|
2116
|
+
id: msg.inReplyTo,
|
|
2117
|
+
format: "minimal"
|
|
2118
|
+
});
|
|
2119
|
+
threadId = refRes.data.threadId ?? void 0;
|
|
2120
|
+
} catch {
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
const draftRes = await gmail.users.drafts.create({
|
|
2124
|
+
userId: "me",
|
|
2125
|
+
requestBody: {
|
|
2126
|
+
message: { raw, threadId }
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
return { id: draftRes.data.message?.id ?? draftRes.data.id ?? "" };
|
|
2130
|
+
}
|
|
2131
|
+
async function updateDraft2(clients, account, id, update) {
|
|
2132
|
+
const { gmail } = clients.get(account);
|
|
2133
|
+
const draftRes = await gmail.users.drafts.get({
|
|
2134
|
+
userId: "me",
|
|
2135
|
+
id,
|
|
2136
|
+
format: "raw"
|
|
2137
|
+
});
|
|
2138
|
+
const existingMessage = draftRes.data.message;
|
|
2139
|
+
if (!existingMessage?.raw) {
|
|
2140
|
+
throw new Error(`draft not found: ${id}`);
|
|
2141
|
+
}
|
|
2142
|
+
const existingHeaders = existingMessage.payload?.headers ?? [];
|
|
2143
|
+
const origSubject = findHeader(existingHeaders, "Subject") ?? "";
|
|
2144
|
+
const rawStr = Buffer2.from(
|
|
2145
|
+
existingMessage.raw.replace(/-/g, "+").replace(/_/g, "/"),
|
|
2146
|
+
"base64"
|
|
2147
|
+
).toString("utf-8");
|
|
2148
|
+
const existingTo = update.to ?? mapHeaderAddr(findHeader(existingHeaders, "To"));
|
|
2149
|
+
const existingCc = update.cc ?? mapHeaderAddr(findHeader(existingHeaders, "Cc"));
|
|
2150
|
+
const existingBcc = update.bcc ?? mapHeaderAddr(findHeader(existingHeaders, "Bcc"));
|
|
2151
|
+
const { raw } = await buildRawMessage2(account, {
|
|
2152
|
+
to: existingTo,
|
|
2153
|
+
subject: update.subject ?? origSubject,
|
|
2154
|
+
body: update.body ?? "",
|
|
2155
|
+
isHtml: update.isHtml,
|
|
2156
|
+
cc: existingCc.length > 0 ? existingCc : void 0,
|
|
2157
|
+
bcc: existingBcc.length > 0 ? existingBcc : void 0
|
|
2158
|
+
});
|
|
2159
|
+
const updated = await gmail.users.drafts.update({
|
|
2160
|
+
userId: "me",
|
|
2161
|
+
id,
|
|
2162
|
+
requestBody: {
|
|
2163
|
+
message: {
|
|
2164
|
+
raw,
|
|
2165
|
+
threadId: existingMessage.threadId ?? void 0
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
return { id: updated.data.message?.id ?? updated.data.id ?? id };
|
|
2170
|
+
}
|
|
2171
|
+
async function moveEmail2(clients, account, id, destinationId) {
|
|
2172
|
+
const { gmail } = clients.get(account);
|
|
2173
|
+
const { addLabelIds, removeLabelIds } = resolveLabelsForMove(destinationId);
|
|
2174
|
+
await gmail.users.messages.modify({
|
|
2175
|
+
userId: "me",
|
|
2176
|
+
id,
|
|
2177
|
+
requestBody: { addLabelIds, removeLabelIds }
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
async function sendDraft2(clients, account, id) {
|
|
2181
|
+
const { gmail } = clients.get(account);
|
|
2182
|
+
const res = await gmail.users.drafts.send({
|
|
2183
|
+
userId: "me",
|
|
2184
|
+
requestBody: { id }
|
|
2185
|
+
});
|
|
2186
|
+
return { id: res.data.id ?? id };
|
|
2187
|
+
}
|
|
2188
|
+
async function addAttachmentToDraft2(clients, account, draftId, name, contentBytes, contentType) {
|
|
2189
|
+
const { gmail } = clients.get(account);
|
|
2190
|
+
const draftRes = await gmail.users.drafts.get({
|
|
2191
|
+
userId: "me",
|
|
2192
|
+
id: draftId,
|
|
2193
|
+
format: "raw"
|
|
2194
|
+
});
|
|
2195
|
+
const existingMessage = draftRes.data.message;
|
|
2196
|
+
if (!existingMessage?.raw) {
|
|
2197
|
+
throw new Error(`draft not found: ${draftId}`);
|
|
2198
|
+
}
|
|
2199
|
+
const rawStr = Buffer2.from(
|
|
2200
|
+
existingMessage.raw.replace(/-/g, "+").replace(/_/g, "/"),
|
|
2201
|
+
"base64"
|
|
2202
|
+
).toString("utf-8");
|
|
2203
|
+
const newRawStr = await new Promise((resolve, reject) => {
|
|
2204
|
+
const mc = new MailComposer3({
|
|
2205
|
+
raw: rawStr,
|
|
2206
|
+
attachments: [
|
|
2207
|
+
{
|
|
2208
|
+
filename: name,
|
|
2209
|
+
content: Buffer2.from(contentBytes, "base64"),
|
|
2210
|
+
contentType: contentType ?? "application/octet-stream"
|
|
2211
|
+
}
|
|
2212
|
+
]
|
|
2213
|
+
});
|
|
2214
|
+
mc.compile().build((err, buf) => {
|
|
2215
|
+
if (err) reject(err);
|
|
2216
|
+
else resolve(buf.toString("utf-8"));
|
|
2217
|
+
});
|
|
2218
|
+
});
|
|
2219
|
+
const updated = await gmail.users.drafts.update({
|
|
2220
|
+
userId: "me",
|
|
2221
|
+
id: draftId,
|
|
2222
|
+
requestBody: {
|
|
2223
|
+
message: {
|
|
2224
|
+
raw: base64urlEncode(Buffer2.from(newRawStr, "utf-8")),
|
|
2225
|
+
threadId: existingMessage.threadId ?? void 0
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
return {
|
|
2230
|
+
id: updated.data.message?.id ?? updated.data.id ?? draftId,
|
|
2231
|
+
attachment: {
|
|
2232
|
+
id: randomUUID4(),
|
|
2233
|
+
name,
|
|
2234
|
+
contentType: contentType ?? "application/octet-stream"
|
|
2235
|
+
}
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
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
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
async function createFolder2(clients, account, input) {
|
|
2250
|
+
const { gmail } = clients.get(account);
|
|
2251
|
+
const created = await gmail.users.labels.create({
|
|
2252
|
+
userId: "me",
|
|
2253
|
+
requestBody: {
|
|
2254
|
+
name: input.displayName,
|
|
2255
|
+
messageListVisibility: "show",
|
|
2256
|
+
labelListVisibility: "labelShow"
|
|
2257
|
+
}
|
|
2258
|
+
});
|
|
2259
|
+
return mapFolder2(created.data);
|
|
2260
|
+
}
|
|
2261
|
+
async function renameFolder2(clients, account, folderId, newName) {
|
|
2262
|
+
const { gmail } = clients.get(account);
|
|
2263
|
+
const updated = await gmail.users.labels.patch({
|
|
2264
|
+
userId: "me",
|
|
2265
|
+
id: folderId,
|
|
2266
|
+
requestBody: { name: newName }
|
|
2267
|
+
});
|
|
2268
|
+
return mapFolder2(updated.data);
|
|
2269
|
+
}
|
|
2270
|
+
async function deleteFolder2(clients, account, folderId) {
|
|
2271
|
+
const { gmail } = clients.get(account);
|
|
2272
|
+
await gmail.users.labels.delete({
|
|
2273
|
+
userId: "me",
|
|
2274
|
+
id: folderId
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
|
|
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
|
+
);
|
|
2289
|
+
}
|
|
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" };
|
|
2364
|
+
}
|
|
2365
|
+
// ── browse ──
|
|
2366
|
+
async listEmails(account, opts) {
|
|
2367
|
+
return listEmails2(this.clients, account, opts);
|
|
2368
|
+
}
|
|
2369
|
+
async searchEmails(account, query, opts) {
|
|
2370
|
+
return searchEmails2(this.clients, account, query, opts);
|
|
2371
|
+
}
|
|
2372
|
+
async readEmail(account, id) {
|
|
2373
|
+
return readEmail2(this.clients, account, id);
|
|
2374
|
+
}
|
|
2375
|
+
async readAttachment(account, messageId, attachmentId) {
|
|
2376
|
+
return readAttachment2(this.clients, account, messageId, attachmentId);
|
|
2377
|
+
}
|
|
2378
|
+
// ── compose ──
|
|
2379
|
+
async sendEmail(account, msg) {
|
|
2380
|
+
return sendEmail2(this.clients, account, msg);
|
|
2381
|
+
}
|
|
2382
|
+
async saveDraft(account, msg) {
|
|
2383
|
+
return saveDraft2(this.clients, account, msg);
|
|
2384
|
+
}
|
|
2385
|
+
async updateDraft(account, id, update) {
|
|
2386
|
+
return updateDraft2(this.clients, account, id, update);
|
|
2387
|
+
}
|
|
2388
|
+
async moveEmail(account, id, destinationId) {
|
|
2389
|
+
return moveEmail2(this.clients, account, id, destinationId);
|
|
2390
|
+
}
|
|
2391
|
+
async sendDraft(account, id) {
|
|
2392
|
+
return sendDraft2(this.clients, account, id);
|
|
2393
|
+
}
|
|
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
|
+
);
|
|
2403
|
+
}
|
|
2404
|
+
// ── organize ──
|
|
2405
|
+
async markRead(account, id, isRead) {
|
|
2406
|
+
return markRead2(this.clients, account, id, isRead);
|
|
2407
|
+
}
|
|
2408
|
+
// ── folders ──
|
|
2409
|
+
async listFolders(account, opts) {
|
|
2410
|
+
return listFolders2(this.clients, account, opts);
|
|
2411
|
+
}
|
|
2412
|
+
async createFolder(account, input) {
|
|
2413
|
+
return createFolder2(this.clients, account, input);
|
|
2414
|
+
}
|
|
2415
|
+
async renameFolder(account, folderId, newName) {
|
|
2416
|
+
return renameFolder2(this.clients, account, folderId, newName);
|
|
2417
|
+
}
|
|
2418
|
+
async deleteFolder(account, folderId) {
|
|
2419
|
+
return deleteFolder2(this.clients, account, folderId);
|
|
2420
|
+
}
|
|
2421
|
+
};
|
|
2422
|
+
|
|
2423
|
+
// src/providers/registry.ts
|
|
2424
|
+
function buildRegistry(opts) {
|
|
2425
|
+
const outlookCfg = opts.providers?.outlook;
|
|
2426
|
+
const providers = /* @__PURE__ */ new Map();
|
|
2427
|
+
providers.set("outlook", new OutlookProvider({
|
|
2428
|
+
store: opts.store,
|
|
2429
|
+
clientId: outlookCfg?.clientId,
|
|
2430
|
+
tenantId: outlookCfg?.tenantId
|
|
2431
|
+
}));
|
|
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
|
+
}));
|
|
2439
|
+
function get(id) {
|
|
2440
|
+
const p = providers.get(id);
|
|
2441
|
+
if (!p) throw new Error(`unknown provider: ${id}`);
|
|
2442
|
+
return p;
|
|
2443
|
+
}
|
|
2444
|
+
function resolveByEmail(email) {
|
|
2445
|
+
const account = opts.store.getAccount(email);
|
|
2446
|
+
if (!account) {
|
|
2447
|
+
throw new Error(
|
|
2448
|
+
`no account registered for "${email}". Call add_account first.`
|
|
2449
|
+
);
|
|
2450
|
+
}
|
|
2451
|
+
return { provider: get(account.provider), account };
|
|
2452
|
+
}
|
|
2453
|
+
return {
|
|
2454
|
+
get,
|
|
2455
|
+
resolveByEmail,
|
|
2456
|
+
list: () => Array.from(providers.values())
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// src/tools/shared.ts
|
|
2461
|
+
import { z } from "zod";
|
|
2462
|
+
function ok(data, structuredContent) {
|
|
2463
|
+
const result = {
|
|
2464
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
2465
|
+
};
|
|
2466
|
+
if (structuredContent !== void 0) {
|
|
2467
|
+
result.structuredContent = structuredContent;
|
|
2468
|
+
}
|
|
2469
|
+
return result;
|
|
2470
|
+
}
|
|
2471
|
+
function fail(message) {
|
|
2472
|
+
return {
|
|
2473
|
+
isError: true,
|
|
2474
|
+
content: [{ type: "text", text: message }]
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
function errMsg(err) {
|
|
2478
|
+
if (err instanceof Error) return err.message;
|
|
2479
|
+
return String(err);
|
|
2480
|
+
}
|
|
2481
|
+
var providerIdEnum = z.enum(["outlook", "imap", "gmail"]);
|
|
2482
|
+
var emailAddrSchema = z.object({
|
|
2483
|
+
address: z.string().email(),
|
|
2484
|
+
name: z.string().optional()
|
|
2485
|
+
});
|
|
2486
|
+
var emailAddrOutputSchema = z.object({
|
|
2487
|
+
name: z.string().optional(),
|
|
2488
|
+
address: z.string()
|
|
829
2489
|
});
|
|
830
2490
|
var accountSummaryOutputSchema = z.object({
|
|
831
2491
|
email: z.string(),
|
|
832
|
-
provider:
|
|
2492
|
+
provider: providerIdEnum,
|
|
833
2493
|
displayName: z.string().optional(),
|
|
834
2494
|
addedAt: z.string(),
|
|
835
2495
|
hasSignature: z.boolean(),
|
|
836
2496
|
hasStyle: z.boolean()
|
|
837
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
|
+
});
|
|
838
2512
|
var emailSummaryOutputSchema = z.object({
|
|
839
2513
|
id: z.string(),
|
|
840
2514
|
subject: z.string(),
|
|
@@ -852,374 +2526,680 @@ var attachmentMetaOutputSchema = z.object({
|
|
|
852
2526
|
contentType: z.string().optional(),
|
|
853
2527
|
size: z.number().optional()
|
|
854
2528
|
});
|
|
855
|
-
var
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
2529
|
+
var folderInfoOutputSchema = z.object({
|
|
2530
|
+
id: z.string(),
|
|
2531
|
+
displayName: z.string(),
|
|
2532
|
+
parentFolderId: z.string().optional(),
|
|
2533
|
+
childFolderCount: z.number(),
|
|
2534
|
+
totalItemCount: z.number(),
|
|
2535
|
+
unreadItemCount: z.number()
|
|
859
2536
|
});
|
|
860
|
-
function
|
|
861
|
-
const {
|
|
2537
|
+
function composeBody(input) {
|
|
2538
|
+
const { body, isHtml = false, signature, style, includeSignature } = input;
|
|
2539
|
+
const hasSignature = includeSignature && !!signature;
|
|
2540
|
+
const hasStyle = !!(style && (style.fontFamily || style.fontSize || style.fontColor));
|
|
2541
|
+
if (!hasSignature && !hasStyle) {
|
|
2542
|
+
return { body, isHtml };
|
|
2543
|
+
}
|
|
2544
|
+
const styleAttr = hasStyle ? buildStyleAttr(style) : "";
|
|
2545
|
+
if (isHtml) {
|
|
2546
|
+
let result2 = hasStyle ? `<div style="${styleAttr}">${body}</div>` : body;
|
|
2547
|
+
if (hasSignature) result2 += `
|
|
2548
|
+
<div class="signature">${signature}</div>`;
|
|
2549
|
+
return { body: result2, isHtml: true };
|
|
2550
|
+
}
|
|
2551
|
+
const escaped = escapeHtml(body);
|
|
2552
|
+
let result = `<div style="${styleAttr}">${escaped}</div>`;
|
|
2553
|
+
if (hasSignature) result += `
|
|
2554
|
+
<div class="signature">${signature}</div>`;
|
|
2555
|
+
return { body: result, isHtml: true };
|
|
2556
|
+
}
|
|
2557
|
+
function buildStyleAttr(style) {
|
|
2558
|
+
const parts = [];
|
|
2559
|
+
if (style.fontFamily) parts.push(`font-family: ${style.fontFamily}`);
|
|
2560
|
+
if (style.fontSize) parts.push(`font-size: ${style.fontSize}`);
|
|
2561
|
+
if (style.fontColor) parts.push(`color: ${style.fontColor}`);
|
|
2562
|
+
return parts.join("; ");
|
|
2563
|
+
}
|
|
2564
|
+
function escapeHtml(text) {
|
|
2565
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/\n/g, "<br>");
|
|
2566
|
+
}
|
|
2567
|
+
function shouldRegister(name, tools) {
|
|
2568
|
+
if (tools.enabledTools) {
|
|
2569
|
+
return tools.enabledTools.has(name);
|
|
2570
|
+
}
|
|
2571
|
+
if (tools.disabledTools) {
|
|
2572
|
+
return !tools.disabledTools.has(name);
|
|
2573
|
+
}
|
|
2574
|
+
return true;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
// src/tools/accounts.ts
|
|
2578
|
+
import { z as z2 } from "zod";
|
|
2579
|
+
function registerAccountTools(server, ctx) {
|
|
2580
|
+
const { store, registry, tools } = ctx;
|
|
862
2581
|
const listAccountsOutputSchema = {
|
|
863
|
-
accounts:
|
|
2582
|
+
accounts: z2.array(accountSummaryOutputSchema)
|
|
864
2583
|
};
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
2584
|
+
if (shouldRegister("list_accounts", tools)) {
|
|
2585
|
+
server.registerTool(
|
|
2586
|
+
"list_accounts",
|
|
2587
|
+
{
|
|
2588
|
+
description: "List all email accounts known to this server (no secrets). Use the returned `email` value as the `account` argument to other tools.",
|
|
2589
|
+
inputSchema: {},
|
|
2590
|
+
outputSchema: listAccountsOutputSchema
|
|
2591
|
+
},
|
|
2592
|
+
async () => {
|
|
2593
|
+
const rows = store.listAccounts().map((a) => ({
|
|
2594
|
+
email: a.email,
|
|
2595
|
+
provider: a.provider,
|
|
2596
|
+
displayName: a.displayName,
|
|
2597
|
+
addedAt: a.addedAt,
|
|
2598
|
+
hasSignature: !!a.signature,
|
|
2599
|
+
hasStyle: !!(a.style && (a.style.fontFamily || a.style.fontSize || a.style.fontColor))
|
|
2600
|
+
}));
|
|
2601
|
+
const data = { accounts: rows };
|
|
2602
|
+
return ok(data, data);
|
|
2603
|
+
}
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
const addAccountOutputSchema = z2.discriminatedUnion("status", [
|
|
2607
|
+
z2.object({
|
|
2608
|
+
status: z2.literal("pending"),
|
|
2609
|
+
handle: z2.string(),
|
|
2610
|
+
verification: z2.object({
|
|
2611
|
+
userCode: z2.string(),
|
|
2612
|
+
verificationUri: z2.string(),
|
|
2613
|
+
expiresAt: z2.string(),
|
|
2614
|
+
message: z2.string()
|
|
894
2615
|
})
|
|
895
2616
|
}),
|
|
896
|
-
|
|
897
|
-
status:
|
|
898
|
-
account:
|
|
899
|
-
email: z.string(),
|
|
900
|
-
provider: z.enum(["outlook", "imap", "gmail"]),
|
|
901
|
-
displayName: z.string().optional(),
|
|
902
|
-
tokens: z.record(z.unknown()),
|
|
903
|
-
addedAt: z.string(),
|
|
904
|
-
signature: z.string().optional(),
|
|
905
|
-
style: styleOutputSchema.optional()
|
|
906
|
-
})
|
|
2617
|
+
z2.object({
|
|
2618
|
+
status: z2.literal("ready"),
|
|
2619
|
+
account: accountFullOutputSchema
|
|
907
2620
|
})
|
|
908
2621
|
]);
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
2622
|
+
if (shouldRegister("add_account", tools)) {
|
|
2623
|
+
server.registerTool(
|
|
2624
|
+
"add_account",
|
|
2625
|
+
{
|
|
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.",
|
|
2627
|
+
inputSchema: {
|
|
2628
|
+
provider: providerIdEnum.describe("Email backend. 'outlook' (Microsoft Graph) and 'imap' are fully implemented."),
|
|
2629
|
+
email: z2.string().email().optional().describe(
|
|
2630
|
+
"Optional hint \u2014 the provider will verify it against the auth result."
|
|
2631
|
+
),
|
|
2632
|
+
config: z2.record(z2.unknown()).optional().describe(
|
|
2633
|
+
"Provider-specific config (e.g. IMAP host/port). Unused for Outlook."
|
|
2634
|
+
)
|
|
2635
|
+
},
|
|
2636
|
+
outputSchema: addAccountOutputSchema
|
|
917
2637
|
},
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
2638
|
+
async (args) => {
|
|
2639
|
+
const provider = registry.get(args.provider);
|
|
2640
|
+
try {
|
|
2641
|
+
const res = await provider.addAccount({
|
|
2642
|
+
email: args.email,
|
|
2643
|
+
config: args.config
|
|
2644
|
+
});
|
|
2645
|
+
return ok(res, res);
|
|
2646
|
+
} catch (err) {
|
|
2647
|
+
return fail(errMsg(err));
|
|
2648
|
+
}
|
|
928
2649
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
const completeAddAccountOutputSchema =
|
|
932
|
-
status:
|
|
933
|
-
account:
|
|
934
|
-
|
|
935
|
-
provider: z.enum(["outlook", "imap", "gmail"]),
|
|
936
|
-
displayName: z.string().optional(),
|
|
937
|
-
tokens: z.record(z.unknown()),
|
|
938
|
-
addedAt: z.string(),
|
|
939
|
-
signature: z.string().optional(),
|
|
940
|
-
style: styleOutputSchema.optional()
|
|
941
|
-
}).optional(),
|
|
942
|
-
error: z.string().optional()
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
const completeAddAccountOutputSchema = z2.object({
|
|
2653
|
+
status: z2.enum(["pending", "ready", "expired", "error"]),
|
|
2654
|
+
account: accountFullOutputSchema.optional(),
|
|
2655
|
+
error: z2.string().optional()
|
|
943
2656
|
});
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
2657
|
+
if (shouldRegister("complete_add_account", tools)) {
|
|
2658
|
+
server.registerTool(
|
|
2659
|
+
"complete_add_account",
|
|
2660
|
+
{
|
|
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.",
|
|
2662
|
+
inputSchema: {
|
|
2663
|
+
provider: providerIdEnum,
|
|
2664
|
+
handle: z2.string().min(1)
|
|
2665
|
+
},
|
|
2666
|
+
outputSchema: completeAddAccountOutputSchema
|
|
951
2667
|
},
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
2668
|
+
async (args) => {
|
|
2669
|
+
const provider = registry.get(args.provider);
|
|
2670
|
+
if (!provider.completeAddAccount) {
|
|
2671
|
+
return fail(
|
|
2672
|
+
`provider ${args.provider} has no async add-account flow`
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
try {
|
|
2676
|
+
const res = await provider.completeAddAccount(args.handle);
|
|
2677
|
+
return ok(res, res);
|
|
2678
|
+
} catch (err) {
|
|
2679
|
+
return fail(errMsg(err));
|
|
2680
|
+
}
|
|
964
2681
|
}
|
|
965
|
-
|
|
966
|
-
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
967
2684
|
const accountSettingsOutputSchema = {
|
|
968
|
-
signature:
|
|
2685
|
+
signature: z2.string().nullable(),
|
|
969
2686
|
style: styleOutputSchema.nullable()
|
|
970
2687
|
};
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
2688
|
+
if (shouldRegister("get_account_settings", tools)) {
|
|
2689
|
+
server.registerTool(
|
|
2690
|
+
"get_account_settings",
|
|
2691
|
+
{
|
|
2692
|
+
description: "Get signature (HTML) and style preferences for an account.",
|
|
2693
|
+
inputSchema: { account: z2.string().email() },
|
|
2694
|
+
outputSchema: accountSettingsOutputSchema
|
|
2695
|
+
},
|
|
2696
|
+
async (args) => {
|
|
2697
|
+
try {
|
|
2698
|
+
const acct = store.getAccount(args.account);
|
|
2699
|
+
if (!acct)
|
|
2700
|
+
return fail(`no account registered for "${args.account}"`);
|
|
2701
|
+
const data = {
|
|
2702
|
+
signature: acct.signature ?? null,
|
|
2703
|
+
style: acct.style ?? null
|
|
2704
|
+
};
|
|
2705
|
+
return ok(data, data);
|
|
2706
|
+
} catch (err) {
|
|
2707
|
+
return fail(errMsg(err));
|
|
2708
|
+
}
|
|
986
2709
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
if (shouldRegister("set_account_settings", tools)) {
|
|
2713
|
+
server.registerTool(
|
|
2714
|
+
"set_account_settings",
|
|
2715
|
+
{
|
|
2716
|
+
description: "Set signature (HTML snippet) and/or style preferences for an account. Disabled in --read-only mode.",
|
|
2717
|
+
inputSchema: {
|
|
2718
|
+
account: z2.string().email(),
|
|
2719
|
+
signature: z2.string().optional().describe(
|
|
2720
|
+
"HTML snippet \u2014 may contain formatting, images, links. Pass null to clear."
|
|
2721
|
+
),
|
|
2722
|
+
style: z2.object({
|
|
2723
|
+
fontFamily: z2.string().optional(),
|
|
2724
|
+
fontSize: z2.string().optional(),
|
|
2725
|
+
fontColor: z2.string().optional()
|
|
2726
|
+
}).optional().describe(
|
|
2727
|
+
"Font preferences applied to outgoing HTML emails. Pass null to clear."
|
|
2728
|
+
)
|
|
2729
|
+
},
|
|
2730
|
+
outputSchema: accountSettingsOutputSchema
|
|
1001
2731
|
},
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
2732
|
+
async (args) => {
|
|
2733
|
+
try {
|
|
2734
|
+
const acct = store.getAccount(args.account);
|
|
2735
|
+
if (!acct)
|
|
2736
|
+
return fail(`no account registered for "${args.account}"`);
|
|
2737
|
+
const updated = await store.upsertAccount({
|
|
2738
|
+
...acct,
|
|
2739
|
+
signature: args.signature ?? acct.signature,
|
|
2740
|
+
style: args.style ?? acct.style
|
|
2741
|
+
});
|
|
2742
|
+
const data = {
|
|
2743
|
+
signature: updated.signature ?? null,
|
|
2744
|
+
style: updated.style ?? null
|
|
2745
|
+
};
|
|
2746
|
+
return ok(data, data);
|
|
2747
|
+
} catch (err) {
|
|
2748
|
+
return fail(errMsg(err));
|
|
2749
|
+
}
|
|
1018
2750
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
1021
2753
|
const removeAccountOutputSchema = {
|
|
1022
|
-
removed:
|
|
1023
|
-
email:
|
|
2754
|
+
removed: z2.boolean(),
|
|
2755
|
+
email: z2.string()
|
|
1024
2756
|
};
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
2757
|
+
if (shouldRegister("remove_account", tools)) {
|
|
2758
|
+
server.registerTool(
|
|
2759
|
+
"remove_account",
|
|
2760
|
+
{
|
|
2761
|
+
description: "Forget an account and delete its stored tokens. Disabled in --read-only mode.",
|
|
2762
|
+
inputSchema: { email: z2.string().email() },
|
|
2763
|
+
outputSchema: removeAccountOutputSchema
|
|
2764
|
+
},
|
|
2765
|
+
async (args) => {
|
|
2766
|
+
const removed = await store.removeAccount(args.email);
|
|
2767
|
+
const data = { removed, email: args.email };
|
|
2768
|
+
return ok(data, data);
|
|
2769
|
+
}
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
// src/tools/browse.ts
|
|
2775
|
+
import { z as z3 } from "zod";
|
|
2776
|
+
|
|
2777
|
+
// src/html-to-markdown.ts
|
|
2778
|
+
import TurndownService from "turndown";
|
|
2779
|
+
var turndown = new TurndownService();
|
|
2780
|
+
function htmlToMarkdown(html) {
|
|
2781
|
+
return turndown.turndown(html);
|
|
2782
|
+
}
|
|
2783
|
+
function selectBody(msg, format) {
|
|
2784
|
+
switch (format) {
|
|
2785
|
+
case "markdown": {
|
|
2786
|
+
if (msg.bodyHtml) return htmlToMarkdown(msg.bodyHtml);
|
|
2787
|
+
if (msg.bodyText) return msg.bodyText;
|
|
2788
|
+
return "";
|
|
1037
2789
|
}
|
|
1038
|
-
|
|
2790
|
+
case "html": {
|
|
2791
|
+
if (msg.bodyHtml) return msg.bodyHtml;
|
|
2792
|
+
if (msg.bodyText) return msg.bodyText;
|
|
2793
|
+
return "";
|
|
2794
|
+
}
|
|
2795
|
+
case "text": {
|
|
2796
|
+
if (msg.bodyText) return msg.bodyText;
|
|
2797
|
+
if (msg.bodyHtml) return msg.bodyHtml.replace(/<[^>]*>/g, "");
|
|
2798
|
+
return "";
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// src/tools/browse.ts
|
|
2804
|
+
function registerBrowseTools(server, ctx) {
|
|
2805
|
+
const { registry, tools } = ctx;
|
|
1039
2806
|
const emailListOutputSchema = {
|
|
1040
|
-
account:
|
|
1041
|
-
count:
|
|
1042
|
-
items:
|
|
2807
|
+
account: z3.string(),
|
|
2808
|
+
count: z3.number(),
|
|
2809
|
+
items: z3.array(emailSummaryOutputSchema),
|
|
2810
|
+
skip: z3.number(),
|
|
2811
|
+
hasMore: z3.boolean()
|
|
1043
2812
|
};
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
2813
|
+
const searchEmailsOutputSchema = {
|
|
2814
|
+
account: z3.string(),
|
|
2815
|
+
count: z3.number(),
|
|
2816
|
+
items: z3.array(emailSummaryOutputSchema)
|
|
2817
|
+
};
|
|
2818
|
+
if (shouldRegister("list_emails", tools)) {
|
|
2819
|
+
server.registerTool(
|
|
2820
|
+
"list_emails",
|
|
2821
|
+
{
|
|
2822
|
+
description: "List recent emails in a folder of the given account. Pass the user's email address as `account`; the server routes to the correct backend automatically.",
|
|
2823
|
+
inputSchema: {
|
|
2824
|
+
account: z3.string().email(),
|
|
2825
|
+
folder: z3.string().default("inbox").optional(),
|
|
2826
|
+
limit: z3.number().int().positive().max(100).optional(),
|
|
2827
|
+
unreadOnly: z3.boolean().optional(),
|
|
2828
|
+
skip: z3.number().int().min(0).optional()
|
|
2829
|
+
},
|
|
2830
|
+
outputSchema: emailListOutputSchema
|
|
1053
2831
|
},
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
2832
|
+
async (args) => {
|
|
2833
|
+
try {
|
|
2834
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
2835
|
+
const { items, hasMore } = await provider.listEmails(account, {
|
|
2836
|
+
folder: args.folder,
|
|
2837
|
+
limit: args.limit,
|
|
2838
|
+
unreadOnly: args.unreadOnly,
|
|
2839
|
+
skip: args.skip
|
|
2840
|
+
});
|
|
2841
|
+
const data = {
|
|
2842
|
+
account: account.email,
|
|
2843
|
+
count: items.length,
|
|
2844
|
+
items,
|
|
2845
|
+
skip: args.skip ?? 0,
|
|
2846
|
+
hasMore
|
|
2847
|
+
};
|
|
2848
|
+
return ok(data, data);
|
|
2849
|
+
} catch (err) {
|
|
2850
|
+
return fail(errMsg(err));
|
|
2851
|
+
}
|
|
1068
2852
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
2853
|
+
);
|
|
2854
|
+
}
|
|
2855
|
+
if (shouldRegister("search_emails", tools)) {
|
|
2856
|
+
server.registerTool(
|
|
2857
|
+
"search_emails",
|
|
2858
|
+
{
|
|
2859
|
+
description: "Search emails by free-text query (KQL on Outlook). Returns lightweight summaries.",
|
|
2860
|
+
inputSchema: {
|
|
2861
|
+
account: z3.string().email(),
|
|
2862
|
+
query: z3.string().min(1),
|
|
2863
|
+
limit: z3.number().int().positive().max(100).optional()
|
|
2864
|
+
},
|
|
2865
|
+
outputSchema: searchEmailsOutputSchema
|
|
2866
|
+
},
|
|
2867
|
+
async (args) => {
|
|
2868
|
+
try {
|
|
2869
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
2870
|
+
const items = await provider.searchEmails(account, args.query, {
|
|
2871
|
+
limit: args.limit
|
|
2872
|
+
});
|
|
2873
|
+
const data = {
|
|
2874
|
+
account: account.email,
|
|
2875
|
+
count: items.length,
|
|
2876
|
+
items
|
|
2877
|
+
};
|
|
2878
|
+
return ok(data, data);
|
|
2879
|
+
} catch (err) {
|
|
2880
|
+
return fail(errMsg(err));
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
const readEmailOutputSchema = {
|
|
2886
|
+
id: z3.string(),
|
|
2887
|
+
subject: z3.string(),
|
|
2888
|
+
from: emailAddrOutputSchema.optional(),
|
|
2889
|
+
to: z3.array(emailAddrOutputSchema).optional(),
|
|
2890
|
+
cc: z3.array(emailAddrOutputSchema).optional(),
|
|
2891
|
+
bcc: z3.array(emailAddrOutputSchema).optional(),
|
|
2892
|
+
receivedAt: z3.string().optional(),
|
|
2893
|
+
preview: z3.string().optional(),
|
|
2894
|
+
isRead: z3.boolean().optional(),
|
|
2895
|
+
hasAttachments: z3.boolean().optional(),
|
|
2896
|
+
folder: z3.string().optional(),
|
|
2897
|
+
attachments: z3.array(attachmentMetaOutputSchema).optional(),
|
|
2898
|
+
body: z3.string(),
|
|
2899
|
+
bodyFormat: z3.enum(["markdown", "html", "text"])
|
|
2900
|
+
};
|
|
2901
|
+
if (shouldRegister("read_email", tools)) {
|
|
2902
|
+
server.registerTool(
|
|
2903
|
+
"read_email",
|
|
2904
|
+
{
|
|
2905
|
+
description: "Fetch a single email with full body and recipients by id. Body is returned as `body` with `bodyFormat` indicating the format. Default format is 'markdown' \u2014 HTML is automatically converted to save context tokens.",
|
|
2906
|
+
inputSchema: {
|
|
2907
|
+
account: z3.string().email(),
|
|
2908
|
+
id: z3.string().min(1),
|
|
2909
|
+
format: z3.enum(["markdown", "html", "text"]).default("markdown").optional().describe(
|
|
2910
|
+
"Output body format. 'markdown' converts HTML to Markdown (default), 'html' returns the raw HTML, 'text' returns plain text."
|
|
2911
|
+
)
|
|
2912
|
+
},
|
|
2913
|
+
outputSchema: readEmailOutputSchema
|
|
2914
|
+
},
|
|
2915
|
+
async (args) => {
|
|
2916
|
+
try {
|
|
2917
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
2918
|
+
const msg = await provider.readEmail(account, args.id);
|
|
2919
|
+
const format = args.format ?? "markdown";
|
|
2920
|
+
const body = selectBody(msg, format);
|
|
2921
|
+
const data = {
|
|
2922
|
+
id: msg.id,
|
|
2923
|
+
subject: msg.subject,
|
|
2924
|
+
from: msg.from,
|
|
2925
|
+
to: msg.to,
|
|
2926
|
+
cc: msg.cc,
|
|
2927
|
+
bcc: msg.bcc,
|
|
2928
|
+
receivedAt: msg.receivedAt,
|
|
2929
|
+
preview: msg.preview,
|
|
2930
|
+
isRead: msg.isRead,
|
|
2931
|
+
hasAttachments: msg.hasAttachments,
|
|
2932
|
+
folder: msg.folder,
|
|
2933
|
+
attachments: msg.attachments,
|
|
2934
|
+
body,
|
|
2935
|
+
bodyFormat: format
|
|
2936
|
+
};
|
|
2937
|
+
return ok(data, data);
|
|
2938
|
+
} catch (err) {
|
|
2939
|
+
return fail(errMsg(err));
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
);
|
|
2943
|
+
}
|
|
2944
|
+
const readAttachmentOutputSchema = {
|
|
2945
|
+
name: z3.string(),
|
|
2946
|
+
contentType: z3.string().optional(),
|
|
2947
|
+
path: z3.string()
|
|
2948
|
+
};
|
|
2949
|
+
if (shouldRegister("read_attachment", tools)) {
|
|
2950
|
+
server.registerTool(
|
|
2951
|
+
"read_attachment",
|
|
2952
|
+
{
|
|
2953
|
+
description: "Download an email attachment to a temporary file and return its path. Use messageId and attachmentId from a prior read_email call.",
|
|
2954
|
+
inputSchema: {
|
|
2955
|
+
account: z3.string().email(),
|
|
2956
|
+
messageId: z3.string().min(1),
|
|
2957
|
+
attachmentId: z3.string().min(1)
|
|
2958
|
+
},
|
|
2959
|
+
outputSchema: readAttachmentOutputSchema
|
|
2960
|
+
},
|
|
2961
|
+
async (args) => {
|
|
2962
|
+
try {
|
|
2963
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
2964
|
+
const res = await provider.readAttachment(
|
|
2965
|
+
account,
|
|
2966
|
+
args.messageId,
|
|
2967
|
+
args.attachmentId
|
|
2968
|
+
);
|
|
2969
|
+
return ok(res, res);
|
|
2970
|
+
} catch (err) {
|
|
2971
|
+
return fail(errMsg(err));
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
// src/tools/folders.ts
|
|
2979
|
+
import { z as z4 } from "zod";
|
|
2980
|
+
function registerFolderTools(server, ctx) {
|
|
2981
|
+
const { registry, tools } = ctx;
|
|
2982
|
+
const listFoldersOutputSchema = {
|
|
2983
|
+
account: z4.string(),
|
|
2984
|
+
count: z4.number(),
|
|
2985
|
+
items: z4.array(folderInfoOutputSchema)
|
|
2986
|
+
};
|
|
2987
|
+
if (shouldRegister("list_folders", tools)) {
|
|
2988
|
+
server.registerTool(
|
|
2989
|
+
"list_folders",
|
|
2990
|
+
{
|
|
2991
|
+
description: "List available mail folders. Returns top-level folders by default, or child folders of the given parent when `parentFolderId` is provided.",
|
|
2992
|
+
inputSchema: {
|
|
2993
|
+
account: z4.string().email(),
|
|
2994
|
+
parentFolderId: z4.string().optional().describe(
|
|
2995
|
+
"When provided, lists child folders of this folder. When omitted, lists top-level folders (children of the root)."
|
|
2996
|
+
)
|
|
2997
|
+
},
|
|
2998
|
+
outputSchema: listFoldersOutputSchema
|
|
2999
|
+
},
|
|
3000
|
+
async (args) => {
|
|
3001
|
+
try {
|
|
3002
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3003
|
+
const items = await provider.listFolders(account, {
|
|
3004
|
+
parentFolderId: args.parentFolderId
|
|
3005
|
+
});
|
|
3006
|
+
const data = {
|
|
3007
|
+
account: account.email,
|
|
3008
|
+
count: items.length,
|
|
3009
|
+
items
|
|
3010
|
+
};
|
|
3011
|
+
return ok(data, data);
|
|
3012
|
+
} catch (err) {
|
|
3013
|
+
return fail(errMsg(err));
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
);
|
|
3017
|
+
}
|
|
3018
|
+
const createFolderOutputSchema = {
|
|
3019
|
+
created: z4.literal(true),
|
|
3020
|
+
folder: folderInfoOutputSchema
|
|
3021
|
+
};
|
|
3022
|
+
if (shouldRegister("create_folder", tools)) {
|
|
3023
|
+
server.registerTool(
|
|
3024
|
+
"create_folder",
|
|
3025
|
+
{
|
|
3026
|
+
description: "Create a new mail folder. Creates under the root folder by default, or under the specified parent when `parentFolderId` is provided. Disabled in --read-only mode.",
|
|
3027
|
+
inputSchema: {
|
|
3028
|
+
account: z4.string().email(),
|
|
3029
|
+
displayName: z4.string().min(1).describe("Name of the new folder"),
|
|
3030
|
+
parentFolderId: z4.string().optional().describe(
|
|
3031
|
+
"When provided, creates the folder as a child of this folder. When omitted, creates under the root folder."
|
|
3032
|
+
)
|
|
3033
|
+
},
|
|
3034
|
+
outputSchema: createFolderOutputSchema
|
|
1079
3035
|
},
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
3036
|
+
async (args) => {
|
|
3037
|
+
try {
|
|
3038
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3039
|
+
const folder = await provider.createFolder(account, {
|
|
3040
|
+
displayName: args.displayName,
|
|
3041
|
+
parentFolderId: args.parentFolderId
|
|
3042
|
+
});
|
|
3043
|
+
const data = { created: true, folder };
|
|
3044
|
+
return ok(data, data);
|
|
3045
|
+
} catch (err) {
|
|
3046
|
+
return fail(errMsg(err));
|
|
3047
|
+
}
|
|
1092
3048
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
from: emailAddrOutputSchema.optional(),
|
|
1099
|
-
to: z.array(emailAddrOutputSchema).optional(),
|
|
1100
|
-
cc: z.array(emailAddrOutputSchema).optional(),
|
|
1101
|
-
bcc: z.array(emailAddrOutputSchema).optional(),
|
|
1102
|
-
receivedAt: z.string().optional(),
|
|
1103
|
-
preview: z.string().optional(),
|
|
1104
|
-
isRead: z.boolean().optional(),
|
|
1105
|
-
hasAttachments: z.boolean().optional(),
|
|
1106
|
-
folder: z.string().optional(),
|
|
1107
|
-
attachments: z.array(attachmentMetaOutputSchema).optional(),
|
|
1108
|
-
body: z.string(),
|
|
1109
|
-
bodyFormat: z.enum(["markdown", "html", "text"])
|
|
3049
|
+
);
|
|
3050
|
+
}
|
|
3051
|
+
const deleteFolderOutputSchema = {
|
|
3052
|
+
deleted: z4.literal(true),
|
|
3053
|
+
id: z4.string()
|
|
1110
3054
|
};
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
3055
|
+
if (shouldRegister("delete_folder", tools)) {
|
|
3056
|
+
server.registerTool(
|
|
3057
|
+
"delete_folder",
|
|
3058
|
+
{
|
|
3059
|
+
description: "Delete a mail folder by ID. Disabled in --read-only mode.",
|
|
3060
|
+
inputSchema: {
|
|
3061
|
+
account: z4.string().email(),
|
|
3062
|
+
folderId: z4.string().min(1).describe("ID of the folder to delete")
|
|
3063
|
+
},
|
|
3064
|
+
outputSchema: deleteFolderOutputSchema
|
|
1121
3065
|
},
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
id: msg.id,
|
|
1132
|
-
subject: msg.subject,
|
|
1133
|
-
from: msg.from,
|
|
1134
|
-
to: msg.to,
|
|
1135
|
-
cc: msg.cc,
|
|
1136
|
-
bcc: msg.bcc,
|
|
1137
|
-
receivedAt: msg.receivedAt,
|
|
1138
|
-
preview: msg.preview,
|
|
1139
|
-
isRead: msg.isRead,
|
|
1140
|
-
hasAttachments: msg.hasAttachments,
|
|
1141
|
-
folder: msg.folder,
|
|
1142
|
-
attachments: msg.attachments,
|
|
1143
|
-
body,
|
|
1144
|
-
bodyFormat: format
|
|
1145
|
-
};
|
|
1146
|
-
return ok(data, data);
|
|
1147
|
-
} catch (err) {
|
|
1148
|
-
return fail(errMsg(err));
|
|
3066
|
+
async (args) => {
|
|
3067
|
+
try {
|
|
3068
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3069
|
+
await provider.deleteFolder(account, args.folderId);
|
|
3070
|
+
const data = { deleted: true, id: args.folderId };
|
|
3071
|
+
return ok(data, data);
|
|
3072
|
+
} catch (err) {
|
|
3073
|
+
return fail(errMsg(err));
|
|
3074
|
+
}
|
|
1149
3075
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
path: z.string()
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
const renameFolderOutputSchema = {
|
|
3079
|
+
renamed: z4.literal(true),
|
|
3080
|
+
folder: folderInfoOutputSchema
|
|
1156
3081
|
};
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
3082
|
+
if (shouldRegister("rename_folder", tools)) {
|
|
3083
|
+
server.registerTool(
|
|
3084
|
+
"rename_folder",
|
|
3085
|
+
{
|
|
3086
|
+
description: "Rename an existing mail folder. Disabled in --read-only mode.",
|
|
3087
|
+
inputSchema: {
|
|
3088
|
+
account: z4.string().email(),
|
|
3089
|
+
folderId: z4.string().min(1).describe("ID of the folder to rename"),
|
|
3090
|
+
newName: z4.string().min(1).describe("New display name for the folder")
|
|
3091
|
+
},
|
|
3092
|
+
outputSchema: renameFolderOutputSchema
|
|
1165
3093
|
},
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
3094
|
+
async (args) => {
|
|
3095
|
+
try {
|
|
3096
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3097
|
+
const folder = await provider.renameFolder(
|
|
3098
|
+
account,
|
|
3099
|
+
args.folderId,
|
|
3100
|
+
args.newName
|
|
3101
|
+
);
|
|
3102
|
+
const data = { renamed: true, folder };
|
|
3103
|
+
return ok(data, data);
|
|
3104
|
+
} catch (err) {
|
|
3105
|
+
return fail(errMsg(err));
|
|
3106
|
+
}
|
|
1175
3107
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
// src/tools/organize.ts
|
|
3113
|
+
import { z as z5 } from "zod";
|
|
3114
|
+
function registerOrganizeTools(server, ctx) {
|
|
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
|
+
}
|
|
3129
|
+
const archiveMoveSchema = {
|
|
3130
|
+
account: z5.string().email(),
|
|
3131
|
+
id: z5.string().min(1).describe("Message ID to move")
|
|
3132
|
+
};
|
|
3133
|
+
const archiveOutputSchema = {
|
|
3134
|
+
archived: z5.literal(true),
|
|
3135
|
+
id: z5.string()
|
|
3136
|
+
};
|
|
3137
|
+
if (shouldRegister("archive_email", tools)) {
|
|
1187
3138
|
server.registerTool(
|
|
1188
3139
|
"archive_email",
|
|
1189
3140
|
{
|
|
1190
3141
|
description: "Move a message to the Archive folder. Disabled in --read-only mode.",
|
|
1191
|
-
inputSchema:
|
|
3142
|
+
inputSchema: archiveMoveSchema,
|
|
1192
3143
|
outputSchema: archiveOutputSchema
|
|
1193
3144
|
},
|
|
1194
3145
|
async (args) => {
|
|
1195
|
-
if (readOnly) return fail("server is in --read-only mode; archive_email is disabled");
|
|
1196
3146
|
try {
|
|
1197
|
-
|
|
1198
|
-
await provider.moveEmail(account, args.id, "archive");
|
|
1199
|
-
const data = { archived: true, id: args.id };
|
|
1200
|
-
return ok(data, data);
|
|
3147
|
+
return await moveToWellKnown(args, "archive", "archived");
|
|
1201
3148
|
} catch (err) {
|
|
1202
3149
|
return fail(errMsg(err));
|
|
1203
3150
|
}
|
|
1204
3151
|
}
|
|
1205
3152
|
);
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
3153
|
+
}
|
|
3154
|
+
const trashOutputSchema = {
|
|
3155
|
+
trashed: z5.literal(true),
|
|
3156
|
+
id: z5.string()
|
|
3157
|
+
};
|
|
3158
|
+
if (shouldRegister("trash_email", tools)) {
|
|
1210
3159
|
server.registerTool(
|
|
1211
3160
|
"trash_email",
|
|
1212
3161
|
{
|
|
1213
3162
|
description: "Move a message to the Deleted Items (trash) folder. Disabled in --read-only mode.",
|
|
1214
|
-
inputSchema:
|
|
3163
|
+
inputSchema: archiveMoveSchema,
|
|
1215
3164
|
outputSchema: trashOutputSchema
|
|
1216
3165
|
},
|
|
1217
3166
|
async (args) => {
|
|
1218
|
-
|
|
3167
|
+
try {
|
|
3168
|
+
return await moveToWellKnown(args, "deleteditems", "trashed");
|
|
3169
|
+
} catch (err) {
|
|
3170
|
+
return fail(errMsg(err));
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
);
|
|
3174
|
+
}
|
|
3175
|
+
const moveEmailOutputSchema = {
|
|
3176
|
+
moved: z5.literal(true),
|
|
3177
|
+
id: z5.string(),
|
|
3178
|
+
destination: z5.string()
|
|
3179
|
+
};
|
|
3180
|
+
if (shouldRegister("move_email", tools)) {
|
|
3181
|
+
server.registerTool(
|
|
3182
|
+
"move_email",
|
|
3183
|
+
{
|
|
3184
|
+
description: "Move a message to any folder by well-known name (e.g. 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or custom folder ID. Disabled in --read-only mode.",
|
|
3185
|
+
inputSchema: {
|
|
3186
|
+
account: z5.string().email(),
|
|
3187
|
+
id: z5.string().min(1).describe("Message ID to move"),
|
|
3188
|
+
destination: z5.string().min(1).describe(
|
|
3189
|
+
"Destination folder \u2014 a well-known folder name ('archive', 'deleteditems', 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or a raw folder ID."
|
|
3190
|
+
)
|
|
3191
|
+
},
|
|
3192
|
+
outputSchema: moveEmailOutputSchema
|
|
3193
|
+
},
|
|
3194
|
+
async (args) => {
|
|
1219
3195
|
try {
|
|
1220
3196
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
1221
|
-
await provider.moveEmail(account, args.id,
|
|
1222
|
-
const data = {
|
|
3197
|
+
await provider.moveEmail(account, args.id, args.destination);
|
|
3198
|
+
const data = {
|
|
3199
|
+
moved: true,
|
|
3200
|
+
id: args.id,
|
|
3201
|
+
destination: args.destination
|
|
3202
|
+
};
|
|
1223
3203
|
return ok(data, data);
|
|
1224
3204
|
} catch (err) {
|
|
1225
3205
|
return fail(errMsg(err));
|
|
@@ -1227,59 +3207,77 @@ function registerTools(server, opts) {
|
|
|
1227
3207
|
}
|
|
1228
3208
|
);
|
|
1229
3209
|
}
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
id:
|
|
1233
|
-
destination: z.string()
|
|
3210
|
+
const markReadInputSchema = {
|
|
3211
|
+
account: z5.string().email(),
|
|
3212
|
+
id: z5.string().min(1).describe("Message ID to mark as read")
|
|
1234
3213
|
};
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
3214
|
+
const markReadOutputSchema = {
|
|
3215
|
+
marked: z5.literal(true),
|
|
3216
|
+
id: z5.string(),
|
|
3217
|
+
isRead: z5.boolean()
|
|
3218
|
+
};
|
|
3219
|
+
if (shouldRegister("mark_read", tools)) {
|
|
3220
|
+
server.registerTool(
|
|
3221
|
+
"mark_read",
|
|
3222
|
+
{
|
|
3223
|
+
description: "Mark a message as read. Disabled in --read-only mode.",
|
|
3224
|
+
inputSchema: markReadInputSchema,
|
|
3225
|
+
outputSchema: markReadOutputSchema
|
|
1245
3226
|
},
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
await provider.moveEmail(account, args.id, args.destination);
|
|
1253
|
-
const data = { moved: true, id: args.id, destination: args.destination };
|
|
1254
|
-
return ok(data, data);
|
|
1255
|
-
} catch (err) {
|
|
1256
|
-
return fail(errMsg(err));
|
|
3227
|
+
async (args) => {
|
|
3228
|
+
try {
|
|
3229
|
+
return await markReadState(args, true);
|
|
3230
|
+
} catch (err) {
|
|
3231
|
+
return fail(errMsg(err));
|
|
3232
|
+
}
|
|
1257
3233
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
3234
|
+
);
|
|
3235
|
+
}
|
|
3236
|
+
if (shouldRegister("mark_unread", tools)) {
|
|
3237
|
+
server.registerTool(
|
|
3238
|
+
"mark_unread",
|
|
3239
|
+
{
|
|
3240
|
+
description: "Mark a message as unread. Disabled in --read-only mode.",
|
|
3241
|
+
inputSchema: markReadInputSchema,
|
|
3242
|
+
outputSchema: markReadOutputSchema
|
|
3243
|
+
},
|
|
3244
|
+
async (args) => {
|
|
3245
|
+
try {
|
|
3246
|
+
return await markReadState(args, false);
|
|
3247
|
+
} catch (err) {
|
|
3248
|
+
return fail(errMsg(err));
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
);
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
// src/tools/compose.ts
|
|
3256
|
+
import { z as z6 } from "zod";
|
|
3257
|
+
function registerComposeTools(server, ctx) {
|
|
3258
|
+
const { store, registry, tools } = ctx;
|
|
3259
|
+
const sendEmailSchema = z6.object({
|
|
3260
|
+
account: z6.string().email(),
|
|
3261
|
+
to: z6.array(emailAddrSchema).min(1),
|
|
3262
|
+
cc: z6.array(emailAddrSchema).optional(),
|
|
3263
|
+
bcc: z6.array(emailAddrSchema).optional(),
|
|
3264
|
+
subject: z6.string(),
|
|
3265
|
+
body: z6.string(),
|
|
3266
|
+
isHtml: z6.boolean().optional(),
|
|
3267
|
+
include_signature: z6.boolean().describe(
|
|
1269
3268
|
"Whether to append the account's saved HTML signature to the email. If true, don't include a signature in the body param to avoid double signature. Returns an error if true but no signature is configured for this account."
|
|
1270
3269
|
),
|
|
1271
|
-
inReplyTo:
|
|
3270
|
+
inReplyTo: z6.string().optional().describe(
|
|
1272
3271
|
"Message ID to reply to. When set, sends as a threaded reply which includes the quoted thread history automatically."
|
|
1273
3272
|
),
|
|
1274
|
-
replyAll:
|
|
3273
|
+
replyAll: z6.boolean().default(false).optional().describe(
|
|
1275
3274
|
"When true and `inReplyTo` is set, reply to all recipients instead of just the sender."
|
|
1276
3275
|
),
|
|
1277
|
-
forwardMessageId:
|
|
3276
|
+
forwardMessageId: z6.string().optional().describe(
|
|
1278
3277
|
"Message ID to forward. When set, sends as a forward of the specified message, preserving the original content. Mutually exclusive with `inReplyTo`."
|
|
1279
3278
|
)
|
|
1280
3279
|
});
|
|
1281
3280
|
async function handleSendOrDraft(args, action, resultKey, toolName) {
|
|
1282
|
-
if (readOnly) return fail(`server is in --read-only mode; ${toolName} is disabled`);
|
|
1283
3281
|
try {
|
|
1284
3282
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
1285
3283
|
if (args.include_signature && !account.signature) {
|
|
@@ -1321,10 +3319,10 @@ function registerTools(server, opts) {
|
|
|
1321
3319
|
}
|
|
1322
3320
|
}
|
|
1323
3321
|
const sendEmailOutputSchema = {
|
|
1324
|
-
sent:
|
|
1325
|
-
id:
|
|
3322
|
+
sent: z6.literal(true),
|
|
3323
|
+
id: z6.string()
|
|
1326
3324
|
};
|
|
1327
|
-
if (
|
|
3325
|
+
if (shouldRegister("send_email", tools)) {
|
|
1328
3326
|
server.registerTool(
|
|
1329
3327
|
"send_email",
|
|
1330
3328
|
{
|
|
@@ -1341,142 +3339,328 @@ function registerTools(server, opts) {
|
|
|
1341
3339
|
);
|
|
1342
3340
|
}
|
|
1343
3341
|
const draftEmailOutputSchema = {
|
|
1344
|
-
draft:
|
|
1345
|
-
id:
|
|
1346
|
-
draftHtml:
|
|
3342
|
+
draft: z6.literal(true),
|
|
3343
|
+
id: z6.string(),
|
|
3344
|
+
draftHtml: z6.string().optional()
|
|
1347
3345
|
};
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
args
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
3346
|
+
if (shouldRegister("draft_email", tools)) {
|
|
3347
|
+
server.registerTool(
|
|
3348
|
+
"draft_email",
|
|
3349
|
+
{
|
|
3350
|
+
description: "Create a draft email from the given account without sending it. Works identically to send_email \u2014 appends signature when `include_signature` is true, applies style, and supports replies and forwards \u2014 but saves the message to the Drafts folder instead of sending. Returns the draft message ID and the draft's HTML body content (`draftHtml`). Before sending the draft, inspect `draftHtml` to verify the draft looks correct: no duplicate signature blocks, no broken or missing inline images, no malformed HTML, and no other formatting issues. Disabled in --read-only mode.",
|
|
3351
|
+
inputSchema: sendEmailSchema,
|
|
3352
|
+
outputSchema: draftEmailOutputSchema
|
|
3353
|
+
},
|
|
3354
|
+
async (args) => handleSendOrDraft(
|
|
3355
|
+
args,
|
|
3356
|
+
(p, a, m) => p.saveDraft(a, m),
|
|
3357
|
+
"draft",
|
|
3358
|
+
"draft_email"
|
|
3359
|
+
)
|
|
3360
|
+
);
|
|
3361
|
+
}
|
|
3362
|
+
const editDraftSchema = z6.object({
|
|
3363
|
+
account: z6.string().email(),
|
|
3364
|
+
id: z6.string().min(1).describe("Draft message ID to edit"),
|
|
3365
|
+
to: z6.array(emailAddrSchema).optional(),
|
|
3366
|
+
cc: z6.array(emailAddrSchema).optional(),
|
|
3367
|
+
bcc: z6.array(emailAddrSchema).optional(),
|
|
3368
|
+
subject: z6.string().optional(),
|
|
3369
|
+
body: z6.string().optional(),
|
|
3370
|
+
isHtml: z6.boolean().optional(),
|
|
3371
|
+
include_signature: z6.boolean().optional().describe(
|
|
1372
3372
|
"Whether to re-apply the account's saved HTML signature to the body. If true, don't include a signature in the body param. Only meaningful when `body` is also provided. Returns an error if true but no signature is configured for this account."
|
|
1373
3373
|
)
|
|
1374
3374
|
});
|
|
1375
3375
|
const editDraftOutputSchema = {
|
|
1376
|
-
edited:
|
|
1377
|
-
id:
|
|
1378
|
-
draftHtml:
|
|
3376
|
+
edited: z6.literal(true),
|
|
3377
|
+
id: z6.string(),
|
|
3378
|
+
draftHtml: z6.string().optional()
|
|
1379
3379
|
};
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
3380
|
+
if (shouldRegister("edit_draft", tools)) {
|
|
3381
|
+
server.registerTool(
|
|
3382
|
+
"edit_draft",
|
|
3383
|
+
{
|
|
3384
|
+
description: "Edit an existing draft email by ID. Only the fields you provide are updated \u2014 unmentioned fields stay unchanged. When `body` is provided and `include_signature` is true, the account's signature is re-applied. Returns the draft ID and the draft's updated HTML body content (`draftHtml`). Before sending, inspect `draftHtml` to verify the draft looks correct. Does not support changing `inReplyTo` or `forwardMessageId` \u2014 those are set at creation time via `draft_email`. Disabled in --read-only mode.",
|
|
3385
|
+
inputSchema: editDraftSchema,
|
|
3386
|
+
outputSchema: editDraftOutputSchema
|
|
3387
|
+
},
|
|
3388
|
+
async (args) => {
|
|
3389
|
+
const a = args;
|
|
3390
|
+
try {
|
|
3391
|
+
const { provider, account } = registry.resolveByEmail(a.account);
|
|
3392
|
+
if (a.include_signature && !account.signature) {
|
|
3393
|
+
return fail(
|
|
3394
|
+
"include_signature is true but no signature is configured for this account. Set up a signature first with set_account_settings."
|
|
3395
|
+
);
|
|
3396
|
+
}
|
|
3397
|
+
let bodyPayload;
|
|
3398
|
+
let isHtmlPayload;
|
|
3399
|
+
if (a.body !== void 0) {
|
|
3400
|
+
const composed = composeBody({
|
|
3401
|
+
body: a.body,
|
|
3402
|
+
isHtml: a.isHtml,
|
|
3403
|
+
signature: account.signature,
|
|
3404
|
+
style: account.style,
|
|
3405
|
+
includeSignature: !!a.include_signature
|
|
3406
|
+
});
|
|
3407
|
+
bodyPayload = composed.body;
|
|
3408
|
+
isHtmlPayload = composed.isHtml;
|
|
3409
|
+
}
|
|
3410
|
+
const res = await provider.updateDraft(account, a.id, {
|
|
3411
|
+
to: a.to,
|
|
3412
|
+
cc: a.cc,
|
|
3413
|
+
bcc: a.bcc,
|
|
3414
|
+
subject: a.subject,
|
|
3415
|
+
body: bodyPayload,
|
|
3416
|
+
isHtml: isHtmlPayload
|
|
1406
3417
|
});
|
|
1407
|
-
|
|
1408
|
-
|
|
3418
|
+
const draft = await provider.readEmail(account, res.id);
|
|
3419
|
+
const result = {
|
|
3420
|
+
edited: true,
|
|
3421
|
+
id: res.id,
|
|
3422
|
+
draftHtml: draft.bodyHtml
|
|
3423
|
+
};
|
|
3424
|
+
return ok(result, result);
|
|
3425
|
+
} catch (err) {
|
|
3426
|
+
return fail(errMsg(err));
|
|
1409
3427
|
}
|
|
1410
|
-
const res = await provider.updateDraft(account, a.id, {
|
|
1411
|
-
to: a.to,
|
|
1412
|
-
cc: a.cc,
|
|
1413
|
-
bcc: a.bcc,
|
|
1414
|
-
subject: a.subject,
|
|
1415
|
-
body: bodyPayload,
|
|
1416
|
-
isHtml: isHtmlPayload
|
|
1417
|
-
});
|
|
1418
|
-
const draft = await provider.readEmail(account, res.id);
|
|
1419
|
-
const result = {
|
|
1420
|
-
edited: true,
|
|
1421
|
-
id: res.id,
|
|
1422
|
-
draftHtml: draft.bodyHtml
|
|
1423
|
-
};
|
|
1424
|
-
return ok(result, result);
|
|
1425
|
-
} catch (err) {
|
|
1426
|
-
return fail(errMsg(err));
|
|
1427
3428
|
}
|
|
1428
|
-
|
|
1429
|
-
);
|
|
1430
|
-
}
|
|
1431
|
-
function composeBody(input) {
|
|
1432
|
-
const { body, isHtml = false, signature, style, includeSignature } = input;
|
|
1433
|
-
const hasSignature = includeSignature && !!signature;
|
|
1434
|
-
const hasStyle = !!(style && (style.fontFamily || style.fontSize || style.fontColor));
|
|
1435
|
-
if (!hasSignature && !hasStyle) {
|
|
1436
|
-
return { body, isHtml };
|
|
3429
|
+
);
|
|
1437
3430
|
}
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
3431
|
+
const sendDraftOutputSchema = {
|
|
3432
|
+
sent: z6.literal(true),
|
|
3433
|
+
id: z6.string()
|
|
3434
|
+
};
|
|
3435
|
+
if (shouldRegister("send_draft", tools)) {
|
|
3436
|
+
server.registerTool(
|
|
3437
|
+
"send_draft",
|
|
3438
|
+
{
|
|
3439
|
+
description: "Send an existing draft email by ID. Use this with draft IDs returned by `draft_email` or `edit_draft`. Disabled in --read-only mode.",
|
|
3440
|
+
inputSchema: {
|
|
3441
|
+
account: z6.string().email(),
|
|
3442
|
+
id: z6.string().min(1).describe("Draft message ID to send")
|
|
3443
|
+
},
|
|
3444
|
+
outputSchema: sendDraftOutputSchema
|
|
3445
|
+
},
|
|
3446
|
+
async (args) => {
|
|
3447
|
+
try {
|
|
3448
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3449
|
+
const res = await provider.sendDraft(account, args.id);
|
|
3450
|
+
const data = { sent: true, id: res.id };
|
|
3451
|
+
return ok(data, data);
|
|
3452
|
+
} catch (err) {
|
|
3453
|
+
return fail(errMsg(err));
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
);
|
|
3457
|
+
}
|
|
3458
|
+
const addAttachmentOutputSchema = {
|
|
3459
|
+
attached: z6.literal(true),
|
|
3460
|
+
id: z6.string(),
|
|
3461
|
+
attachment: z6.object({
|
|
3462
|
+
id: z6.string(),
|
|
3463
|
+
name: z6.string(),
|
|
3464
|
+
contentType: z6.string().optional()
|
|
3465
|
+
})
|
|
3466
|
+
};
|
|
3467
|
+
if (shouldRegister("add_attachment_to_draft", tools)) {
|
|
3468
|
+
server.registerTool(
|
|
3469
|
+
"add_attachment_to_draft",
|
|
3470
|
+
{
|
|
3471
|
+
description: "Add a file attachment to an existing draft email by ID. `contentBytes` must be base64-encoded file content. `contentType` is the MIME type (e.g. 'application/pdf'); defaults to 'application/octet-stream' if omitted. Disabled in --read-only mode.",
|
|
3472
|
+
inputSchema: {
|
|
3473
|
+
account: z6.string().email(),
|
|
3474
|
+
id: z6.string().min(1).describe("Draft message ID"),
|
|
3475
|
+
name: z6.string().min(1).describe("Attachment filename (e.g. 'report.pdf')"),
|
|
3476
|
+
contentBytes: z6.string().min(1).describe("Base64-encoded file content"),
|
|
3477
|
+
contentType: z6.string().optional().describe("MIME type (e.g. 'application/pdf')")
|
|
3478
|
+
},
|
|
3479
|
+
outputSchema: addAttachmentOutputSchema
|
|
3480
|
+
},
|
|
3481
|
+
async (args) => {
|
|
3482
|
+
try {
|
|
3483
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3484
|
+
const res = await provider.addAttachmentToDraft(
|
|
3485
|
+
account,
|
|
3486
|
+
args.id,
|
|
3487
|
+
args.name,
|
|
3488
|
+
args.contentBytes,
|
|
3489
|
+
args.contentType
|
|
3490
|
+
);
|
|
3491
|
+
const data = {
|
|
3492
|
+
attached: true,
|
|
3493
|
+
id: res.id,
|
|
3494
|
+
attachment: res.attachment
|
|
3495
|
+
};
|
|
3496
|
+
return ok(data, data);
|
|
3497
|
+
} catch (err) {
|
|
3498
|
+
return fail(errMsg(err));
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
);
|
|
1444
3502
|
}
|
|
1445
|
-
const escaped = escapeHtml(body);
|
|
1446
|
-
let result = `<div style="${styleAttr}">${escaped}</div>`;
|
|
1447
|
-
if (hasSignature) result += `
|
|
1448
|
-
<div class="signature">${signature}</div>`;
|
|
1449
|
-
return { body: result, isHtml: true };
|
|
1450
|
-
}
|
|
1451
|
-
function buildStyleAttr(style) {
|
|
1452
|
-
const parts = [];
|
|
1453
|
-
if (style.fontFamily) parts.push(`font-family: ${style.fontFamily}`);
|
|
1454
|
-
if (style.fontSize) parts.push(`font-size: ${style.fontSize}`);
|
|
1455
|
-
if (style.fontColor) parts.push(`color: ${style.fontColor}`);
|
|
1456
|
-
return parts.join("; ");
|
|
1457
|
-
}
|
|
1458
|
-
function escapeHtml(text) {
|
|
1459
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/\n/g, "<br>");
|
|
1460
3503
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
3504
|
+
|
|
3505
|
+
// src/tools/index.ts
|
|
3506
|
+
function registerTools(server, opts) {
|
|
3507
|
+
const { store, registry, tools } = opts;
|
|
3508
|
+
registerAccountTools(server, { store, registry, tools });
|
|
3509
|
+
registerBrowseTools(server, { registry, tools });
|
|
3510
|
+
registerFolderTools(server, { registry, tools });
|
|
3511
|
+
registerOrganizeTools(server, { registry, tools });
|
|
3512
|
+
registerComposeTools(server, { store, registry, tools });
|
|
1464
3513
|
}
|
|
1465
3514
|
|
|
1466
3515
|
// src/version.ts
|
|
1467
|
-
var VERSION = "0.
|
|
3516
|
+
var VERSION = "0.4.1";
|
|
3517
|
+
|
|
3518
|
+
// src/config.ts
|
|
3519
|
+
import { readFileSync } from "fs";
|
|
3520
|
+
import { z as z7 } from "zod";
|
|
3521
|
+
var httpConfigSchema = z7.object({
|
|
3522
|
+
enabled: z7.boolean().default(false),
|
|
3523
|
+
port: z7.number().int().min(1).max(65535).default(3e3),
|
|
3524
|
+
host: z7.string().default("127.0.0.1")
|
|
3525
|
+
});
|
|
3526
|
+
var toolsConfigSchema = z7.object({
|
|
3527
|
+
disabled: z7.array(z7.string()).optional(),
|
|
3528
|
+
enabled: z7.array(z7.string()).optional()
|
|
3529
|
+
});
|
|
3530
|
+
var outlookProviderSchema = z7.object({
|
|
3531
|
+
clientId: z7.string().optional(),
|
|
3532
|
+
tenantId: z7.string().optional()
|
|
3533
|
+
});
|
|
3534
|
+
var gmailProviderSchema = z7.object({
|
|
3535
|
+
clientId: z7.string().optional(),
|
|
3536
|
+
clientSecret: z7.string().optional()
|
|
3537
|
+
});
|
|
3538
|
+
var providersConfigSchema = z7.object({
|
|
3539
|
+
outlook: outlookProviderSchema.optional(),
|
|
3540
|
+
gmail: gmailProviderSchema.optional()
|
|
3541
|
+
});
|
|
3542
|
+
var rawConfigSchema = z7.object({
|
|
3543
|
+
dataDir: z7.string().optional(),
|
|
3544
|
+
http: httpConfigSchema.optional(),
|
|
3545
|
+
tools: toolsConfigSchema.optional(),
|
|
3546
|
+
providers: providersConfigSchema.optional()
|
|
3547
|
+
});
|
|
3548
|
+
var KNOWN_TOOLS = [
|
|
3549
|
+
"list_accounts",
|
|
3550
|
+
"add_account",
|
|
3551
|
+
"complete_add_account",
|
|
3552
|
+
"get_account_settings",
|
|
3553
|
+
"set_account_settings",
|
|
3554
|
+
"remove_account",
|
|
3555
|
+
"list_emails",
|
|
3556
|
+
"search_emails",
|
|
3557
|
+
"read_email",
|
|
3558
|
+
"read_attachment",
|
|
3559
|
+
"archive_email",
|
|
3560
|
+
"trash_email",
|
|
3561
|
+
"move_email",
|
|
3562
|
+
"mark_read",
|
|
3563
|
+
"mark_unread",
|
|
3564
|
+
"list_folders",
|
|
3565
|
+
"create_folder",
|
|
3566
|
+
"delete_folder",
|
|
3567
|
+
"rename_folder",
|
|
3568
|
+
"send_email",
|
|
3569
|
+
"draft_email",
|
|
3570
|
+
"edit_draft",
|
|
3571
|
+
"send_draft",
|
|
3572
|
+
"add_attachment_to_draft"
|
|
3573
|
+
];
|
|
3574
|
+
var ENV_VAR_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
3575
|
+
function resolveEnvVars(value) {
|
|
3576
|
+
return value.replace(ENV_VAR_RE, (_match, name) => {
|
|
3577
|
+
return process.env[name] ?? "";
|
|
3578
|
+
});
|
|
3579
|
+
}
|
|
3580
|
+
function deepResolve(obj) {
|
|
3581
|
+
if (typeof obj === "string") return resolveEnvVars(obj);
|
|
3582
|
+
if (Array.isArray(obj)) return obj.map(deepResolve);
|
|
3583
|
+
if (obj && typeof obj === "object") {
|
|
3584
|
+
const out = {};
|
|
3585
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
3586
|
+
out[key] = deepResolve(val);
|
|
3587
|
+
}
|
|
3588
|
+
return out;
|
|
3589
|
+
}
|
|
3590
|
+
return obj;
|
|
3591
|
+
}
|
|
3592
|
+
function validateToolNames(toolNames, listName) {
|
|
3593
|
+
if (!toolNames || toolNames.length === 0) return;
|
|
3594
|
+
const known = new Set(KNOWN_TOOLS);
|
|
3595
|
+
for (const name of toolNames) {
|
|
3596
|
+
if (!known.has(name)) {
|
|
3597
|
+
throw new Error(
|
|
3598
|
+
`Unknown tool "${name}" in ${listName}. Known tools: ${KNOWN_TOOLS.join(", ")}`
|
|
3599
|
+
);
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
function loadConfig(configPath, cliOverrides = {}) {
|
|
3604
|
+
let raw = {};
|
|
3605
|
+
if (configPath) {
|
|
3606
|
+
try {
|
|
3607
|
+
raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
3608
|
+
} catch (err) {
|
|
3609
|
+
const detail = err instanceof SyntaxError ? "Invalid JSON" : err instanceof Error ? err.message : String(err);
|
|
3610
|
+
throw new Error(`Failed to read config file "${configPath}": ${detail}`);
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
raw = deepResolve(raw);
|
|
3614
|
+
const parsed = rawConfigSchema.parse(raw);
|
|
3615
|
+
if (parsed.tools) {
|
|
3616
|
+
if (parsed.tools.disabled && parsed.tools.enabled) {
|
|
3617
|
+
throw new Error(
|
|
3618
|
+
"tools.disabled and tools.enabled are mutually exclusive \u2014 use one or the other"
|
|
3619
|
+
);
|
|
3620
|
+
}
|
|
3621
|
+
if (parsed.tools.enabled !== void 0 && parsed.tools.enabled.length === 0) {
|
|
3622
|
+
throw new Error(
|
|
3623
|
+
"tools.enabled is empty \u2014 at least one tool must be listed. To enable all tools, omit the tools section entirely."
|
|
3624
|
+
);
|
|
3625
|
+
}
|
|
3626
|
+
validateToolNames(parsed.tools.disabled, "tools.disabled");
|
|
3627
|
+
validateToolNames(parsed.tools.enabled, "tools.enabled");
|
|
3628
|
+
}
|
|
3629
|
+
const http = {
|
|
3630
|
+
enabled: cliOverrides.http ?? parsed.http?.enabled ?? false,
|
|
3631
|
+
port: cliOverrides.port ?? parsed.http?.port ?? 3e3,
|
|
3632
|
+
host: cliOverrides.host ?? parsed.http?.host ?? "127.0.0.1"
|
|
3633
|
+
};
|
|
3634
|
+
return {
|
|
3635
|
+
dataDir: cliOverrides.dataDir ?? parsed.dataDir ?? process.env.HYPERMAIL_MCP_DATA_DIR,
|
|
3636
|
+
http,
|
|
3637
|
+
tools: parsed.tools ? { disabled: parsed.tools.disabled, enabled: parsed.tools.enabled } : void 0,
|
|
3638
|
+
providers: parsed.providers
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
function resolveTools(config) {
|
|
3642
|
+
if (!config.tools) {
|
|
3643
|
+
return { enabledTools: null, disabledTools: null };
|
|
3644
|
+
}
|
|
3645
|
+
return {
|
|
3646
|
+
enabledTools: config.tools.enabled ? new Set(config.tools.enabled) : null,
|
|
3647
|
+
disabledTools: config.tools.disabled ? new Set(config.tools.disabled) : null
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
1468
3650
|
|
|
1469
3651
|
// src/server.ts
|
|
1470
|
-
async function startServer(opts
|
|
1471
|
-
const
|
|
1472
|
-
const
|
|
3652
|
+
async function startServer(opts) {
|
|
3653
|
+
const { config } = opts;
|
|
3654
|
+
const store = await AccountStore.open({ dataDir: config.dataDir });
|
|
3655
|
+
const registry = buildRegistry({ store, providers: config.providers });
|
|
3656
|
+
const tools = resolveTools(config);
|
|
1473
3657
|
const server = new McpServer(
|
|
1474
3658
|
{ name: "hypermail-mcp", version: VERSION },
|
|
1475
3659
|
{ capabilities: { tools: {}, logging: {} } }
|
|
1476
3660
|
);
|
|
1477
|
-
registerTools(server, { store, registry,
|
|
1478
|
-
if (
|
|
1479
|
-
await startHttp(server,
|
|
3661
|
+
registerTools(server, { store, registry, tools });
|
|
3662
|
+
if (config.http.enabled) {
|
|
3663
|
+
await startHttp(server, config.http.host, config.http.port);
|
|
1480
3664
|
} else {
|
|
1481
3665
|
const transport = new StdioServerTransport();
|
|
1482
3666
|
await server.connect(transport);
|
|
@@ -1495,7 +3679,7 @@ async function startHttp(server, host, port) {
|
|
|
1495
3679
|
let transport = sessionId ? sessions.get(sessionId) : void 0;
|
|
1496
3680
|
if (!transport) {
|
|
1497
3681
|
transport = new StreamableHTTPServerTransport({
|
|
1498
|
-
sessionIdGenerator: () =>
|
|
3682
|
+
sessionIdGenerator: () => randomUUID6(),
|
|
1499
3683
|
onsessioninitialized: (sid) => {
|
|
1500
3684
|
sessions.set(sid, transport);
|
|
1501
3685
|
}
|
|
@@ -1531,7 +3715,6 @@ function parseArgs(argv) {
|
|
|
1531
3715
|
http: false,
|
|
1532
3716
|
port: 3e3,
|
|
1533
3717
|
host: "127.0.0.1",
|
|
1534
|
-
readOnly: false,
|
|
1535
3718
|
help: false
|
|
1536
3719
|
};
|
|
1537
3720
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -1549,8 +3732,8 @@ function parseArgs(argv) {
|
|
|
1549
3732
|
case "--data-dir":
|
|
1550
3733
|
out.dataDir = String(argv[++i] ?? "");
|
|
1551
3734
|
break;
|
|
1552
|
-
case "--
|
|
1553
|
-
out.
|
|
3735
|
+
case "--config":
|
|
3736
|
+
out.config = String(argv[++i] ?? "");
|
|
1554
3737
|
break;
|
|
1555
3738
|
case "-h":
|
|
1556
3739
|
case "--help":
|
|
@@ -1575,14 +3758,23 @@ Options:
|
|
|
1575
3758
|
--host <addr> HTTP bind address (default: 127.0.0.1)
|
|
1576
3759
|
--data-dir <path> Where to store the encrypted accounts file
|
|
1577
3760
|
(default: $HYPERMAIL_MCP_DATA_DIR or ~/.hypermail-mcp)
|
|
1578
|
-
--
|
|
3761
|
+
--config <path> Path to hypermail-config.json
|
|
1579
3762
|
-h, --help Show this help
|
|
1580
3763
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
3764
|
+
Configuration:
|
|
3765
|
+
All server settings (data dir, HTTP, tool filtering, provider credentials)
|
|
3766
|
+
live in hypermail-config.json. Pass it via --config.
|
|
3767
|
+
|
|
3768
|
+
Example hypermail-config.json:
|
|
3769
|
+
{
|
|
3770
|
+
"dataDir": "/path/to/data",
|
|
3771
|
+
"http": { "enabled": false },
|
|
3772
|
+
"tools": { "disabled": ["send_email"] },
|
|
3773
|
+
"providers": {
|
|
3774
|
+
"outlook": { "clientId": "\${MS_CLIENT_ID}", "tenantId": "\${MS_TENANT_ID}" },
|
|
3775
|
+
"gmail": { "clientId": "\${GOOGLE_CLIENT_ID}", "clientSecret": "\${GOOGLE_CLIENT_SECRET}" }
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
1586
3778
|
`;
|
|
1587
3779
|
process.stdout.write(msg);
|
|
1588
3780
|
}
|
|
@@ -1592,15 +3784,13 @@ async function main() {
|
|
|
1592
3784
|
printHelp();
|
|
1593
3785
|
return;
|
|
1594
3786
|
}
|
|
1595
|
-
const
|
|
1596
|
-
await startServer({
|
|
3787
|
+
const config = loadConfig(opts.config, {
|
|
1597
3788
|
http: opts.http,
|
|
1598
3789
|
port: opts.port,
|
|
1599
3790
|
host: opts.host,
|
|
1600
|
-
dataDir: opts.dataDir
|
|
1601
|
-
readOnly: opts.readOnly,
|
|
1602
|
-
draftOnly
|
|
3791
|
+
dataDir: opts.dataDir
|
|
1603
3792
|
});
|
|
3793
|
+
await startServer({ config });
|
|
1604
3794
|
}
|
|
1605
3795
|
main().catch((err) => {
|
|
1606
3796
|
console.error("[hypermail-mcp] fatal:", err);
|