@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 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 (24)
106
+ ## Available Tools (30)
107
107
 
108
108
  ### Account Management (4)
109
109
 
@@ -134,11 +134,12 @@ npx @marlinjai/email-mcp setup
134
134
  | `email_draft_create` | Save a draft without sending |
135
135
  | `email_draft_list` | List all drafts |
136
136
 
137
- ### Organization (7)
137
+ ### Organization (8)
138
138
 
139
139
  | Tool | Description |
140
140
  |------|-------------|
141
141
  | `email_move` | Move an email to a different folder. Supports `sourceFolder` for IMAP/iCloud |
142
+ | `email_transfer` | Move or copy emails **between accounts**, preserving the original message (sender, date, threading) via raw MIME transfer. `deleteAfter=true` trashes the source only after a confirmed import (safe cross-account move) |
142
143
  | `email_delete` | Delete an email (trash or permanent). Supports `sourceFolder` for IMAP/iCloud |
143
144
  | `email_mark` | Mark as read/unread, starred, or flagged. Supports `sourceFolder` for IMAP/iCloud |
144
145
  | `email_label` | Add/remove labels (Gmail only) |
@@ -156,6 +157,18 @@ npx @marlinjai/email-mcp setup
156
157
 
157
158
  All batch tools accept a `sourceFolder` parameter for IMAP/iCloud and include a sequential fallback for maximum compatibility.
158
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
+
159
172
  ## Usage with Claude Code
160
173
 
161
174
  Add the following to your `.mcp.json` file (project-level or global `~/.claude/.mcp.json`):
package/dist/index.js CHANGED
@@ -7070,6 +7070,7 @@ var init_mapper = __esm({
7070
7070
  });
7071
7071
 
7072
7072
  // src/providers/gmail/adapter.ts
7073
+ import { Readable } from "node:stream";
7073
7074
  import { google } from "googleapis";
7074
7075
  import { OAuth2Client } from "google-auth-library";
7075
7076
  var GmailAdapter;
