@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/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 (
|
|
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 (
|
|
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
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
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
|
|
7155
|
+
for (const id of windowIds) {
|
|
7143
7156
|
const full = await gmail.users.messages.get({
|
|
7144
7157
|
userId: "me",
|
|
7145
|
-
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
|
-
|
|
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() {
|