@marlinjai/email-mcp 1.8.1 → 1.9.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 +32 -3
- package/dist/index.js +291 -57
- package/dist/index.js.map +4 -4
- package/dist/setup/wizard.js +169 -56
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +1 -1
package/dist/setup/wizard.js
CHANGED
|
@@ -1377,23 +1377,36 @@ var GmailAdapter = class {
|
|
|
1377
1377
|
requestBody: { addLabelIds: ["SPAM"], removeLabelIds: ["INBOX"] }
|
|
1378
1378
|
});
|
|
1379
1379
|
}
|
|
1380
|
-
|
|
1381
|
-
const gmail = this.ensureConnected();
|
|
1380
|
+
filterCriteria(matchType, value) {
|
|
1382
1381
|
const criteria = {};
|
|
1383
|
-
switch (
|
|
1382
|
+
switch (matchType) {
|
|
1384
1383
|
case "senderDomain":
|
|
1385
|
-
criteria.from = `@${
|
|
1384
|
+
criteria.from = `@${value}`;
|
|
1386
1385
|
break;
|
|
1387
1386
|
case "senderAddress":
|
|
1388
|
-
criteria.from =
|
|
1387
|
+
criteria.from = value;
|
|
1389
1388
|
break;
|
|
1390
1389
|
case "subjectContains":
|
|
1391
|
-
criteria.subject =
|
|
1390
|
+
criteria.subject = value;
|
|
1392
1391
|
break;
|
|
1393
1392
|
case "headerContains":
|
|
1394
|
-
criteria.query =
|
|
1393
|
+
criteria.query = value;
|
|
1395
1394
|
break;
|
|
1396
1395
|
}
|
|
1396
|
+
return criteria;
|
|
1397
|
+
}
|
|
1398
|
+
/** The reverse of filterCriteria, for listing. A best-effort reading of filters made elsewhere. */
|
|
1399
|
+
readCriteria(criteria) {
|
|
1400
|
+
if (typeof criteria?.from === "string" && criteria.from.startsWith("@")) {
|
|
1401
|
+
return { matchType: "senderDomain", value: criteria.from.slice(1) };
|
|
1402
|
+
}
|
|
1403
|
+
if (criteria?.from) return { matchType: "senderAddress", value: criteria.from };
|
|
1404
|
+
if (criteria?.subject) return { matchType: "subjectContains", value: criteria.subject };
|
|
1405
|
+
return { matchType: "headerContains", value: criteria?.query || "" };
|
|
1406
|
+
}
|
|
1407
|
+
async createBlockRule(rule) {
|
|
1408
|
+
const gmail = this.ensureConnected();
|
|
1409
|
+
const criteria = this.filterCriteria(rule.matchType, rule.value);
|
|
1397
1410
|
const action = rule.action === "delete" ? { addLabelIds: ["TRASH"] } : { removeLabelIds: ["INBOX"] };
|
|
1398
1411
|
const res = await gmail.users.settings.filters.create({
|
|
1399
1412
|
userId: "me",
|
|
@@ -1404,25 +1417,11 @@ var GmailAdapter = class {
|
|
|
1404
1417
|
async listBlockRules() {
|
|
1405
1418
|
const gmail = this.ensureConnected();
|
|
1406
1419
|
const res = await gmail.users.settings.filters.list({ userId: "me" });
|
|
1407
|
-
return (res.data.filter || []).map((f) => {
|
|
1408
|
-
const criteria = f.criteria || {};
|
|
1420
|
+
return (res.data.filter || []).filter((f) => !f.action?.forward).map((f) => {
|
|
1409
1421
|
const addLabelIds = f.action?.addLabelIds || [];
|
|
1410
|
-
let matchType = "headerContains";
|
|
1411
|
-
let value = criteria.query || "";
|
|
1412
|
-
if (typeof criteria.from === "string" && criteria.from.startsWith("@")) {
|
|
1413
|
-
matchType = "senderDomain";
|
|
1414
|
-
value = criteria.from.slice(1);
|
|
1415
|
-
} else if (criteria.from) {
|
|
1416
|
-
matchType = "senderAddress";
|
|
1417
|
-
value = criteria.from;
|
|
1418
|
-
} else if (criteria.subject) {
|
|
1419
|
-
matchType = "subjectContains";
|
|
1420
|
-
value = criteria.subject;
|
|
1421
|
-
}
|
|
1422
1422
|
return {
|
|
1423
1423
|
id: f.id || "",
|
|
1424
|
-
|
|
1425
|
-
value,
|
|
1424
|
+
...this.readCriteria(f.criteria),
|
|
1426
1425
|
action: addLabelIds.includes("TRASH") ? "delete" : "moveToJunk",
|
|
1427
1426
|
createdAt: ""
|
|
1428
1427
|
// Gmail's filter API does not expose a creation timestamp
|
|
@@ -1433,6 +1432,62 @@ var GmailAdapter = class {
|
|
|
1433
1432
|
const gmail = this.ensureConnected();
|
|
1434
1433
|
await gmail.users.settings.filters.delete({ userId: "me", id: ruleId });
|
|
1435
1434
|
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Gmail only forwards to an address the user has added and confirmed under
|
|
1437
|
+
* Settings, "Forwarding and POP/IMAP". Adding one through the API needs a
|
|
1438
|
+
* scope this app does not request (gmail.settings.sharing), so a missing or
|
|
1439
|
+
* unconfirmed address is answered with the manual step, before Gmail's own
|
|
1440
|
+
* less helpful error.
|
|
1441
|
+
*/
|
|
1442
|
+
async createForwardRule(rule) {
|
|
1443
|
+
const gmail = this.ensureConnected();
|
|
1444
|
+
const target = rule.forwardTo.trim().toLowerCase();
|
|
1445
|
+
const addresses = await gmail.users.settings.forwardingAddresses.list({ userId: "me" });
|
|
1446
|
+
const known = (addresses.data.forwardingAddresses || []).find(
|
|
1447
|
+
(a) => (a.forwardingEmail || "").toLowerCase() === target
|
|
1448
|
+
);
|
|
1449
|
+
if (!known) {
|
|
1450
|
+
throw new Error(
|
|
1451
|
+
`${rule.forwardTo} is not a forwarding address of this Gmail account yet. Add it once in Gmail under Settings, "Forwarding and POP/IMAP", "Add a forwarding address", confirm the email Google sends to it, then run this again.`
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
if (known.verificationStatus !== "accepted") {
|
|
1455
|
+
throw new Error(
|
|
1456
|
+
`${rule.forwardTo} was added to this Gmail account but not confirmed yet. Open the confirmation email Google sent to that address, confirm it, then run this again.`
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
const existing = (await this.listForwardRules()).find(
|
|
1460
|
+
(r) => r.matchType === rule.matchType && r.value.toLowerCase() === rule.value.toLowerCase() && r.forwardTo.toLowerCase() === target
|
|
1461
|
+
);
|
|
1462
|
+
if (existing) return { id: existing.id, alreadyExisted: true };
|
|
1463
|
+
const action = { forward: known.forwardingEmail };
|
|
1464
|
+
if (!rule.keepInInbox) action.removeLabelIds = ["INBOX"];
|
|
1465
|
+
const res = await gmail.users.settings.filters.create({
|
|
1466
|
+
userId: "me",
|
|
1467
|
+
requestBody: { criteria: this.filterCriteria(rule.matchType, rule.value), action }
|
|
1468
|
+
});
|
|
1469
|
+
return { id: res.data.id || "" };
|
|
1470
|
+
}
|
|
1471
|
+
/** Every filter that forwards, including ones made by hand in Gmail. */
|
|
1472
|
+
async listForwardRules() {
|
|
1473
|
+
const gmail = this.ensureConnected();
|
|
1474
|
+
const res = await gmail.users.settings.filters.list({ userId: "me" });
|
|
1475
|
+
return (res.data.filter || []).filter((f) => f.action?.forward).map((f) => ({
|
|
1476
|
+
id: f.id || "",
|
|
1477
|
+
...this.readCriteria(f.criteria),
|
|
1478
|
+
forwardTo: f.action.forward,
|
|
1479
|
+
keepInInbox: !(f.action.removeLabelIds || []).includes("INBOX"),
|
|
1480
|
+
createdAt: ""
|
|
1481
|
+
// Gmail's filter API does not expose a creation timestamp
|
|
1482
|
+
}));
|
|
1483
|
+
}
|
|
1484
|
+
async deleteForwardRule(ruleId) {
|
|
1485
|
+
const rules = await this.listForwardRules();
|
|
1486
|
+
if (!rules.some((r) => r.id === ruleId)) {
|
|
1487
|
+
throw new Error(`No forwarding rule with id ${ruleId} on this account. Use email_list_forward_rules to see the ids.`);
|
|
1488
|
+
}
|
|
1489
|
+
await this.deleteBlockRule(ruleId);
|
|
1490
|
+
}
|
|
1436
1491
|
/**
|
|
1437
1492
|
* Builds the base64url `raw` payload Gmail expects for send, draft create and
|
|
1438
1493
|
* draft update: a full MIME message with a text part, an HTML part when one
|
|
@@ -1593,7 +1648,7 @@ function buildGraphFilter(query) {
|
|
|
1593
1648
|
}
|
|
1594
1649
|
|
|
1595
1650
|
// src/providers/outlook/adapter.ts
|
|
1596
|
-
var OutlookAdapter = class {
|
|
1651
|
+
var OutlookAdapter = class _OutlookAdapter {
|
|
1597
1652
|
providerType = ProviderType.Outlook;
|
|
1598
1653
|
client = null;
|
|
1599
1654
|
accountId = "";
|
|
@@ -1961,28 +2016,53 @@ var OutlookAdapter = class {
|
|
|
1961
2016
|
* before this scope was added need to re-run the setup wizard to
|
|
1962
2017
|
* re-consent — old tokens will 403 on this call until then.
|
|
1963
2018
|
*/
|
|
1964
|
-
|
|
1965
|
-
const client = this.ensureClient();
|
|
2019
|
+
ruleConditions(matchType, value) {
|
|
1966
2020
|
const conditions = {};
|
|
1967
|
-
switch (
|
|
2021
|
+
switch (matchType) {
|
|
1968
2022
|
case "senderDomain":
|
|
1969
2023
|
case "senderAddress":
|
|
1970
|
-
conditions.senderContains = [
|
|
2024
|
+
conditions.senderContains = [value];
|
|
1971
2025
|
break;
|
|
1972
2026
|
case "subjectContains":
|
|
1973
|
-
conditions.subjectContains = [
|
|
2027
|
+
conditions.subjectContains = [value];
|
|
1974
2028
|
break;
|
|
1975
2029
|
case "headerContains":
|
|
1976
|
-
conditions.headerContains = [
|
|
2030
|
+
conditions.headerContains = [value];
|
|
1977
2031
|
break;
|
|
1978
2032
|
}
|
|
2033
|
+
return conditions;
|
|
2034
|
+
}
|
|
2035
|
+
// Graph doesn't tag which of our four matchTypes a rule came from:
|
|
2036
|
+
// this reconstruction is a best-effort heuristic for listing/auditing,
|
|
2037
|
+
// not a guaranteed round-trip of what was originally requested.
|
|
2038
|
+
readConditions(conditions) {
|
|
2039
|
+
if (conditions?.senderContains?.length) {
|
|
2040
|
+
const value = conditions.senderContains[0];
|
|
2041
|
+
return { matchType: value.includes("@") ? "senderAddress" : "senderDomain", value };
|
|
2042
|
+
}
|
|
2043
|
+
if (conditions?.subjectContains?.length) return { matchType: "subjectContains", value: conditions.subjectContains[0] };
|
|
2044
|
+
if (conditions?.headerContains?.length) return { matchType: "headerContains", value: conditions.headerContains[0] };
|
|
2045
|
+
return { matchType: "headerContains", value: "" };
|
|
2046
|
+
}
|
|
2047
|
+
/** Addresses a rule sends mail on to, whether by "forward" or "redirect". */
|
|
2048
|
+
static forwardTargets(actions) {
|
|
2049
|
+
return [...actions?.forwardTo || [], ...actions?.redirectTo || [], ...actions?.forwardAsAttachmentTo || []].map((r) => r?.emailAddress?.address).filter((a) => typeof a === "string" && a.length > 0);
|
|
2050
|
+
}
|
|
2051
|
+
async listMessageRules() {
|
|
2052
|
+
const client = this.ensureClient();
|
|
2053
|
+
const response = await client.api("/me/mailFolders/inbox/messageRules").get();
|
|
2054
|
+
return response.value || [];
|
|
2055
|
+
}
|
|
2056
|
+
async createBlockRule(rule) {
|
|
2057
|
+
const client = this.ensureClient();
|
|
2058
|
+
const conditions = this.ruleConditions(rule.matchType, rule.value);
|
|
1979
2059
|
const actions = { stopProcessingRules: true };
|
|
1980
2060
|
if (rule.action === "delete") {
|
|
1981
2061
|
actions.delete = true;
|
|
1982
2062
|
} else {
|
|
1983
2063
|
actions.moveToFolder = await this.resolveFolder("junkemail");
|
|
1984
2064
|
}
|
|
1985
|
-
const existing = await this.
|
|
2065
|
+
const existing = await this.listMessageRules();
|
|
1986
2066
|
const result = await client.api("/me/mailFolders/inbox/messageRules").post({
|
|
1987
2067
|
displayName: `email-mcp: ${rule.matchType} "${rule.value}"`.slice(0, 255),
|
|
1988
2068
|
sequence: existing.length + 1,
|
|
@@ -1993,36 +2073,66 @@ var OutlookAdapter = class {
|
|
|
1993
2073
|
return { id: result.id };
|
|
1994
2074
|
}
|
|
1995
2075
|
async listBlockRules() {
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
value = conditions.senderContains[0];
|
|
2004
|
-
matchType = value.includes("@") ? "senderAddress" : "senderDomain";
|
|
2005
|
-
} else if (conditions.subjectContains?.length) {
|
|
2006
|
-
matchType = "subjectContains";
|
|
2007
|
-
value = conditions.subjectContains[0];
|
|
2008
|
-
} else if (conditions.headerContains?.length) {
|
|
2009
|
-
matchType = "headerContains";
|
|
2010
|
-
value = conditions.headerContains[0];
|
|
2011
|
-
}
|
|
2012
|
-
return {
|
|
2013
|
-
id: r.id,
|
|
2014
|
-
matchType,
|
|
2015
|
-
value,
|
|
2016
|
-
action: r.actions?.delete ? "delete" : "moveToJunk",
|
|
2017
|
-
createdAt: ""
|
|
2018
|
-
// Graph's messageRule resource has no creation timestamp field
|
|
2019
|
-
};
|
|
2020
|
-
});
|
|
2076
|
+
return (await this.listMessageRules()).filter((r) => _OutlookAdapter.forwardTargets(r.actions).length === 0).map((r) => ({
|
|
2077
|
+
id: r.id,
|
|
2078
|
+
...this.readConditions(r.conditions),
|
|
2079
|
+
action: r.actions?.delete ? "delete" : "moveToJunk",
|
|
2080
|
+
createdAt: ""
|
|
2081
|
+
// Graph's messageRule resource has no creation timestamp field
|
|
2082
|
+
}));
|
|
2021
2083
|
}
|
|
2022
2084
|
async deleteBlockRule(ruleId) {
|
|
2023
2085
|
const client = this.ensureClient();
|
|
2024
2086
|
await client.api(`/me/mailFolders/inbox/messageRules/${encodeURIComponent(ruleId)}`).delete();
|
|
2025
2087
|
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Outlook forwards to any address without a confirmation step, so the only
|
|
2090
|
+
* check on the target is the allowlist in the tool layer. Needs the same
|
|
2091
|
+
* MailboxSettings.ReadWrite scope as block rules.
|
|
2092
|
+
*/
|
|
2093
|
+
async createForwardRule(rule) {
|
|
2094
|
+
const client = this.ensureClient();
|
|
2095
|
+
const forwardTo = rule.forwardTo.trim();
|
|
2096
|
+
const target = forwardTo.toLowerCase();
|
|
2097
|
+
const all = await this.listMessageRules();
|
|
2098
|
+
const existing = all.find((r) => {
|
|
2099
|
+
const read = this.readConditions(r.conditions);
|
|
2100
|
+
const sameMatch = read.value.toLowerCase() === rule.value.toLowerCase() && (read.matchType === rule.matchType || read.matchType.startsWith("sender") && rule.matchType.startsWith("sender"));
|
|
2101
|
+
return sameMatch && _OutlookAdapter.forwardTargets(r.actions).some((a) => a.toLowerCase() === target);
|
|
2102
|
+
});
|
|
2103
|
+
if (existing) return { id: existing.id, alreadyExisted: true };
|
|
2104
|
+
const actions = { forwardTo: [{ emailAddress: { address: forwardTo } }] };
|
|
2105
|
+
if (!rule.keepInInbox) {
|
|
2106
|
+
actions.moveToFolder = await this.resolveFolder("archive");
|
|
2107
|
+
actions.stopProcessingRules = true;
|
|
2108
|
+
}
|
|
2109
|
+
const result = await client.api("/me/mailFolders/inbox/messageRules").post({
|
|
2110
|
+
displayName: `email-mcp: forward ${rule.matchType} "${rule.value}" to ${forwardTo}`.slice(0, 255),
|
|
2111
|
+
sequence: all.length + 1,
|
|
2112
|
+
isEnabled: true,
|
|
2113
|
+
conditions: this.ruleConditions(rule.matchType, rule.value),
|
|
2114
|
+
actions
|
|
2115
|
+
});
|
|
2116
|
+
return { id: result.id };
|
|
2117
|
+
}
|
|
2118
|
+
/** Every inbox rule that forwards or redirects, including ones made by hand in Outlook. */
|
|
2119
|
+
async listForwardRules() {
|
|
2120
|
+
return (await this.listMessageRules()).filter((r) => _OutlookAdapter.forwardTargets(r.actions).length > 0).map((r) => ({
|
|
2121
|
+
id: r.id,
|
|
2122
|
+
...this.readConditions(r.conditions),
|
|
2123
|
+
forwardTo: _OutlookAdapter.forwardTargets(r.actions).join(", "),
|
|
2124
|
+
keepInInbox: !r.actions?.moveToFolder && !r.actions?.delete,
|
|
2125
|
+
createdAt: ""
|
|
2126
|
+
// Graph's messageRule resource has no creation timestamp field
|
|
2127
|
+
}));
|
|
2128
|
+
}
|
|
2129
|
+
async deleteForwardRule(ruleId) {
|
|
2130
|
+
const rules = await this.listForwardRules();
|
|
2131
|
+
if (!rules.some((r) => r.id === ruleId)) {
|
|
2132
|
+
throw new Error(`No forwarding rule with id ${ruleId} on this account. Use email_list_forward_rules to see the ids.`);
|
|
2133
|
+
}
|
|
2134
|
+
await this.deleteBlockRule(ruleId);
|
|
2135
|
+
}
|
|
2026
2136
|
async getCategories() {
|
|
2027
2137
|
const client = this.ensureClient();
|
|
2028
2138
|
const response = await client.api("/me/outlook/masterCategories").get();
|
|
@@ -3390,6 +3500,9 @@ async function main() {
|
|
|
3390
3500
|
addMore = await askConfirm("Would you like to add another email account?", false);
|
|
3391
3501
|
}
|
|
3392
3502
|
console.log("\nSetup complete! You can now use the email tools in Claude Code.");
|
|
3503
|
+
console.log(
|
|
3504
|
+
"\nemail-mcp is free and open source. Google requires a yearly independent security\naudit (675 US dollars) for the Gmail sign-in it ships. If email-mcp is useful to you,\nyou can help cover it: https://github.com/sponsors/marlinjai"
|
|
3505
|
+
);
|
|
3393
3506
|
}
|
|
3394
3507
|
main().catch((err) => {
|
|
3395
3508
|
console.error(`
|