@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.
@@ -1377,23 +1377,36 @@ var GmailAdapter = class {
1377
1377
  requestBody: { addLabelIds: ["SPAM"], removeLabelIds: ["INBOX"] }
1378
1378
  });
1379
1379
  }
1380
- async createBlockRule(rule) {
1381
- const gmail = this.ensureConnected();
1380
+ filterCriteria(matchType, value) {
1382
1381
  const criteria = {};
1383
- switch (rule.matchType) {
1382
+ switch (matchType) {
1384
1383
  case "senderDomain":
1385
- criteria.from = `@${rule.value}`;
1384
+ criteria.from = `@${value}`;
1386
1385
  break;
1387
1386
  case "senderAddress":
1388
- criteria.from = rule.value;
1387
+ criteria.from = value;
1389
1388
  break;
1390
1389
  case "subjectContains":
1391
- criteria.subject = rule.value;
1390
+ criteria.subject = value;
1392
1391
  break;
1393
1392
  case "headerContains":
1394
- criteria.query = rule.value;
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
- matchType,
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
- async createBlockRule(rule) {
1965
- const client = this.ensureClient();
2019
+ ruleConditions(matchType, value) {
1966
2020
  const conditions = {};
1967
- switch (rule.matchType) {
2021
+ switch (matchType) {
1968
2022
  case "senderDomain":
1969
2023
  case "senderAddress":
1970
- conditions.senderContains = [rule.value];
2024
+ conditions.senderContains = [value];
1971
2025
  break;
1972
2026
  case "subjectContains":
1973
- conditions.subjectContains = [rule.value];
2027
+ conditions.subjectContains = [value];
1974
2028
  break;
1975
2029
  case "headerContains":
1976
- conditions.headerContains = [rule.value];
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.listBlockRules();
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
- const client = this.ensureClient();
1997
- const response = await client.api("/me/mailFolders/inbox/messageRules").get();
1998
- return (response.value || []).map((r) => {
1999
- const conditions = r.conditions || {};
2000
- let matchType = "headerContains";
2001
- let value = "";
2002
- if (conditions.senderContains?.length) {
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(`