@@ -7129,20 +7130,32 @@ var init_adapter = __esm({
7129
7130
  async search(query) {
7130
7131
  const gmail = this.ensureConnected();
7131
7132
  const q = buildGmailQuery(query);
7132
- const params = {
7133
- userId: "me",
7134
- maxResults: query.limit || 20
7135
- };
7136
- if (q) params.q = q;
7137
- if (query.folder) params.labelIds = [query.folder];
7138
- const res = await gmail.users.messages.list(params);
7139
- const messages = res.data.messages || [];
7140
- if (messages.length === 0) return [];
7133
+ const limit = query.limit || 20;
7134
+ const offset = query.offset || 0;
7135
+ const needed = offset + limit;
7136
+ const ids = [];
7137
+ let pageToken;
7138
+ do {
7139
+ const params = {
7140
+ userId: "me",
7141
+ maxResults: Math.min(500, needed - ids.length)
7142
+ };
7143
+ if (q) params.q = q;
7144
+ if (query.folder) params.labelIds = [query.folder];
7145
+ if (pageToken) params.pageToken = pageToken;
7146
+ const res = await gmail.users.messages.list(params);
7147
+ for (const msg of res.data.messages || []) {
7148
+ if (msg.id) ids.push(msg.id);
7149
+ }
7150
+ pageToken = res.data.nextPageToken || void 0;
7151
+ } while (pageToken && ids.length < needed);
7152
+ const windowIds = ids.slice(offset, offset + limit);
7153
+ if (windowIds.length === 0) return [];
7141
7154
  const emails = [];
7142
- for (const msg of messages) {
7155
+ for (const id of windowIds) {
7143
7156
  const full = await gmail.users.messages.get({
7144
7157
  userId: "me",
7145
- id: msg.id,
7158
+ id,
7146
7159
  format: "full"
7147
7160
  });
7148
7161
  emails.push(mapGmailMessage(full.data, this.accountId));
@@ -7390,6 +7403,40 @@ var init_adapter = __esm({
7390
7403
  }
7391
7404
  return result;
7392
7405
  }
7406
+ async getRawMessage(emailId, _sourceFolder) {
7407
+ const gmail = this.ensureConnected();
7408
+ const res = await gmail.users.messages.get({
7409
+ userId: "me",
7410
+ id: emailId,
7411
+ format: "raw"
7412
+ });
7413
+ return Buffer.from(res.data.raw || "", "base64url");
7414
+ }
7415
+ async appendRawMessage(raw, targetFolder, flags) {
7416
+ const gmail = this.ensureConnected();
7417
+ const labelIds = [];
7418
+ labelIds.push(targetFolder ? await this.resolveLabelId(targetFolder) : "INBOX");
7419
+ if (flags?.read === false) labelIds.push("UNREAD");
7420
+ if (flags?.starred === true) labelIds.push("STARRED");
7421
+ const res = await gmail.users.messages.insert({
7422
+ userId: "me",
7423
+ internalDateSource: "dateHeader",
7424
+ requestBody: { labelIds },
7425
+ media: { mimeType: "message/rfc822", body: Readable.from(raw) }
7426
+ });
7427
+ return { id: res.data.id || "" };
7428
+ }
7429
+ /** Resolve a Gmail label name or id to a label id (accepts either). */
7430
+ async resolveLabelId(nameOrId) {
7431
+ const gmail = this.ensureConnected();
7432
+ const res = await gmail.users.labels.list({ userId: "me" });
7433
+ const labels = res.data.labels || [];
7434
+ const byId = labels.find((l) => l.id === nameOrId);
7435
+ if (byId?.id) return byId.id;
7436
+ const byName = labels.find((l) => (l.name || "").toLowerCase() === nameOrId.toLowerCase());
7437
+ if (byName?.id) return byName.id;
7438
+ return nameOrId;
7439
+ }
7393
7440
  async addLabels(emailId, labels) {
7394
7441
  const gmail = this.ensureConnected();
7395
7442
  await gmail.users.messages.modify({
@@ -7416,6 +7463,74 @@ var init_adapter = __esm({
7416
7463
  messageCount: label.messagesTotal ?? 0
7417
7464
  }));
7418
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
+ }
7419
7534
  buildRfc2822(params) {
7420
7535
  const lines = [];
7421
7536
  lines.push(`From: ${this.email}`);
@@ -7848,6 +7963,41 @@ var init_adapter2 = __esm({
7848
7963
  destinationId
7849
7964
  });
7850
7965
  }
7966
+ async getRawMessage(emailId, _sourceFolder) {
7967
+ this.ensureClient();
7968
+ const res = await fetch(
7969
+ `https://graph.microsoft.com/v1.0/me/messages/${encodeURIComponent(emailId)}/$value`,
7970
+ { headers: { Authorization: `Bearer ${this.accessToken}` } }
7971
+ );
7972
+ if (!res.ok) {
7973
+ throw new Error(`Graph $value fetch failed for ${emailId}: HTTP ${res.status}`);
7974
+ }
7975
+ return Buffer.from(await res.arrayBuffer());
7976
+ }
7977
+ async appendRawMessage(raw, targetFolder, flags) {
7978
+ this.ensureClient();
7979
+ 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";
7980
+ const res = await fetch(endpoint, {
7981
+ method: "POST",
7982
+ headers: {
7983
+ Authorization: `Bearer ${this.accessToken}`,
7984
+ "Content-Type": "text/plain"
7985
+ },
7986
+ body: raw.toString("base64")
7987
+ });
7988
+ if (!res.ok) {
7989
+ const detail = await res.text().catch(() => "");
7990
+ throw new Error(`Graph MIME import failed: HTTP ${res.status} ${detail.slice(0, 200)}`);
7991
+ }
7992
+ const data = await res.json().catch(() => ({}));
7993
+ if (flags && data.id && (flags.read !== void 0 || flags.starred !== void 0)) {
7994
+ try {
7995
+ await this.markEmail(data.id, flags);
7996
+ } catch {
7997
+ }
7998
+ }
7999
+ return { id: data.id || "" };
8000
+ }
7851
8001
  async deleteEmail(emailId, permanent, _sourceFolder) {
7852
8002
  const client = this.ensureClient();
7853
8003
  if (permanent) {
@@ -7977,6 +8127,86 @@ var init_adapter2 = __esm({
7977
8127
  }
7978
8128
  return result;
7979
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
+ }
7980
8210
  async getCategories() {
7981
8211
  const client = this.ensureClient();
7982
8212
  const response = await client.api("/me/outlook/masterCategories").get();
@@ -8040,7 +8270,10 @@ var init_auth2 = __esm({
8040
8270
  DEFAULT_CACHE_FILE = path2.join(DEFAULT_CACHE_DIR, "msal-cache.json");
8041
8271
  OutlookAuth = class _OutlookAuth {
8042
8272
  static AUTHORITY = "https://login.microsoftonline.com/consumers";
8043
- 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"];
8044
8277
  pca;
8045
8278
  cryptoProvider;
8046
8279
  constructor(clientId, cacheFilePath) {
@@ -8663,6 +8896,30 @@ var init_adapter3 = __esm({
8663
8896
  lock.release();
8664
8897
  }
8665
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
+ }
8666
8923
  async deleteEmail(emailId, permanent, sourceFolder) {
8667
8924
  if (!this.client) throw new Error("Not connected");
8668
8925
  const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
@@ -8707,6 +8964,32 @@ var init_adapter3 = __esm({
8707
8964
  lock.release();
8708
8965
  }
8709
8966
  }
8967
+ async getRawMessage(emailId, sourceFolder) {
8968
+ if (!this.client) throw new Error("Not connected");
8969
+ const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
8970
+ let lock;
8971
+ try {
8972
+ lock = await this.client.getMailboxLock(folder);
8973
+ } catch (error48) {
8974
+ throw formatImapError(error48, `Failed to open folder "${folder}"`);
8975
+ }
8976
+ try {
8977
+ const msg = await this.client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
8978
+ if (!msg || !msg.source) throw new Error(`Email ${emailId} not found in "${folder}"`);
8979
+ return msg.source;
8980
+ } finally {
8981
+ lock.release();
8982
+ }
8983
+ }
8984
+ async appendRawMessage(raw, targetFolder, flags) {
8985
+ if (!this.client) throw new Error("Not connected");
8986
+ const folder = targetFolder ? await this.resolveFolder(targetFolder) : "INBOX";
8987
+ const imapFlags = [];
8988
+ if (flags?.read) imapFlags.push("\\Seen");
8989
+ if (flags?.starred) imapFlags.push("\\Flagged");
8990
+ const result = await this.client.append(folder, raw, imapFlags);
8991
+ return { id: String(result?.uid ?? "") };
8992
+ }
8710
8993
  async batchDelete(emailIds, permanent, sourceFolder) {
8711
8994
  if (!this.client) throw new Error("Not connected");
8712
8995
  const result = { succeeded: [], failed: [] };
@@ -33227,6 +33510,66 @@ function registerOrganizingTools(server, accountManager) {
33227
33510
  }
33228
33511
  }
33229
33512
  );
33513
+ server.tool(
33514
+ "email_transfer",
33515
+ "Move or copy emails BETWEEN accounts, preserving the original message intact (sender, date, threading) by transferring the raw MIME. Fetches each message from the source account and imports it into the target account. With deleteAfter=true the source copy is sent to trash only after a confirmed import, making this a safe cross-account move.",
33516
+ {
33517
+ sourceAccountId: external_exports3.string().describe("Account to read messages from"),
33518
+ targetAccountId: external_exports3.string().describe("Account to import messages into"),
33519
+ emailIds: external_exports3.array(external_exports3.string()).min(1).describe("Source email IDs to transfer"),
33520
+ targetFolder: external_exports3.string().optional().describe("Destination folder/label in the target account (Gmail label id or name; IMAP/Outlook folder name). Defaults to INBOX."),
33521
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when emails are not in INBOX)"),
33522
+ deleteAfter: external_exports3.boolean().optional().describe("Send the source copy to trash after a successful import. Default false (copy, leaving the original in place)."),
33523
+ markRead: external_exports3.boolean().optional().describe("Mark imported messages as read in the target. Default: provider default (usually unread).")
33524
+ },
33525
+ async (args) => {
33526
+ try {
33527
+ const source = await accountManager.getProvider(args.sourceAccountId);
33528
+ const target = await accountManager.getProvider(args.targetAccountId);
33529
+ if (!source.getRawMessage) {
33530
+ return jsonResult4({ success: false, error: "Source provider does not support raw message export" });
33531
+ }
33532
+ if (!target.appendRawMessage) {
33533
+ return jsonResult4({ success: false, error: "Target provider does not support raw message import" });
33534
+ }
33535
+ const flags = args.markRead !== void 0 ? { read: args.markRead } : void 0;
33536
+ const transferred = [];
33537
+ const failed = [];
33538
+ const deleted = [];
33539
+ const deleteFailed = [];
33540
+ for (const id of args.emailIds) {
33541
+ let targetId;
33542
+ try {
33543
+ const raw = await source.getRawMessage(id, args.sourceFolder);
33544
+ const res = await target.appendRawMessage(raw, args.targetFolder, flags);
33545
+ targetId = res.id;
33546
+ transferred.push({ sourceId: id, targetId });
33547
+ } catch (e) {
33548
+ failed.push({ id, error: e.message });
33549
+ continue;
33550
+ }
33551
+ if (args.deleteAfter) {
33552
+ try {
33553
+ await source.deleteEmail(id, false, args.sourceFolder);
33554
+ deleted.push(id);
33555
+ } catch (e) {
33556
+ deleteFailed.push({ id, error: e.message });
33557
+ }
33558
+ }
33559
+ }
33560
+ return jsonResult4({
33561
+ success: true,
33562
+ transferred: transferred.length,
33563
+ deleted: deleted.length,
33564
+ ...failed.length ? { failedCount: failed.length } : {},
33565
+ ...deleteFailed.length ? { deleteFailedCount: deleteFailed.length } : {},
33566
+ details: { transferred, failed, deleted, deleteFailed }
33567
+ });
33568
+ } catch (error48) {
33569
+ return jsonResult4({ success: false, error: error48.message });
33570
+ }
33571
+ }
33572
+ );
33230
33573
  server.tool(
33231
33574
  "email_label",
33232
33575
  "Add or remove labels on an email (Gmail only)",
@@ -33324,6 +33667,144 @@ function registerOrganizingTools(server, accountManager) {
33324
33667
  );
33325
33668
  }
33326
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
+
33327
33808
  // src/server.ts
33328
33809
  async function createServer(accountManager) {
33329
33810
  const mgr = accountManager ?? new AccountManager();
@@ -33335,6 +33816,7 @@ async function createServer(accountManager) {
33335
33816
  registerReadingTools(server, mgr);
33336
33817
  registerSendingTools(server, mgr);
33337
33818
  registerOrganizingTools(server, mgr);
33819
+ registerModerationTools(server, mgr);
33338
33820
  return { server, accountManager: mgr };
33339
33821
  }
33340
33822
  async function startServer() {