@marlinjai/email-mcp 1.2.8 → 1.4.0
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 +15 -2
- package/dist/index.js +494 -12
- package/dist/index.js.map +4 -4
- package/dist/setup/wizard.js +295 -12
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +2 -2
package/dist/setup/wizard.js
CHANGED
|
@@ -264,7 +264,10 @@ function createFileCachePlugin(cacheFilePath) {
|
|
|
264
264
|
}
|
|
265
265
|
var OutlookAuth = class _OutlookAuth {
|
|
266
266
|
static AUTHORITY = "https://login.microsoftonline.com/consumers";
|
|
267
|
-
|
|
267
|
+
// MailboxSettings.ReadWrite is required for the messageRules API (block
|
|
268
|
+
// rules) — confirmed by Microsoft's own docs to be supported for personal
|
|
269
|
+
// Microsoft accounts (not just work/school), not just Mail.ReadWrite/Send.
|
|
270
|
+
static SCOPES = ["Mail.ReadWrite", "Mail.Send", "MailboxSettings.ReadWrite", "offline_access"];
|
|
268
271
|
pca;
|
|
269
272
|
cryptoProvider;
|
|
270
273
|
constructor(clientId, cacheFilePath) {
|
|
@@ -363,6 +366,7 @@ var FolderType = {
|
|
|
363
366
|
};
|
|
364
367
|
|
|
365
368
|
// src/providers/gmail/adapter.ts
|
|
369
|
+
import { Readable } from "node:stream";
|
|
366
370
|
import { google } from "googleapis";
|
|
367
371
|
import { OAuth2Client as OAuth2Client2 } from "google-auth-library";
|
|
368
372
|
|
|
@@ -567,20 +571,32 @@ var GmailAdapter = class {
|
|
|
567
571
|
async search(query) {
|
|
568
572
|
const gmail = this.ensureConnected();
|
|
569
573
|
const q = buildGmailQuery(query);
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
574
|
+
const limit = query.limit || 20;
|
|
575
|
+
const offset = query.offset || 0;
|
|
576
|
+
const needed = offset + limit;
|
|
577
|
+
const ids = [];
|
|
578
|
+
let pageToken;
|
|
579
|
+
do {
|
|
580
|
+
const params = {
|
|
581
|
+
userId: "me",
|
|
582
|
+
maxResults: Math.min(500, needed - ids.length)
|
|
583
|
+
};
|
|
584
|
+
if (q) params.q = q;
|
|
585
|
+
if (query.folder) params.labelIds = [query.folder];
|
|
586
|
+
if (pageToken) params.pageToken = pageToken;
|
|
587
|
+
const res = await gmail.users.messages.list(params);
|
|
588
|
+
for (const msg of res.data.messages || []) {
|
|
589
|
+
if (msg.id) ids.push(msg.id);
|
|
590
|
+
}
|
|
591
|
+
pageToken = res.data.nextPageToken || void 0;
|
|
592
|
+
} while (pageToken && ids.length < needed);
|
|
593
|
+
const windowIds = ids.slice(offset, offset + limit);
|
|
594
|
+
if (windowIds.length === 0) return [];
|
|
579
595
|
const emails = [];
|
|
580
|
-
for (const
|
|
596
|
+
for (const id of windowIds) {
|
|
581
597
|
const full = await gmail.users.messages.get({
|
|
582
598
|
userId: "me",
|
|
583
|
-
id
|
|
599
|
+
id,
|
|
584
600
|
format: "full"
|
|
585
601
|
});
|
|
586
602
|
emails.push(mapGmailMessage(full.data, this.accountId));
|
|
@@ -828,6 +844,40 @@ var GmailAdapter = class {
|
|
|
828
844
|
}
|
|
829
845
|
return result;
|
|
830
846
|
}
|
|
847
|
+
async getRawMessage(emailId, _sourceFolder) {
|
|
848
|
+
const gmail = this.ensureConnected();
|
|
849
|
+
const res = await gmail.users.messages.get({
|
|
850
|
+
userId: "me",
|
|
851
|
+
id: emailId,
|
|
852
|
+
format: "raw"
|
|
853
|
+
});
|
|
854
|
+
return Buffer.from(res.data.raw || "", "base64url");
|
|
855
|
+
}
|
|
856
|
+
async appendRawMessage(raw, targetFolder, flags) {
|
|
857
|
+
const gmail = this.ensureConnected();
|
|
858
|
+
const labelIds = [];
|
|
859
|
+
labelIds.push(targetFolder ? await this.resolveLabelId(targetFolder) : "INBOX");
|
|
860
|
+
if (flags?.read === false) labelIds.push("UNREAD");
|
|
861
|
+
if (flags?.starred === true) labelIds.push("STARRED");
|
|
862
|
+
const res = await gmail.users.messages.insert({
|
|
863
|
+
userId: "me",
|
|
864
|
+
internalDateSource: "dateHeader",
|
|
865
|
+
requestBody: { labelIds },
|
|
866
|
+
media: { mimeType: "message/rfc822", body: Readable.from(raw) }
|
|
867
|
+
});
|
|
868
|
+
return { id: res.data.id || "" };
|
|
869
|
+
}
|
|
870
|
+
/** Resolve a Gmail label name or id to a label id (accepts either). */
|
|
871
|
+
async resolveLabelId(nameOrId) {
|
|
872
|
+
const gmail = this.ensureConnected();
|
|
873
|
+
const res = await gmail.users.labels.list({ userId: "me" });
|
|
874
|
+
const labels = res.data.labels || [];
|
|
875
|
+
const byId = labels.find((l) => l.id === nameOrId);
|
|
876
|
+
if (byId?.id) return byId.id;
|
|
877
|
+
const byName = labels.find((l) => (l.name || "").toLowerCase() === nameOrId.toLowerCase());
|
|
878
|
+
if (byName?.id) return byName.id;
|
|
879
|
+
return nameOrId;
|
|
880
|
+
}
|
|
831
881
|
async addLabels(emailId, labels) {
|
|
832
882
|
const gmail = this.ensureConnected();
|
|
833
883
|
await gmail.users.messages.modify({
|
|
@@ -854,6 +904,74 @@ var GmailAdapter = class {
|
|
|
854
904
|
messageCount: label.messagesTotal ?? 0
|
|
855
905
|
}));
|
|
856
906
|
}
|
|
907
|
+
/**
|
|
908
|
+
* Trains Gmail's own spam filter — the same signal the "Report spam" UI
|
|
909
|
+
* button sends (add SPAM, drop INBOX). Not an abuse report to Google.
|
|
910
|
+
*/
|
|
911
|
+
async reportSpam(emailId, _sourceFolder) {
|
|
912
|
+
const gmail = this.ensureConnected();
|
|
913
|
+
await gmail.users.messages.modify({
|
|
914
|
+
userId: "me",
|
|
915
|
+
id: emailId,
|
|
916
|
+
requestBody: { addLabelIds: ["SPAM"], removeLabelIds: ["INBOX"] }
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
async createBlockRule(rule) {
|
|
920
|
+
const gmail = this.ensureConnected();
|
|
921
|
+
const criteria = {};
|
|
922
|
+
switch (rule.matchType) {
|
|
923
|
+
case "senderDomain":
|
|
924
|
+
criteria.from = `@${rule.value}`;
|
|
925
|
+
break;
|
|
926
|
+
case "senderAddress":
|
|
927
|
+
criteria.from = rule.value;
|
|
928
|
+
break;
|
|
929
|
+
case "subjectContains":
|
|
930
|
+
criteria.subject = rule.value;
|
|
931
|
+
break;
|
|
932
|
+
case "headerContains":
|
|
933
|
+
criteria.query = rule.value;
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
const action = rule.action === "delete" ? { addLabelIds: ["TRASH"] } : { addLabelIds: ["SPAM"], removeLabelIds: ["INBOX"] };
|
|
937
|
+
const res = await gmail.users.settings.filters.create({
|
|
938
|
+
userId: "me",
|
|
939
|
+
requestBody: { criteria, action }
|
|
940
|
+
});
|
|
941
|
+
return { id: res.data.id || "" };
|
|
942
|
+
}
|
|
943
|
+
async listBlockRules() {
|
|
944
|
+
const gmail = this.ensureConnected();
|
|
945
|
+
const res = await gmail.users.settings.filters.list({ userId: "me" });
|
|
946
|
+
return (res.data.filter || []).map((f) => {
|
|
947
|
+
const criteria = f.criteria || {};
|
|
948
|
+
const addLabelIds = f.action?.addLabelIds || [];
|
|
949
|
+
let matchType = "headerContains";
|
|
950
|
+
let value = criteria.query || "";
|
|
951
|
+
if (typeof criteria.from === "string" && criteria.from.startsWith("@")) {
|
|
952
|
+
matchType = "senderDomain";
|
|
953
|
+
value = criteria.from.slice(1);
|
|
954
|
+
} else if (criteria.from) {
|
|
955
|
+
matchType = "senderAddress";
|
|
956
|
+
value = criteria.from;
|
|
957
|
+
} else if (criteria.subject) {
|
|
958
|
+
matchType = "subjectContains";
|
|
959
|
+
value = criteria.subject;
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
id: f.id || "",
|
|
963
|
+
matchType,
|
|
964
|
+
value,
|
|
965
|
+
action: addLabelIds.includes("TRASH") ? "delete" : "moveToJunk",
|
|
966
|
+
createdAt: ""
|
|
967
|
+
// Gmail's filter API does not expose a creation timestamp
|
|
968
|
+
};
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
async deleteBlockRule(ruleId) {
|
|
972
|
+
const gmail = this.ensureConnected();
|
|
973
|
+
await gmail.users.settings.filters.delete({ userId: "me", id: ruleId });
|
|
974
|
+
}
|
|
857
975
|
buildRfc2822(params) {
|
|
858
976
|
const lines = [];
|
|
859
977
|
lines.push(`From: ${this.email}`);
|
|
@@ -1209,6 +1327,41 @@ var OutlookAdapter = class {
|
|
|
1209
1327
|
destinationId
|
|
1210
1328
|
});
|
|
1211
1329
|
}
|
|
1330
|
+
async getRawMessage(emailId, _sourceFolder) {
|
|
1331
|
+
this.ensureClient();
|
|
1332
|
+
const res = await fetch(
|
|
1333
|
+
`https://graph.microsoft.com/v1.0/me/messages/${encodeURIComponent(emailId)}/$value`,
|
|
1334
|
+
{ headers: { Authorization: `Bearer ${this.accessToken}` } }
|
|
1335
|
+
);
|
|
1336
|
+
if (!res.ok) {
|
|
1337
|
+
throw new Error(`Graph $value fetch failed for ${emailId}: HTTP ${res.status}`);
|
|
1338
|
+
}
|
|
1339
|
+
return Buffer.from(await res.arrayBuffer());
|
|
1340
|
+
}
|
|
1341
|
+
async appendRawMessage(raw, targetFolder, flags) {
|
|
1342
|
+
this.ensureClient();
|
|
1343
|
+
const endpoint = targetFolder ? `https://graph.microsoft.com/v1.0/me/mailFolders/${encodeURIComponent(await this.resolveFolder(targetFolder))}/messages` : "https://graph.microsoft.com/v1.0/me/messages";
|
|
1344
|
+
const res = await fetch(endpoint, {
|
|
1345
|
+
method: "POST",
|
|
1346
|
+
headers: {
|
|
1347
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
1348
|
+
"Content-Type": "text/plain"
|
|
1349
|
+
},
|
|
1350
|
+
body: raw.toString("base64")
|
|
1351
|
+
});
|
|
1352
|
+
if (!res.ok) {
|
|
1353
|
+
const detail = await res.text().catch(() => "");
|
|
1354
|
+
throw new Error(`Graph MIME import failed: HTTP ${res.status} ${detail.slice(0, 200)}`);
|
|
1355
|
+
}
|
|
1356
|
+
const data = await res.json().catch(() => ({}));
|
|
1357
|
+
if (flags && data.id && (flags.read !== void 0 || flags.starred !== void 0)) {
|
|
1358
|
+
try {
|
|
1359
|
+
await this.markEmail(data.id, flags);
|
|
1360
|
+
} catch {
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return { id: data.id || "" };
|
|
1364
|
+
}
|
|
1212
1365
|
async deleteEmail(emailId, permanent, _sourceFolder) {
|
|
1213
1366
|
const client = this.ensureClient();
|
|
1214
1367
|
if (permanent) {
|
|
@@ -1338,6 +1491,86 @@ var OutlookAdapter = class {
|
|
|
1338
1491
|
}
|
|
1339
1492
|
return result;
|
|
1340
1493
|
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Trains Outlook's own spam filter — the same signal "Report Junk" sends
|
|
1496
|
+
* in the UI (move to the Junk Email well-known folder). Not an abuse
|
|
1497
|
+
* report to Microsoft's security team.
|
|
1498
|
+
*/
|
|
1499
|
+
async reportSpam(emailId, _sourceFolder) {
|
|
1500
|
+
const client = this.ensureClient();
|
|
1501
|
+
await client.api(`/me/messages/${encodeURIComponent(emailId)}/move`).post({
|
|
1502
|
+
destinationId: "junkemail"
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Requires the `MailboxSettings.ReadWrite` delegated scope, confirmed by
|
|
1507
|
+
* Microsoft's own docs to be supported for personal Microsoft accounts
|
|
1508
|
+
* (outlook.com/hotmail), not just work/school. Accounts authenticated
|
|
1509
|
+
* before this scope was added need to re-run the setup wizard to
|
|
1510
|
+
* re-consent — old tokens will 403 on this call until then.
|
|
1511
|
+
*/
|
|
1512
|
+
async createBlockRule(rule) {
|
|
1513
|
+
const client = this.ensureClient();
|
|
1514
|
+
const conditions = {};
|
|
1515
|
+
switch (rule.matchType) {
|
|
1516
|
+
case "senderDomain":
|
|
1517
|
+
case "senderAddress":
|
|
1518
|
+
conditions.senderContains = [rule.value];
|
|
1519
|
+
break;
|
|
1520
|
+
case "subjectContains":
|
|
1521
|
+
conditions.subjectContains = [rule.value];
|
|
1522
|
+
break;
|
|
1523
|
+
case "headerContains":
|
|
1524
|
+
conditions.headerContains = [rule.value];
|
|
1525
|
+
break;
|
|
1526
|
+
}
|
|
1527
|
+
const actions = { stopProcessingRules: true };
|
|
1528
|
+
if (rule.action === "delete") {
|
|
1529
|
+
actions.delete = true;
|
|
1530
|
+
} else {
|
|
1531
|
+
actions.moveToFolder = await this.resolveFolder("junkemail");
|
|
1532
|
+
}
|
|
1533
|
+
const existing = await this.listBlockRules();
|
|
1534
|
+
const result = await client.api("/me/mailFolders/inbox/messageRules").post({
|
|
1535
|
+
displayName: `email-mcp: ${rule.matchType} "${rule.value}"`.slice(0, 255),
|
|
1536
|
+
sequence: existing.length + 1,
|
|
1537
|
+
isEnabled: true,
|
|
1538
|
+
conditions,
|
|
1539
|
+
actions
|
|
1540
|
+
});
|
|
1541
|
+
return { id: result.id };
|
|
1542
|
+
}
|
|
1543
|
+
async listBlockRules() {
|
|
1544
|
+
const client = this.ensureClient();
|
|
1545
|
+
const response = await client.api("/me/mailFolders/inbox/messageRules").get();
|
|
1546
|
+
return (response.value || []).map((r) => {
|
|
1547
|
+
const conditions = r.conditions || {};
|
|
1548
|
+
let matchType = "headerContains";
|
|
1549
|
+
let value = "";
|
|
1550
|
+
if (conditions.senderContains?.length) {
|
|
1551
|
+
value = conditions.senderContains[0];
|
|
1552
|
+
matchType = value.includes("@") ? "senderAddress" : "senderDomain";
|
|
1553
|
+
} else if (conditions.subjectContains?.length) {
|
|
1554
|
+
matchType = "subjectContains";
|
|
1555
|
+
value = conditions.subjectContains[0];
|
|
1556
|
+
} else if (conditions.headerContains?.length) {
|
|
1557
|
+
matchType = "headerContains";
|
|
1558
|
+
value = conditions.headerContains[0];
|
|
1559
|
+
}
|
|
1560
|
+
return {
|
|
1561
|
+
id: r.id,
|
|
1562
|
+
matchType,
|
|
1563
|
+
value,
|
|
1564
|
+
action: r.actions?.delete ? "delete" : "moveToJunk",
|
|
1565
|
+
createdAt: ""
|
|
1566
|
+
// Graph's messageRule resource has no creation timestamp field
|
|
1567
|
+
};
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
async deleteBlockRule(ruleId) {
|
|
1571
|
+
const client = this.ensureClient();
|
|
1572
|
+
await client.api(`/me/mailFolders/inbox/messageRules/${encodeURIComponent(ruleId)}`).delete();
|
|
1573
|
+
}
|
|
1341
1574
|
async getCategories() {
|
|
1342
1575
|
const client = this.ensureClient();
|
|
1343
1576
|
const response = await client.api("/me/outlook/masterCategories").get();
|
|
@@ -1887,6 +2120,30 @@ var ImapAdapter = class {
|
|
|
1887
2120
|
lock.release();
|
|
1888
2121
|
}
|
|
1889
2122
|
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Best-effort only: generic IMAP has no server-side ML classifier to
|
|
2125
|
+
* train, so this just moves the message to the account's Junk-typed
|
|
2126
|
+
* folder (via the same alias resolution `deleteEmail` uses for Trash).
|
|
2127
|
+
* No standing block-rule mechanism exists across IMAP servers (Sieve
|
|
2128
|
+
* exists on some but isn't universal), so `createBlockRule` is
|
|
2129
|
+
* intentionally left unimplemented for this provider.
|
|
2130
|
+
*/
|
|
2131
|
+
async reportSpam(emailId, sourceFolder) {
|
|
2132
|
+
if (!this.client) throw new Error("Not connected");
|
|
2133
|
+
const folder = sourceFolder || "INBOX";
|
|
2134
|
+
const junkFolder = await this.resolveFolder("junk");
|
|
2135
|
+
let lock;
|
|
2136
|
+
try {
|
|
2137
|
+
lock = await this.client.getMailboxLock(folder);
|
|
2138
|
+
} catch (error) {
|
|
2139
|
+
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
2140
|
+
}
|
|
2141
|
+
try {
|
|
2142
|
+
await this.client.messageMove(emailId, junkFolder, { uid: true });
|
|
2143
|
+
} finally {
|
|
2144
|
+
lock.release();
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
1890
2147
|
async deleteEmail(emailId, permanent, sourceFolder) {
|
|
1891
2148
|
if (!this.client) throw new Error("Not connected");
|
|
1892
2149
|
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
@@ -1931,6 +2188,32 @@ var ImapAdapter = class {
|
|
|
1931
2188
|
lock.release();
|
|
1932
2189
|
}
|
|
1933
2190
|
}
|
|
2191
|
+
async getRawMessage(emailId, sourceFolder) {
|
|
2192
|
+
if (!this.client) throw new Error("Not connected");
|
|
2193
|
+
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
2194
|
+
let lock;
|
|
2195
|
+
try {
|
|
2196
|
+
lock = await this.client.getMailboxLock(folder);
|
|
2197
|
+
} catch (error) {
|
|
2198
|
+
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
2199
|
+
}
|
|
2200
|
+
try {
|
|
2201
|
+
const msg = await this.client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
|
|
2202
|
+
if (!msg || !msg.source) throw new Error(`Email ${emailId} not found in "${folder}"`);
|
|
2203
|
+
return msg.source;
|
|
2204
|
+
} finally {
|
|
2205
|
+
lock.release();
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
async appendRawMessage(raw, targetFolder, flags) {
|
|
2209
|
+
if (!this.client) throw new Error("Not connected");
|
|
2210
|
+
const folder = targetFolder ? await this.resolveFolder(targetFolder) : "INBOX";
|
|
2211
|
+
const imapFlags = [];
|
|
2212
|
+
if (flags?.read) imapFlags.push("\\Seen");
|
|
2213
|
+
if (flags?.starred) imapFlags.push("\\Flagged");
|
|
2214
|
+
const result = await this.client.append(folder, raw, imapFlags);
|
|
2215
|
+
return { id: String(result?.uid ?? "") };
|
|
2216
|
+
}
|
|
1934
2217
|
async batchDelete(emailIds, permanent, sourceFolder) {
|
|
1935
2218
|
if (!this.client) throw new Error("Not connected");
|
|
1936
2219
|
const result = { succeeded: [], failed: [] };
|