@marlinjai/email-mcp 1.5.0 → 1.7.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 +13 -4
- package/dist/index.js +207 -21
- package/dist/index.js.map +2 -2
- package/dist/setup/wizard.js +142 -21
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,11 +62,19 @@ No configuration needed — the setup wizard handles everything using built-in O
|
|
|
62
62
|
```bash
|
|
63
63
|
npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
|
|
64
64
|
# Select "Gmail" when prompted
|
|
65
|
+
# Choose "Full" or "Restricted" permission scope when asked
|
|
65
66
|
# A browser window opens for Google authorization
|
|
66
67
|
# Grant the requested permissions and return to the terminal
|
|
67
68
|
```
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
The wizard asks which Gmail permission scope to authorize:
|
|
71
|
+
|
|
72
|
+
- **Full** (default) — everything below, plus immediate, Trash-bypassing permanent deletion (`https://mail.google.com/`, Gmail's maximum-permission scope).
|
|
73
|
+
- **Restricted** — read, send, label, archive, and move-to-trash (`gmail.modify` + `gmail.settings.basic`), but no permanent deletion. Every tool in this server works identically under Restricted except an explicit `permanent: true` delete, which fails with a Gmail API error instead of succeeding.
|
|
74
|
+
|
|
75
|
+
Pass `--scope full` or `--scope restricted` to skip the prompt, or set `EMAIL_MCP_GMAIL_SCOPE=restricted` in the environment the wizard runs in.
|
|
76
|
+
|
|
77
|
+
> **Note:** If you prefer to use your own OAuth app instead of the shared one this package ships with, create a Desktop OAuth 2.0 Client in the [Google Cloud Console](https://console.cloud.google.com/) with the Gmail API enabled, then set `EMAIL_MCP_GMAIL_CLIENT_ID` and `EMAIL_MCP_GMAIL_CLIENT_SECRET` in the environment before running the setup wizard (and in the MCP server's environment, since re-authentication uses the same variables). This gives you your own token lifecycle, independent of the publisher's Cloud project, and sidesteps Google's unverified-app warning and 100-test-user cap for your own account(s) once you add yourself as a test user on your own app.
|
|
70
78
|
|
|
71
79
|
### Outlook
|
|
72
80
|
|
|
@@ -79,7 +87,7 @@ npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
|
|
|
79
87
|
# Sign in and grant the requested permissions
|
|
80
88
|
```
|
|
81
89
|
|
|
82
|
-
> **Note:** If you prefer to use your own OAuth app, register one in the [Azure Portal](https://portal.azure.com/) with `Mail.ReadWrite`, `Mail.Send`, `MailboxSettings.ReadWrite` (needed for `email_create_block_rule`), and `offline_access` permissions.
|
|
90
|
+
> **Note:** If you prefer to use your own OAuth app, register one in the [Azure Portal](https://portal.azure.com/) with `Mail.ReadWrite`, `Mail.Send`, `MailboxSettings.ReadWrite` (needed for `email_create_block_rule`), and `offline_access` permissions, then set `EMAIL_MCP_OUTLOOK_CLIENT_ID` in the environment before running the setup wizard.
|
|
83
91
|
|
|
84
92
|
### iCloud
|
|
85
93
|
|
|
@@ -105,7 +113,7 @@ npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
|
|
|
105
113
|
# Optionally enter SMTP host and port for sending
|
|
106
114
|
```
|
|
107
115
|
|
|
108
|
-
## Available Tools (
|
|
116
|
+
## Available Tools (32)
|
|
109
117
|
|
|
110
118
|
### Account Management (4)
|
|
111
119
|
|
|
@@ -127,7 +135,7 @@ npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
|
|
|
127
135
|
| `email_get_attachment` | Download a specific attachment by ID (returns base64 data) |
|
|
128
136
|
| `email_save_attachment` | Download an attachment directly to disk, returning metadata only — avoids the token cost of round-tripping large files as base64. `outputPath` is relative to a fixed downloads directory (`~/.email-mcp/downloads`, override with `EMAIL_MCP_DOWNLOADS_DIR`) and cannot escape it |
|
|
129
137
|
|
|
130
|
-
### Sending & Drafts (
|
|
138
|
+
### Sending & Drafts (6)
|
|
131
139
|
|
|
132
140
|
| Tool | Description |
|
|
133
141
|
|------|-------------|
|
|
@@ -135,6 +143,7 @@ npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
|
|
|
135
143
|
| `email_reply` | Reply to an email (supports reply-all, preserves threading) |
|
|
136
144
|
| `email_forward` | Forward an email to new recipients |
|
|
137
145
|
| `email_draft_create` | Save a draft without sending |
|
|
146
|
+
| `email_draft_update` | Update an existing draft in place. On Gmail/Outlook the draft id is unchanged; on iCloud/generic IMAP there's no in-place update (IMAP messages are immutable), so the old draft is deleted and a new one appended — the returned id is a **new** id, always use it going forward |
|
|
138
147
|
| `email_draft_list` | List all drafts |
|
|
139
148
|
|
|
140
149
|
### Organization (8)
|
package/dist/index.js
CHANGED
|
@@ -7182,11 +7182,12 @@ var init_adapter = __esm({
|
|
|
7182
7182
|
const labels = res.data.labels || [];
|
|
7183
7183
|
return labels.map(mapGmailLabel);
|
|
7184
7184
|
}
|
|
7185
|
-
async createFolder(name) {
|
|
7185
|
+
async createFolder(name, parentPath) {
|
|
7186
7186
|
const gmail = this.ensureConnected();
|
|
7187
|
+
const fullName = parentPath ? `${parentPath}/${name}` : name;
|
|
7187
7188
|
const res = await gmail.users.labels.create({
|
|
7188
7189
|
userId: "me",
|
|
7189
|
-
requestBody: { name }
|
|
7190
|
+
requestBody: { name: fullName }
|
|
7190
7191
|
});
|
|
7191
7192
|
return mapGmailLabel(res.data);
|
|
7192
7193
|
}
|
|
@@ -7303,6 +7304,18 @@ var init_adapter = __esm({
|
|
|
7303
7304
|
});
|
|
7304
7305
|
return { id: res.data.id || "" };
|
|
7305
7306
|
}
|
|
7307
|
+
async updateDraft(draftId, params) {
|
|
7308
|
+
const gmail = this.ensureConnected();
|
|
7309
|
+
const raw = this.buildRfc2822(params);
|
|
7310
|
+
const res = await gmail.users.drafts.update({
|
|
7311
|
+
userId: "me",
|
|
7312
|
+
id: draftId,
|
|
7313
|
+
requestBody: {
|
|
7314
|
+
message: { raw }
|
|
7315
|
+
}
|
|
7316
|
+
});
|
|
7317
|
+
return { id: res.data.id || draftId };
|
|
7318
|
+
}
|
|
7306
7319
|
async listDrafts(limit, _offset) {
|
|
7307
7320
|
const gmail = this.ensureConnected();
|
|
7308
7321
|
const res = await gmail.users.drafts.list({
|
|
@@ -7466,6 +7479,34 @@ var init_adapter = __esm({
|
|
|
7466
7479
|
}
|
|
7467
7480
|
return result;
|
|
7468
7481
|
}
|
|
7482
|
+
async batchLabel(emailIds, addLabels, removeLabels) {
|
|
7483
|
+
const gmail = this.ensureConnected();
|
|
7484
|
+
const result = { succeeded: [], failed: [] };
|
|
7485
|
+
const addLabelIds = addLabels ?? [];
|
|
7486
|
+
const removeLabelIds = removeLabels ?? [];
|
|
7487
|
+
if (addLabelIds.length === 0 && removeLabelIds.length === 0) {
|
|
7488
|
+
result.succeeded = [...emailIds];
|
|
7489
|
+
return result;
|
|
7490
|
+
}
|
|
7491
|
+
try {
|
|
7492
|
+
await gmail.users.messages.batchModify({
|
|
7493
|
+
userId: "me",
|
|
7494
|
+
requestBody: { ids: emailIds, addLabelIds, removeLabelIds }
|
|
7495
|
+
});
|
|
7496
|
+
result.succeeded = [...emailIds];
|
|
7497
|
+
} catch (error48) {
|
|
7498
|
+
for (const id of emailIds) {
|
|
7499
|
+
try {
|
|
7500
|
+
if (addLabelIds.length > 0) await this.addLabels(id, addLabelIds);
|
|
7501
|
+
if (removeLabelIds.length > 0) await this.removeLabels(id, removeLabelIds);
|
|
7502
|
+
result.succeeded.push(id);
|
|
7503
|
+
} catch (e) {
|
|
7504
|
+
result.failed.push({ id, error: e.message });
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
}
|
|
7508
|
+
return result;
|
|
7509
|
+
}
|
|
7469
7510
|
async getRawMessage(emailId, _sourceFolder) {
|
|
7470
7511
|
const gmail = this.ensureConnected();
|
|
7471
7512
|
const res = await gmail.users.messages.get({
|
|
@@ -7626,18 +7667,37 @@ var init_adapter = __esm({
|
|
|
7626
7667
|
// src/providers/gmail/auth.ts
|
|
7627
7668
|
import { OAuth2Client as OAuth2Client2 } from "google-auth-library";
|
|
7628
7669
|
import crypto2 from "node:crypto";
|
|
7670
|
+
function resolveScopeMode(explicit) {
|
|
7671
|
+
if (explicit) return explicit;
|
|
7672
|
+
return process.env.EMAIL_MCP_GMAIL_SCOPE?.toLowerCase() === "restricted" ? "restricted" : "full";
|
|
7673
|
+
}
|
|
7629
7674
|
var GMAIL_SCOPES, GmailAuth;
|
|
7630
7675
|
var init_auth = __esm({
|
|
7631
7676
|
"src/providers/gmail/auth.ts"() {
|
|
7632
7677
|
"use strict";
|
|
7633
|
-
GMAIL_SCOPES =
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7678
|
+
GMAIL_SCOPES = {
|
|
7679
|
+
full: [
|
|
7680
|
+
// Gmail's maximum-permission scope. Superset of gmail.modify below;
|
|
7681
|
+
// the only capability it adds is immediate, trash-bypassing permanent
|
|
7682
|
+
// deletion (messages.delete / batchDelete with permanent: true).
|
|
7683
|
+
"https://mail.google.com/",
|
|
7684
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
7685
|
+
// Mailbox content access (above) does NOT cover the Settings API —
|
|
7686
|
+
// Google treats them as separate permission domains. Needed for
|
|
7687
|
+
// users.settings.filters.* (email_create_block_rule/list/delete).
|
|
7688
|
+
"https://www.googleapis.com/auth/gmail.settings.basic"
|
|
7689
|
+
],
|
|
7690
|
+
// Drops https://mail.google.com/ so an AI-triggered permanent delete
|
|
7691
|
+
// can't happen: gmail.modify already covers every other read/write
|
|
7692
|
+
// operation this server performs (read, send, label, archive, trash,
|
|
7693
|
+
// drafts, filters). Choosing this mode means deleteEmail(..., permanent:
|
|
7694
|
+
// true) and batchDelete(..., permanent: true) will fail with a Gmail API
|
|
7695
|
+
// 403 instead of succeeding — everything else behaves identically.
|
|
7696
|
+
restricted: [
|
|
7697
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
7698
|
+
"https://www.googleapis.com/auth/gmail.settings.basic"
|
|
7699
|
+
]
|
|
7700
|
+
};
|
|
7641
7701
|
GmailAuth = class {
|
|
7642
7702
|
clientId;
|
|
7643
7703
|
clientSecret;
|
|
@@ -7647,13 +7707,13 @@ var init_auth = __esm({
|
|
|
7647
7707
|
this.clientSecret = clientSecret;
|
|
7648
7708
|
this.oauth2Client = new OAuth2Client2(clientId, clientSecret, redirectUri);
|
|
7649
7709
|
}
|
|
7650
|
-
getAuthUrl(redirectUri) {
|
|
7710
|
+
getAuthUrl(redirectUri, scopeMode) {
|
|
7651
7711
|
const codeVerifier = crypto2.randomBytes(32).toString("base64url");
|
|
7652
7712
|
const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
7653
7713
|
this.oauth2Client = new OAuth2Client2(this.clientId, this.clientSecret, redirectUri);
|
|
7654
7714
|
const url2 = this.oauth2Client.generateAuthUrl({
|
|
7655
7715
|
access_type: "offline",
|
|
7656
|
-
scope: GMAIL_SCOPES,
|
|
7716
|
+
scope: GMAIL_SCOPES[resolveScopeMode(scopeMode)],
|
|
7657
7717
|
prompt: "consent",
|
|
7658
7718
|
code_challenge: codeChallenge,
|
|
7659
7719
|
code_challenge_method: "S256"
|
|
@@ -8010,6 +8070,12 @@ var init_adapter2 = __esm({
|
|
|
8010
8070
|
const result = await client.api("/me/messages").post(message);
|
|
8011
8071
|
return { id: result.id };
|
|
8012
8072
|
}
|
|
8073
|
+
async updateDraft(draftId, params) {
|
|
8074
|
+
const client = this.ensureClient();
|
|
8075
|
+
const message = this.buildGraphMessage(params);
|
|
8076
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(message);
|
|
8077
|
+
return { id: draftId };
|
|
8078
|
+
}
|
|
8013
8079
|
async listDrafts(limit, offset) {
|
|
8014
8080
|
const client = this.ensureClient();
|
|
8015
8081
|
let request = client.api("/me/mailFolders/drafts/messages");
|
|
@@ -8992,8 +9058,7 @@ var init_adapter3 = __esm({
|
|
|
8992
9058
|
const messageId = await sendViaSmtp(transport, this.email, params);
|
|
8993
9059
|
return { id: messageId };
|
|
8994
9060
|
}
|
|
8995
|
-
|
|
8996
|
-
const client = await this.ensureConnected();
|
|
9061
|
+
buildDraftRfc2822(params) {
|
|
8997
9062
|
const lines = [];
|
|
8998
9063
|
lines.push(`From: ${this.email}`);
|
|
8999
9064
|
lines.push(`To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`);
|
|
@@ -9008,12 +9073,41 @@ var init_adapter3 = __esm({
|
|
|
9008
9073
|
lines.push("Content-Type: text/plain; charset=utf-8");
|
|
9009
9074
|
lines.push("");
|
|
9010
9075
|
lines.push(params.body.text || "");
|
|
9011
|
-
|
|
9076
|
+
return lines.join("\r\n");
|
|
9077
|
+
}
|
|
9078
|
+
async createDraft(params) {
|
|
9079
|
+
const client = await this.ensureConnected();
|
|
9080
|
+
const rawMessage = this.buildDraftRfc2822(params);
|
|
9012
9081
|
const draftsFolder = await this.resolveFolder("Drafts");
|
|
9013
9082
|
const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
|
|
9014
9083
|
if (!result) throw new Error("Failed to append draft");
|
|
9015
9084
|
return { id: String(result.uid || result) };
|
|
9016
9085
|
}
|
|
9086
|
+
/**
|
|
9087
|
+
* IMAP has no in-place message update — a draft is just a message in the
|
|
9088
|
+
* Drafts folder, and IMAP messages are immutable once appended. This
|
|
9089
|
+
* deletes the old revision and appends a new one, so the returned `id`
|
|
9090
|
+
* is a NEW uid; the old id no longer resolves after this call.
|
|
9091
|
+
*/
|
|
9092
|
+
async updateDraft(draftId, params, sourceFolder) {
|
|
9093
|
+
const client = await this.ensureConnected();
|
|
9094
|
+
const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
|
|
9095
|
+
let lock;
|
|
9096
|
+
try {
|
|
9097
|
+
lock = await client.getMailboxLock(draftsFolder);
|
|
9098
|
+
} catch (error48) {
|
|
9099
|
+
throw formatImapError(error48, `Failed to open folder "${draftsFolder}"`);
|
|
9100
|
+
}
|
|
9101
|
+
try {
|
|
9102
|
+
await client.messageDelete(draftId, { uid: true });
|
|
9103
|
+
} finally {
|
|
9104
|
+
lock.release();
|
|
9105
|
+
}
|
|
9106
|
+
const rawMessage = this.buildDraftRfc2822(params);
|
|
9107
|
+
const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
|
|
9108
|
+
if (!result) throw new Error("Failed to append updated draft");
|
|
9109
|
+
return { id: String(result.uid || result) };
|
|
9110
|
+
}
|
|
9017
9111
|
async listDrafts(limit, offset) {
|
|
9018
9112
|
return this.search({
|
|
9019
9113
|
folder: "Drafts",
|
|
@@ -9269,9 +9363,9 @@ var GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, OUTLOOK_CLIENT_ID;
|
|
|
9269
9363
|
var init_oauth_config = __esm({
|
|
9270
9364
|
"src/oauth-config.ts"() {
|
|
9271
9365
|
"use strict";
|
|
9272
|
-
GMAIL_CLIENT_ID = "1062266096576-lu2ukplhc3lcm0bvheml5i18urptdp4t.apps.googleusercontent.com";
|
|
9273
|
-
GMAIL_CLIENT_SECRET = "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";
|
|
9274
|
-
OUTLOOK_CLIENT_ID = "76d32225-ef10-4b49-951c-c80ae906595d";
|
|
9366
|
+
GMAIL_CLIENT_ID = process.env.EMAIL_MCP_GMAIL_CLIENT_ID || "1062266096576-lu2ukplhc3lcm0bvheml5i18urptdp4t.apps.googleusercontent.com";
|
|
9367
|
+
GMAIL_CLIENT_SECRET = process.env.EMAIL_MCP_GMAIL_CLIENT_SECRET || "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";
|
|
9368
|
+
OUTLOOK_CLIENT_ID = process.env.EMAIL_MCP_OUTLOOK_CLIENT_ID || "76d32225-ef10-4b49-951c-c80ae906595d";
|
|
9275
9369
|
}
|
|
9276
9370
|
});
|
|
9277
9371
|
|
|
@@ -9677,14 +9771,30 @@ async function testAndSave(creds) {
|
|
|
9677
9771
|
async function promptAccountName(defaultName) {
|
|
9678
9772
|
return askText("Account name:", defaultName);
|
|
9679
9773
|
}
|
|
9680
|
-
async function setupGmail() {
|
|
9774
|
+
async function setupGmail(scopeModeFlag) {
|
|
9681
9775
|
console.log("\n--- Gmail Setup ---");
|
|
9776
|
+
if (process.env.EMAIL_MCP_GMAIL_CLIENT_ID) {
|
|
9777
|
+
console.log("Using your own Google OAuth Client ID (EMAIL_MCP_GMAIL_CLIENT_ID).");
|
|
9778
|
+
}
|
|
9682
9779
|
console.log("A browser window will open for Google authorization.\n");
|
|
9780
|
+
let scopeMode = scopeModeFlag;
|
|
9781
|
+
if (!scopeMode) {
|
|
9782
|
+
scopeMode = await askChoice("Gmail permission scope:", [
|
|
9783
|
+
{
|
|
9784
|
+
name: "Full (default) \u2014 includes permanent delete, bypassing Trash",
|
|
9785
|
+
value: "full"
|
|
9786
|
+
},
|
|
9787
|
+
{
|
|
9788
|
+
name: "Restricted \u2014 read/send/label/archive/trash only, no permanent delete",
|
|
9789
|
+
value: "restricted"
|
|
9790
|
+
}
|
|
9791
|
+
]);
|
|
9792
|
+
}
|
|
9683
9793
|
const server = new OAuthCallbackServer();
|
|
9684
9794
|
const port = await server.start();
|
|
9685
9795
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
9686
9796
|
const gmailAuth = new GmailAuth(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, redirectUri);
|
|
9687
|
-
const { url: url2, codeVerifier } = gmailAuth.getAuthUrl(redirectUri);
|
|
9797
|
+
const { url: url2, codeVerifier } = gmailAuth.getAuthUrl(redirectUri, scopeMode);
|
|
9688
9798
|
console.log("\nOpening browser for Google authorization...");
|
|
9689
9799
|
console.log(`If the browser does not open, visit:
|
|
9690
9800
|
${url2}
|
|
@@ -9883,6 +9993,17 @@ async function main() {
|
|
|
9883
9993
|
await removeAccount(id);
|
|
9884
9994
|
return;
|
|
9885
9995
|
}
|
|
9996
|
+
const scopeIdx = args.indexOf("--scope");
|
|
9997
|
+
let scopeModeFlag;
|
|
9998
|
+
if (scopeIdx !== -1) {
|
|
9999
|
+
const value = args[scopeIdx + 1];
|
|
10000
|
+
if (value === "full" || value === "restricted") {
|
|
10001
|
+
scopeModeFlag = value;
|
|
10002
|
+
} else {
|
|
10003
|
+
console.error("Usage: --scope <full|restricted>");
|
|
10004
|
+
process.exit(1);
|
|
10005
|
+
}
|
|
10006
|
+
}
|
|
9886
10007
|
console.log("@marlinjai/email-mcp Account Setup\n");
|
|
9887
10008
|
let addMore = true;
|
|
9888
10009
|
while (addMore) {
|
|
@@ -9894,7 +10015,7 @@ async function main() {
|
|
|
9894
10015
|
]);
|
|
9895
10016
|
switch (provider) {
|
|
9896
10017
|
case ProviderType.Gmail:
|
|
9897
|
-
await setupGmail();
|
|
10018
|
+
await setupGmail(scopeModeFlag);
|
|
9898
10019
|
break;
|
|
9899
10020
|
case ProviderType.Outlook:
|
|
9900
10021
|
await setupOutlook();
|
|
@@ -33538,6 +33659,32 @@ function registerSendingTools(server, accountManager) {
|
|
|
33538
33659
|
}
|
|
33539
33660
|
}
|
|
33540
33661
|
);
|
|
33662
|
+
server.tool(
|
|
33663
|
+
"email_draft_update",
|
|
33664
|
+
"Update an existing email draft in place (to/subject/body). On Gmail and Outlook the draft id is unchanged. On iCloud/generic IMAP there is no in-place update \u2014 the old draft is deleted and a new one appended \u2014 so the returned id is a NEW id and the old one no longer resolves; always use the id this call returns, not the one you passed in.",
|
|
33665
|
+
{
|
|
33666
|
+
accountId: external_exports3.string(),
|
|
33667
|
+
draftId: external_exports3.string(),
|
|
33668
|
+
to: external_exports3.array(ContactSchema),
|
|
33669
|
+
subject: external_exports3.string(),
|
|
33670
|
+
body: BodySchema,
|
|
33671
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when the draft is not in the default Drafts folder)")
|
|
33672
|
+
},
|
|
33673
|
+
async (args) => {
|
|
33674
|
+
try {
|
|
33675
|
+
const provider = await accountManager.getProvider(args.accountId);
|
|
33676
|
+
const params = {
|
|
33677
|
+
to: args.to,
|
|
33678
|
+
subject: args.subject,
|
|
33679
|
+
body: args.body
|
|
33680
|
+
};
|
|
33681
|
+
const result = await provider.updateDraft(args.draftId, params, args.sourceFolder);
|
|
33682
|
+
return jsonResult3(result);
|
|
33683
|
+
} catch (error48) {
|
|
33684
|
+
return jsonResult3({ error: error48.message });
|
|
33685
|
+
}
|
|
33686
|
+
}
|
|
33687
|
+
);
|
|
33541
33688
|
server.tool(
|
|
33542
33689
|
"email_draft_list",
|
|
33543
33690
|
"List email drafts",
|
|
@@ -33725,6 +33872,45 @@ function registerOrganizingTools(server, accountManager) {
|
|
|
33725
33872
|
}
|
|
33726
33873
|
}
|
|
33727
33874
|
);
|
|
33875
|
+
server.tool(
|
|
33876
|
+
"email_batch_label",
|
|
33877
|
+
"Add or remove labels on multiple emails at once (Gmail only). Much faster than individual email_label calls.",
|
|
33878
|
+
{
|
|
33879
|
+
accountId: external_exports3.string(),
|
|
33880
|
+
emailIds: external_exports3.array(external_exports3.string()).min(1).describe("Array of email IDs to label"),
|
|
33881
|
+
addLabels: external_exports3.array(external_exports3.string()).optional(),
|
|
33882
|
+
removeLabels: external_exports3.array(external_exports3.string()).optional()
|
|
33883
|
+
},
|
|
33884
|
+
async (args) => {
|
|
33885
|
+
try {
|
|
33886
|
+
const provider = await accountManager.getProvider(args.accountId);
|
|
33887
|
+
if (!provider.addLabels || !provider.removeLabels) {
|
|
33888
|
+
return jsonResult4({
|
|
33889
|
+
success: false,
|
|
33890
|
+
error: "email_batch_label is only supported on Gmail accounts",
|
|
33891
|
+
supportedProviders: ["gmail"]
|
|
33892
|
+
});
|
|
33893
|
+
}
|
|
33894
|
+
if (provider.batchLabel) {
|
|
33895
|
+
const result2 = await provider.batchLabel(args.emailIds, args.addLabels, args.removeLabels);
|
|
33896
|
+
return jsonResult4({ success: true, ...result2 });
|
|
33897
|
+
}
|
|
33898
|
+
const result = { succeeded: [], failed: [] };
|
|
33899
|
+
for (const id of args.emailIds) {
|
|
33900
|
+
try {
|
|
33901
|
+
if (args.addLabels && args.addLabels.length > 0) await provider.addLabels(id, args.addLabels);
|
|
33902
|
+
if (args.removeLabels && args.removeLabels.length > 0) await provider.removeLabels(id, args.removeLabels);
|
|
33903
|
+
result.succeeded.push(id);
|
|
33904
|
+
} catch (e) {
|
|
33905
|
+
result.failed.push({ id, error: e.message });
|
|
33906
|
+
}
|
|
33907
|
+
}
|
|
33908
|
+
return jsonResult4({ success: true, ...result });
|
|
33909
|
+
} catch (error48) {
|
|
33910
|
+
return jsonResult4({ success: false, error: error48.message });
|
|
33911
|
+
}
|
|
33912
|
+
}
|
|
33913
|
+
);
|
|
33728
33914
|
server.tool(
|
|
33729
33915
|
"email_transfer",
|
|
33730
33916
|
"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.",
|