@marlinjai/email-mcp 1.3.0 → 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 CHANGED
@@ -103,7 +103,7 @@ npx @marlinjai/email-mcp setup
103
103
  # Optionally enter SMTP host and port for sending
104
104
  ```
105
105
 
106
- ## Available Tools (25)
106
+ ## Available Tools (30)
107
107
 
108
108
  ### Account Management (4)
109
109
 
@@ -157,6 +157,18 @@ npx @marlinjai/email-mcp setup
157
157
 
158
158
  All batch tools accept a `sourceFolder` parameter for IMAP/iCloud and include a sequential fallback for maximum compatibility.
159
159
 
160
+ ### Spam Moderation (5)
161
+
162
+ | Tool | Description |
163
+ |------|-------------|
164
+ | `email_report_spam` | Report an email as spam/junk, training the provider's own filter — the same signal the "Report Junk" button sends in Gmail/Outlook. This is different from `email_delete`, which removes the message but teaches the filter nothing. **Not** an abuse report to the provider's security team; it only trains this account's filter |
165
+ | `email_batch_report_spam` | Report multiple emails as spam/junk at once |
166
+ | `email_create_block_rule` | Create a standing rule that intercepts future mail matching a pattern (sender domain/address, subject, or arbitrary header content) and either deletes it or files it straight to Junk. Use `headerContains` (e.g. a Reply-To domain) to block a spam template family whose visible "From" domain rotates — matching the rotating domain directly stops working within days. **Not supported on iCloud/generic IMAP** (no standard server-side rule mechanism exists across IMAP servers). On Outlook, requires the `MailboxSettings.ReadWrite` scope — accounts authenticated before this tool existed need to re-run the setup wizard once to re-consent |
167
+ | `email_list_block_rules` | List the standing block rules on an account, for auditing or before deleting one |
168
+ | `email_delete_block_rule` | Delete a standing block rule — use to undo a rule that turned out too broad |
169
+
170
+ Gmail and Outlook only for the rule tools; `email_report_spam`/`email_batch_report_spam` work on every provider (iCloud/IMAP fall back to a best-effort move into the account's Junk-typed folder, with no vendor ML training signal since generic IMAP has none to train).
171
+
160
172
  ## Usage with Claude Code
161
173
 
162
174
  Add the following to your `.mcp.json` file (project-level or global `~/.claude/.mcp.json`):
package/dist/index.js CHANGED
@@ -7463,6 +7463,74 @@ var init_adapter = __esm({
7463
7463
  messageCount: label.messagesTotal ?? 0
7464
7464
  }));
7465
7465
  }
7466
+ /**
7467
+ * Trains Gmail's own spam filter — the same signal the "Report spam" UI
7468
+ * button sends (add SPAM, drop INBOX). Not an abuse report to Google.
7469
+ */
7470
+ async reportSpam(emailId, _sourceFolder) {
7471
+ const gmail = this.ensureConnected();
7472
+ await gmail.users.messages.modify({
7473
+ userId: "me",
7474
+ id: emailId,
7475
+ requestBody: { addLabelIds: ["SPAM"], removeLabelIds: ["INBOX"] }
7476
+ });
7477
+ }
7478
+ async createBlockRule(rule) {
7479
+ const gmail = this.ensureConnected();
7480
+ const criteria = {};
7481
+ switch (rule.matchType) {
7482
+ case "senderDomain":
7483
+ criteria.from = `@${rule.value}`;
7484
+ break;
7485
+ case "senderAddress":
7486
+ criteria.from = rule.value;
7487
+ break;
7488
+ case "subjectContains":
7489
+ criteria.subject = rule.value;
7490
+ break;
7491
+ case "headerContains":
7492
+ criteria.query = rule.value;
7493
+ break;
7494
+ }
7495
+ const action = rule.action === "delete" ? { addLabelIds: ["TRASH"] } : { addLabelIds: ["SPAM"], removeLabelIds: ["INBOX"] };
7496
+ const res = await gmail.users.settings.filters.create({
7497
+ userId: "me",
7498
+ requestBody: { criteria, action }
7499
+ });
7500
+ return { id: res.data.id || "" };
7501
+ }
7502
+ async listBlockRules() {
7503
+ const gmail = this.ensureConnected();
7504
+ const res = await gmail.users.settings.filters.list({ userId: "me" });
7505
+ return (res.data.filter || []).map((f) => {
7506
+ const criteria = f.criteria || {};
7507
+ const addLabelIds = f.action?.addLabelIds || [];
7508
+ let matchType = "headerContains";
7509
+ let value = criteria.query || "";
7510
+ if (typeof criteria.from === "string" && criteria.from.startsWith("@")) {
7511
+ matchType = "senderDomain";
7512
+ value = criteria.from.slice(1);
7513
+ } else if (criteria.from) {
7514
+ matchType = "senderAddress";
7515
+ value = criteria.from;
7516
+ } else if (criteria.subject) {
7517
+ matchType = "subjectContains";
7518
+ value = criteria.subject;
7519
+ }
7520
+ return {
7521
+ id: f.id || "",
7522
+ matchType,
7523
+ value,
7524
+ action: addLabelIds.includes("TRASH") ? "delete" : "moveToJunk",
7525
+ createdAt: ""
7526
+ // Gmail's filter API does not expose a creation timestamp
7527
+ };
7528
+ });
7529
+ }
7530
+ async deleteBlockRule(ruleId) {
7531
+ const gmail = this.ensureConnected();
7532
+ await gmail.users.settings.filters.delete({ userId: "me", id: ruleId });
7533
+ }
7466
7534
  buildRfc2822(params) {
7467
7535
  const lines = [];
7468
7536
  lines.push(`From: ${this.email}`);
@@ -8059,6 +8127,86 @@ var init_adapter2 = __esm({
8059
8127
  }
8060
8128
  return result;
8061
8129
  }
8130
+ /**
8131
+ * Trains Outlook's own spam filter — the same signal "Report Junk" sends
8132
+ * in the UI (move to the Junk Email well-known folder). Not an abuse
8133
+ * report to Microsoft's security team.
8134
+ */
8135
+ async reportSpam(emailId, _sourceFolder) {
8136
+ const client = this.ensureClient();
8137
+ await client.api(`/me/messages/${encodeURIComponent(emailId)}/move`).post({
8138
+ destinationId: "junkemail"
8139
+ });
8140
+ }
8141
+ /**
8142
+ * Requires the `MailboxSettings.ReadWrite` delegated scope, confirmed by
8143
+ * Microsoft's own docs to be supported for personal Microsoft accounts
8144
+ * (outlook.com/hotmail), not just work/school. Accounts authenticated
8145
+ * before this scope was added need to re-run the setup wizard to
8146
+ * re-consent — old tokens will 403 on this call until then.
8147
+ */
8148
+ async createBlockRule(rule) {
8149
+ const client = this.ensureClient();
8150
+ const conditions = {};
8151
+ switch (rule.matchType) {
8152
+ case "senderDomain":
8153
+ case "senderAddress":
8154
+ conditions.senderContains = [rule.value];
8155
+ break;
8156
+ case "subjectContains":
8157
+ conditions.subjectContains = [rule.value];
8158
+ break;
8159
+ case "headerContains":
8160
+ conditions.headerContains = [rule.value];
8161
+ break;
8162
+ }
8163
+ const actions = { stopProcessingRules: true };
8164
+ if (rule.action === "delete") {
8165
+ actions.delete = true;
8166
+ } else {
8167
+ actions.moveToFolder = await this.resolveFolder("junkemail");
8168
+ }
8169
+ const existing = await this.listBlockRules();
8170
+ const result = await client.api("/me/mailFolders/inbox/messageRules").post({
8171
+ displayName: `email-mcp: ${rule.matchType} "${rule.value}"`.slice(0, 255),
8172
+ sequence: existing.length + 1,
8173
+ isEnabled: true,
8174
+ conditions,
8175
+ actions
8176
+ });
8177
+ return { id: result.id };
8178
+ }
8179
+ async listBlockRules() {
8180
+ const client = this.ensureClient();
8181
+ const response = await client.api("/me/mailFolders/inbox/messageRules").get();
8182
+ return (response.value || []).map((r) => {
8183
+ const conditions = r.conditions || {};
8184
+ let matchType = "headerContains";
8185
+ let value = "";
8186
+ if (conditions.senderContains?.length) {
8187
+ value = conditions.senderContains[0];
8188
+ matchType = value.includes("@") ? "senderAddress" : "senderDomain";
8189
+ } else if (conditions.subjectContains?.length) {
8190
+ matchType = "subjectContains";
8191
+ value = conditions.subjectContains[0];
8192
+ } else if (conditions.headerContains?.length) {
8193
+ matchType = "headerContains";
8194
+ value = conditions.headerContains[0];
8195
+ }
8196
+ return {
8197
+ id: r.id,
8198
+ matchType,
8199
+ value,
8200
+ action: r.actions?.delete ? "delete" : "moveToJunk",
8201
+ createdAt: ""
8202
+ // Graph's messageRule resource has no creation timestamp field
8203
+ };
8204
+ });
8205
+ }
8206
+ async deleteBlockRule(ruleId) {
8207
+ const client = this.ensureClient();
8208
+ await client.api(`/me/mailFolders/inbox/messageRules/${encodeURIComponent(ruleId)}`).delete();
8209
+ }
8062
8210
  async getCategories() {
8063
8211
  const client = this.ensureClient();
8064
8212
  const response = await client.api("/me/outlook/masterCategories").get();
@@ -8122,7 +8270,10 @@ var init_auth2 = __esm({
8122
8270
  DEFAULT_CACHE_FILE = path2.join(DEFAULT_CACHE_DIR, "msal-cache.json");
8123
8271
  OutlookAuth = class _OutlookAuth {
8124
8272
  static AUTHORITY = "https://login.microsoftonline.com/consumers";
8125
- static SCOPES = ["Mail.ReadWrite", "Mail.Send", "offline_access"];
8273
+ // MailboxSettings.ReadWrite is required for the messageRules API (block
8274
+ // rules) — confirmed by Microsoft's own docs to be supported for personal
8275
+ // Microsoft accounts (not just work/school), not just Mail.ReadWrite/Send.
8276
+ static SCOPES = ["Mail.ReadWrite", "Mail.Send", "MailboxSettings.ReadWrite", "offline_access"];
8126
8277
  pca;
8127
8278
  cryptoProvider;
8128
8279
  constructor(clientId, cacheFilePath) {
@@ -8745,6 +8896,30 @@ var init_adapter3 = __esm({
8745
8896
  lock.release();
8746
8897
  }
8747
8898
  }
8899
+ /**
8900
+ * Best-effort only: generic IMAP has no server-side ML classifier to
8901
+ * train, so this just moves the message to the account's Junk-typed
8902
+ * folder (via the same alias resolution `deleteEmail` uses for Trash).
8903
+ * No standing block-rule mechanism exists across IMAP servers (Sieve
8904
+ * exists on some but isn't universal), so `createBlockRule` is
8905
+ * intentionally left unimplemented for this provider.
8906
+ */
8907
+ async reportSpam(emailId, sourceFolder) {
8908
+ if (!this.client) throw new Error("Not connected");
8909
+ const folder = sourceFolder || "INBOX";
8910
+ const junkFolder = await this.resolveFolder("junk");
8911
+ let lock;
8912
+ try {
8913
+ lock = await this.client.getMailboxLock(folder);
8914
+ } catch (error48) {
8915
+ throw formatImapError(error48, `Failed to open folder "${folder}"`);
8916
+ }
8917
+ try {
8918
+ await this.client.messageMove(emailId, junkFolder, { uid: true });
8919
+ } finally {
8920
+ lock.release();
8921
+ }
8922
+ }
8748
8923
  async deleteEmail(emailId, permanent, sourceFolder) {
8749
8924
  if (!this.client) throw new Error("Not connected");
8750
8925
  const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
@@ -33492,6 +33667,144 @@ function registerOrganizingTools(server, accountManager) {
33492
33667
  );
33493
33668
  }
33494
33669
 
33670
+ // src/tools/moderation.ts
33671
+ function jsonResult5(data) {
33672
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
33673
+ }
33674
+ var BLOCK_RULE_MATCH_TYPES = ["senderDomain", "senderAddress", "subjectContains", "headerContains"];
33675
+ var BLOCK_RULE_ACTIONS = ["delete", "moveToJunk"];
33676
+ function registerModerationTools(server, accountManager) {
33677
+ server.tool(
33678
+ "email_report_spam",
33679
+ `Report an email as spam/junk, training the provider's own spam filter (the same signal the "Report Junk" button sends in Gmail/Outlook). This is different from email_delete, which removes the message but teaches the filter nothing. Not an abuse report to the provider's security team \u2014 it only trains this account's filter.`,
33680
+ {
33681
+ accountId: external_exports3.string(),
33682
+ emailId: external_exports3.string(),
33683
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
33684
+ },
33685
+ async (args) => {
33686
+ try {
33687
+ const provider = await accountManager.getProvider(args.accountId);
33688
+ if (!provider.reportSpam) {
33689
+ return jsonResult5({ success: false, error: "email_report_spam is not supported on this provider" });
33690
+ }
33691
+ await provider.reportSpam(args.emailId, args.sourceFolder);
33692
+ return jsonResult5({ success: true });
33693
+ } catch (error48) {
33694
+ return jsonResult5({ success: false, error: error48.message });
33695
+ }
33696
+ }
33697
+ );
33698
+ server.tool(
33699
+ "email_batch_report_spam",
33700
+ "Report multiple emails as spam/junk at once. Much faster than individual calls.",
33701
+ {
33702
+ accountId: external_exports3.string(),
33703
+ emailIds: external_exports3.array(external_exports3.string()).min(1).describe("Array of email IDs to report as spam"),
33704
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when emails are not in INBOX)")
33705
+ },
33706
+ async (args) => {
33707
+ try {
33708
+ const provider = await accountManager.getProvider(args.accountId);
33709
+ if (!provider.reportSpam) {
33710
+ return jsonResult5({ success: false, error: "email_batch_report_spam is not supported on this provider" });
33711
+ }
33712
+ const result = { succeeded: [], failed: [] };
33713
+ for (const id of args.emailIds) {
33714
+ try {
33715
+ await provider.reportSpam(id, args.sourceFolder);
33716
+ result.succeeded.push(id);
33717
+ } catch (e) {
33718
+ result.failed.push({ id, error: e.message });
33719
+ }
33720
+ }
33721
+ return jsonResult5({ success: true, ...result });
33722
+ } catch (error48) {
33723
+ return jsonResult5({ success: false, error: error48.message });
33724
+ }
33725
+ }
33726
+ );
33727
+ server.tool(
33728
+ "email_create_block_rule",
33729
+ 'Create a standing rule that intercepts future mail matching a pattern, before it reaches the inbox. Use headerContains (e.g. matching a Reply-To domain) to block a spam template family whose visible "From" domain rotates \u2014 matching the rotating domain directly stops working within days. Not supported on iCloud/generic IMAP (no standard server-side rule mechanism exists across IMAP servers). Outlook requires the MailboxSettings.ReadWrite scope \u2014 accounts authenticated before this tool existed need to re-run the setup wizard once to re-consent.',
33730
+ {
33731
+ accountId: external_exports3.string(),
33732
+ matchType: external_exports3.enum(BLOCK_RULE_MATCH_TYPES).describe(
33733
+ "senderDomain/senderAddress: match the From address. subjectContains: match the subject. headerContains: match any header's raw content \u2014 the right choice for a stable element (e.g. a Reply-To domain) when the From domain rotates."
33734
+ ),
33735
+ value: external_exports3.string().describe("The domain, address, or text to match"),
33736
+ action: external_exports3.enum(BLOCK_RULE_ACTIONS).describe("delete: discard on arrival. moveToJunk: file straight to Junk/Spam without notifying.")
33737
+ },
33738
+ async (args) => {
33739
+ try {
33740
+ const provider = await accountManager.getProvider(args.accountId);
33741
+ if (!provider.createBlockRule) {
33742
+ return jsonResult5({
33743
+ success: false,
33744
+ error: "email_create_block_rule is not supported on this provider",
33745
+ supportedProviders: ["gmail", "outlook"]
33746
+ });
33747
+ }
33748
+ const result = await provider.createBlockRule({
33749
+ matchType: args.matchType,
33750
+ value: args.value,
33751
+ action: args.action
33752
+ });
33753
+ return jsonResult5({ success: true, data: result });
33754
+ } catch (error48) {
33755
+ return jsonResult5({ success: false, error: error48.message });
33756
+ }
33757
+ }
33758
+ );
33759
+ server.tool(
33760
+ "email_list_block_rules",
33761
+ "List the standing block rules on an account, for auditing or before deleting one.",
33762
+ {
33763
+ accountId: external_exports3.string()
33764
+ },
33765
+ async (args) => {
33766
+ try {
33767
+ const provider = await accountManager.getProvider(args.accountId);
33768
+ if (!provider.listBlockRules) {
33769
+ return jsonResult5({
33770
+ success: false,
33771
+ error: "email_list_block_rules is not supported on this provider",
33772
+ supportedProviders: ["gmail", "outlook"]
33773
+ });
33774
+ }
33775
+ const rules = await provider.listBlockRules();
33776
+ return jsonResult5({ success: true, data: rules });
33777
+ } catch (error48) {
33778
+ return jsonResult5({ success: false, error: error48.message });
33779
+ }
33780
+ }
33781
+ );
33782
+ server.tool(
33783
+ "email_delete_block_rule",
33784
+ "Delete a standing block rule \u2014 use this to undo a rule that turned out to be too broad (e.g. it blocked a domain a real correspondent also uses).",
33785
+ {
33786
+ accountId: external_exports3.string(),
33787
+ ruleId: external_exports3.string()
33788
+ },
33789
+ async (args) => {
33790
+ try {
33791
+ const provider = await accountManager.getProvider(args.accountId);
33792
+ if (!provider.deleteBlockRule) {
33793
+ return jsonResult5({
33794
+ success: false,
33795
+ error: "email_delete_block_rule is not supported on this provider",
33796
+ supportedProviders: ["gmail", "outlook"]
33797
+ });
33798
+ }
33799
+ await provider.deleteBlockRule(args.ruleId);
33800
+ return jsonResult5({ success: true });
33801
+ } catch (error48) {
33802
+ return jsonResult5({ success: false, error: error48.message });
33803
+ }
33804
+ }
33805
+ );
33806
+ }
33807
+
33495
33808
  // src/server.ts
33496
33809
  async function createServer(accountManager) {
33497
33810
  const mgr = accountManager ?? new AccountManager();
@@ -33503,6 +33816,7 @@ async function createServer(accountManager) {
33503
33816
  registerReadingTools(server, mgr);
33504
33817
  registerSendingTools(server, mgr);
33505
33818
  registerOrganizingTools(server, mgr);
33819
+ registerModerationTools(server, mgr);
33506
33820
  return { server, accountManager: mgr };
33507
33821
  }
33508
33822
  async function startServer() {