@marlinjai/email-mcp 1.5.0 → 1.7.1
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 -4
- package/dist/index.js +240 -37
- package/dist/index.js.map +3 -3
- package/dist/setup/wizard.js +173 -35
- package/dist/setup/wizard.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @marlinjai/email-mcp
|
|
2
2
|
|
|
3
|
+
<p align="center"><img src="assets/icon-512.png" width="120" height="120" alt="email-mcp logo"></p>
|
|
4
|
+
|
|
3
5
|
A unified MCP server for email access across Gmail, Outlook, iCloud, and generic IMAP providers.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
@@ -62,11 +64,19 @@ No configuration needed — the setup wizard handles everything using built-in O
|
|
|
62
64
|
```bash
|
|
63
65
|
npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
|
|
64
66
|
# Select "Gmail" when prompted
|
|
67
|
+
# Choose "Full" or "Restricted" permission scope when asked
|
|
65
68
|
# A browser window opens for Google authorization
|
|
66
69
|
# Grant the requested permissions and return to the terminal
|
|
67
70
|
```
|
|
68
71
|
|
|
69
|
-
|
|
72
|
+
The wizard asks which Gmail permission scope to authorize:
|
|
73
|
+
|
|
74
|
+
- **Full** (default) — everything below, plus immediate, Trash-bypassing permanent deletion (`https://mail.google.com/`, Gmail's maximum-permission scope).
|
|
75
|
+
- **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.
|
|
76
|
+
|
|
77
|
+
Pass `--scope full` or `--scope restricted` to skip the prompt, or set `EMAIL_MCP_GMAIL_SCOPE=restricted` in the environment the wizard runs in.
|
|
78
|
+
|
|
79
|
+
> **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
80
|
|
|
71
81
|
### Outlook
|
|
72
82
|
|
|
@@ -79,7 +89,7 @@ npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
|
|
|
79
89
|
# Sign in and grant the requested permissions
|
|
80
90
|
```
|
|
81
91
|
|
|
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.
|
|
92
|
+
> **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
93
|
|
|
84
94
|
### iCloud
|
|
85
95
|
|
|
@@ -105,7 +115,7 @@ npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
|
|
|
105
115
|
# Optionally enter SMTP host and port for sending
|
|
106
116
|
```
|
|
107
117
|
|
|
108
|
-
## Available Tools (
|
|
118
|
+
## Available Tools (32)
|
|
109
119
|
|
|
110
120
|
### Account Management (4)
|
|
111
121
|
|
|
@@ -127,7 +137,7 @@ npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
|
|
|
127
137
|
| `email_get_attachment` | Download a specific attachment by ID (returns base64 data) |
|
|
128
138
|
| `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
139
|
|
|
130
|
-
### Sending & Drafts (
|
|
140
|
+
### Sending & Drafts (6)
|
|
131
141
|
|
|
132
142
|
| Tool | Description |
|
|
133
143
|
|------|-------------|
|
|
@@ -135,6 +145,7 @@ npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
|
|
|
135
145
|
| `email_reply` | Reply to an email (supports reply-all, preserves threading) |
|
|
136
146
|
| `email_forward` | Forward an email to new recipients |
|
|
137
147
|
| `email_draft_create` | Save a draft without sending |
|
|
148
|
+
| `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
149
|
| `email_draft_list` | List all drafts |
|
|
139
150
|
|
|
140
151
|
### 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,18 +7707,20 @@ 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");
|
|
7713
|
+
const state = crypto2.randomBytes(16).toString("base64url");
|
|
7653
7714
|
this.oauth2Client = new OAuth2Client2(this.clientId, this.clientSecret, redirectUri);
|
|
7654
7715
|
const url2 = this.oauth2Client.generateAuthUrl({
|
|
7655
7716
|
access_type: "offline",
|
|
7656
|
-
scope: GMAIL_SCOPES,
|
|
7717
|
+
scope: GMAIL_SCOPES[resolveScopeMode(scopeMode)],
|
|
7657
7718
|
prompt: "consent",
|
|
7658
7719
|
code_challenge: codeChallenge,
|
|
7659
|
-
code_challenge_method: "S256"
|
|
7720
|
+
code_challenge_method: "S256",
|
|
7721
|
+
state
|
|
7660
7722
|
});
|
|
7661
|
-
return { url: url2, codeVerifier };
|
|
7723
|
+
return { url: url2, codeVerifier, state };
|
|
7662
7724
|
}
|
|
7663
7725
|
async exchangeCode(code, codeVerifier) {
|
|
7664
7726
|
const { tokens } = await this.oauth2Client.getToken({ code, codeVerifier });
|
|
@@ -8010,6 +8072,12 @@ var init_adapter2 = __esm({
|
|
|
8010
8072
|
const result = await client.api("/me/messages").post(message);
|
|
8011
8073
|
return { id: result.id };
|
|
8012
8074
|
}
|
|
8075
|
+
async updateDraft(draftId, params) {
|
|
8076
|
+
const client = this.ensureClient();
|
|
8077
|
+
const message = this.buildGraphMessage(params);
|
|
8078
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(message);
|
|
8079
|
+
return { id: draftId };
|
|
8080
|
+
}
|
|
8013
8081
|
async listDrafts(limit, offset) {
|
|
8014
8082
|
const client = this.ensureClient();
|
|
8015
8083
|
let request = client.api("/me/mailFolders/drafts/messages");
|
|
@@ -8303,6 +8371,7 @@ var init_adapter2 = __esm({
|
|
|
8303
8371
|
});
|
|
8304
8372
|
|
|
8305
8373
|
// src/providers/outlook/auth.ts
|
|
8374
|
+
import crypto3 from "node:crypto";
|
|
8306
8375
|
import fs2 from "node:fs";
|
|
8307
8376
|
import path2 from "node:path";
|
|
8308
8377
|
import os2 from "node:os";
|
|
@@ -8357,13 +8426,15 @@ var init_auth2 = __esm({
|
|
|
8357
8426
|
}
|
|
8358
8427
|
async getAuthUrl(redirectUri) {
|
|
8359
8428
|
const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
|
|
8429
|
+
const state = crypto3.randomBytes(16).toString("base64url");
|
|
8360
8430
|
const url2 = await this.pca.getAuthCodeUrl({
|
|
8361
8431
|
scopes: _OutlookAuth.SCOPES,
|
|
8362
8432
|
redirectUri,
|
|
8363
8433
|
codeChallenge: challenge,
|
|
8364
|
-
codeChallengeMethod: "S256"
|
|
8434
|
+
codeChallengeMethod: "S256",
|
|
8435
|
+
state
|
|
8365
8436
|
});
|
|
8366
|
-
return { url: url2, codeVerifier: verifier };
|
|
8437
|
+
return { url: url2, codeVerifier: verifier, state };
|
|
8367
8438
|
}
|
|
8368
8439
|
async exchangeCode(code, codeVerifier, redirectUri) {
|
|
8369
8440
|
const result = await this.pca.acquireTokenByCode({
|
|
@@ -8992,8 +9063,7 @@ var init_adapter3 = __esm({
|
|
|
8992
9063
|
const messageId = await sendViaSmtp(transport, this.email, params);
|
|
8993
9064
|
return { id: messageId };
|
|
8994
9065
|
}
|
|
8995
|
-
|
|
8996
|
-
const client = await this.ensureConnected();
|
|
9066
|
+
buildDraftRfc2822(params) {
|
|
8997
9067
|
const lines = [];
|
|
8998
9068
|
lines.push(`From: ${this.email}`);
|
|
8999
9069
|
lines.push(`To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`);
|
|
@@ -9008,12 +9078,41 @@ var init_adapter3 = __esm({
|
|
|
9008
9078
|
lines.push("Content-Type: text/plain; charset=utf-8");
|
|
9009
9079
|
lines.push("");
|
|
9010
9080
|
lines.push(params.body.text || "");
|
|
9011
|
-
|
|
9081
|
+
return lines.join("\r\n");
|
|
9082
|
+
}
|
|
9083
|
+
async createDraft(params) {
|
|
9084
|
+
const client = await this.ensureConnected();
|
|
9085
|
+
const rawMessage = this.buildDraftRfc2822(params);
|
|
9012
9086
|
const draftsFolder = await this.resolveFolder("Drafts");
|
|
9013
9087
|
const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
|
|
9014
9088
|
if (!result) throw new Error("Failed to append draft");
|
|
9015
9089
|
return { id: String(result.uid || result) };
|
|
9016
9090
|
}
|
|
9091
|
+
/**
|
|
9092
|
+
* IMAP has no in-place message update — a draft is just a message in the
|
|
9093
|
+
* Drafts folder, and IMAP messages are immutable once appended. This
|
|
9094
|
+
* deletes the old revision and appends a new one, so the returned `id`
|
|
9095
|
+
* is a NEW uid; the old id no longer resolves after this call.
|
|
9096
|
+
*/
|
|
9097
|
+
async updateDraft(draftId, params, sourceFolder) {
|
|
9098
|
+
const client = await this.ensureConnected();
|
|
9099
|
+
const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
|
|
9100
|
+
let lock;
|
|
9101
|
+
try {
|
|
9102
|
+
lock = await client.getMailboxLock(draftsFolder);
|
|
9103
|
+
} catch (error48) {
|
|
9104
|
+
throw formatImapError(error48, `Failed to open folder "${draftsFolder}"`);
|
|
9105
|
+
}
|
|
9106
|
+
try {
|
|
9107
|
+
await client.messageDelete(draftId, { uid: true });
|
|
9108
|
+
} finally {
|
|
9109
|
+
lock.release();
|
|
9110
|
+
}
|
|
9111
|
+
const rawMessage = this.buildDraftRfc2822(params);
|
|
9112
|
+
const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
|
|
9113
|
+
if (!result) throw new Error("Failed to append updated draft");
|
|
9114
|
+
return { id: String(result.uid || result) };
|
|
9115
|
+
}
|
|
9017
9116
|
async listDrafts(limit, offset) {
|
|
9018
9117
|
return this.search({
|
|
9019
9118
|
folder: "Drafts",
|
|
@@ -9269,9 +9368,9 @@ var GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, OUTLOOK_CLIENT_ID;
|
|
|
9269
9368
|
var init_oauth_config = __esm({
|
|
9270
9369
|
"src/oauth-config.ts"() {
|
|
9271
9370
|
"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";
|
|
9371
|
+
GMAIL_CLIENT_ID = process.env.EMAIL_MCP_GMAIL_CLIENT_ID || "1062266096576-lu2ukplhc3lcm0bvheml5i18urptdp4t.apps.googleusercontent.com";
|
|
9372
|
+
GMAIL_CLIENT_SECRET = process.env.EMAIL_MCP_GMAIL_CLIENT_SECRET || "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";
|
|
9373
|
+
OUTLOOK_CLIENT_ID = process.env.EMAIL_MCP_OUTLOOK_CLIENT_ID || "76d32225-ef10-4b49-951c-c80ae906595d";
|
|
9275
9374
|
}
|
|
9276
9375
|
});
|
|
9277
9376
|
|
|
@@ -9566,6 +9665,7 @@ var init_oauth_server = __esm({
|
|
|
9566
9665
|
rejectCode = null;
|
|
9567
9666
|
timeoutMs;
|
|
9568
9667
|
timeoutHandle = null;
|
|
9668
|
+
expectedState = null;
|
|
9569
9669
|
constructor(timeoutMs = 12e4) {
|
|
9570
9670
|
this.timeoutMs = timeoutMs;
|
|
9571
9671
|
}
|
|
@@ -9576,7 +9676,12 @@ var init_oauth_server = __esm({
|
|
|
9576
9676
|
if (url2.pathname === "/callback" || url2.pathname === "/") {
|
|
9577
9677
|
const code = url2.searchParams.get("code");
|
|
9578
9678
|
const error48 = url2.searchParams.get("error");
|
|
9579
|
-
|
|
9679
|
+
const state = url2.searchParams.get("state");
|
|
9680
|
+
if (code && this.expectedState !== null && state !== this.expectedState) {
|
|
9681
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
9682
|
+
res.end(ERROR_HTML);
|
|
9683
|
+
this.rejectCode?.(new Error("OAuth error: state mismatch, callback did not come from the authorization request we started"));
|
|
9684
|
+
} else if (code) {
|
|
9580
9685
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
9581
9686
|
res.end(SUCCESS_HTML);
|
|
9582
9687
|
this.resolveCode?.(code);
|
|
@@ -9601,7 +9706,13 @@ var init_oauth_server = __esm({
|
|
|
9601
9706
|
this.server.on("error", reject);
|
|
9602
9707
|
});
|
|
9603
9708
|
}
|
|
9604
|
-
|
|
9709
|
+
/**
|
|
9710
|
+
* Resolve with the authorization code once the browser is redirected back.
|
|
9711
|
+
* Pass the `state` that was put into the authorization URL: callbacks whose
|
|
9712
|
+
* state does not match are rejected instead of resolved (CSRF protection).
|
|
9713
|
+
*/
|
|
9714
|
+
waitForCode(expectedState) {
|
|
9715
|
+
this.expectedState = expectedState ?? null;
|
|
9605
9716
|
return new Promise((resolve, reject) => {
|
|
9606
9717
|
this.resolveCode = (code) => {
|
|
9607
9718
|
if (this.timeoutHandle) clearTimeout(this.timeoutHandle);
|
|
@@ -9633,7 +9744,7 @@ var init_oauth_server = __esm({
|
|
|
9633
9744
|
|
|
9634
9745
|
// src/setup/wizard.ts
|
|
9635
9746
|
var wizard_exports = {};
|
|
9636
|
-
import
|
|
9747
|
+
import crypto5 from "node:crypto";
|
|
9637
9748
|
import { exec } from "node:child_process";
|
|
9638
9749
|
function openBrowser(url2) {
|
|
9639
9750
|
const platform = process.platform;
|
|
@@ -9677,14 +9788,30 @@ async function testAndSave(creds) {
|
|
|
9677
9788
|
async function promptAccountName(defaultName) {
|
|
9678
9789
|
return askText("Account name:", defaultName);
|
|
9679
9790
|
}
|
|
9680
|
-
async function setupGmail() {
|
|
9791
|
+
async function setupGmail(scopeModeFlag) {
|
|
9681
9792
|
console.log("\n--- Gmail Setup ---");
|
|
9793
|
+
if (process.env.EMAIL_MCP_GMAIL_CLIENT_ID) {
|
|
9794
|
+
console.log("Using your own Google OAuth Client ID (EMAIL_MCP_GMAIL_CLIENT_ID).");
|
|
9795
|
+
}
|
|
9682
9796
|
console.log("A browser window will open for Google authorization.\n");
|
|
9797
|
+
let scopeMode = scopeModeFlag;
|
|
9798
|
+
if (!scopeMode) {
|
|
9799
|
+
scopeMode = await askChoice("Gmail permission scope:", [
|
|
9800
|
+
{
|
|
9801
|
+
name: "Full (default) \u2014 includes permanent delete, bypassing Trash",
|
|
9802
|
+
value: "full"
|
|
9803
|
+
},
|
|
9804
|
+
{
|
|
9805
|
+
name: "Restricted \u2014 read/send/label/archive/trash only, no permanent delete",
|
|
9806
|
+
value: "restricted"
|
|
9807
|
+
}
|
|
9808
|
+
]);
|
|
9809
|
+
}
|
|
9683
9810
|
const server = new OAuthCallbackServer();
|
|
9684
9811
|
const port = await server.start();
|
|
9685
9812
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
9686
9813
|
const gmailAuth = new GmailAuth(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, redirectUri);
|
|
9687
|
-
const { url: url2, codeVerifier } = gmailAuth.getAuthUrl(redirectUri);
|
|
9814
|
+
const { url: url2, codeVerifier, state } = gmailAuth.getAuthUrl(redirectUri, scopeMode);
|
|
9688
9815
|
console.log("\nOpening browser for Google authorization...");
|
|
9689
9816
|
console.log(`If the browser does not open, visit:
|
|
9690
9817
|
${url2}
|
|
@@ -9692,7 +9819,7 @@ ${url2}
|
|
|
9692
9819
|
openBrowser(url2);
|
|
9693
9820
|
let code;
|
|
9694
9821
|
try {
|
|
9695
|
-
code = await server.waitForCode();
|
|
9822
|
+
code = await server.waitForCode(state);
|
|
9696
9823
|
} catch (err) {
|
|
9697
9824
|
server.shutdown();
|
|
9698
9825
|
throw new Error(`OAuth flow failed: ${err.message}`);
|
|
@@ -9708,7 +9835,7 @@ ${url2}
|
|
|
9708
9835
|
if (!email3) throw new Error("Could not retrieve Gmail address from profile");
|
|
9709
9836
|
console.log(`Authenticated as ${email3}`);
|
|
9710
9837
|
const name = await promptAccountName("Gmail");
|
|
9711
|
-
const id =
|
|
9838
|
+
const id = crypto5.randomUUID();
|
|
9712
9839
|
const creds = {
|
|
9713
9840
|
id,
|
|
9714
9841
|
name,
|
|
@@ -9725,7 +9852,7 @@ async function setupOutlook() {
|
|
|
9725
9852
|
const port = await server.start();
|
|
9726
9853
|
const redirectUri = `http://localhost:${port}`;
|
|
9727
9854
|
const outlookAuth = new OutlookAuth(OUTLOOK_CLIENT_ID);
|
|
9728
|
-
const { url: url2, codeVerifier } = await outlookAuth.getAuthUrl(redirectUri);
|
|
9855
|
+
const { url: url2, codeVerifier, state } = await outlookAuth.getAuthUrl(redirectUri);
|
|
9729
9856
|
console.log("\nOpening browser for Microsoft authorization...");
|
|
9730
9857
|
console.log(`If the browser does not open, visit:
|
|
9731
9858
|
${url2}
|
|
@@ -9733,7 +9860,7 @@ ${url2}
|
|
|
9733
9860
|
openBrowser(url2);
|
|
9734
9861
|
let code;
|
|
9735
9862
|
try {
|
|
9736
|
-
code = await server.waitForCode();
|
|
9863
|
+
code = await server.waitForCode(state);
|
|
9737
9864
|
} catch (err) {
|
|
9738
9865
|
server.shutdown();
|
|
9739
9866
|
throw new Error(`OAuth flow failed: ${err.message}`);
|
|
@@ -9746,7 +9873,7 @@ ${url2}
|
|
|
9746
9873
|
if (!email3) throw new Error("Could not retrieve email from Microsoft account");
|
|
9747
9874
|
console.log(`Authenticated as ${email3}`);
|
|
9748
9875
|
const name = await promptAccountName("Outlook");
|
|
9749
|
-
const id =
|
|
9876
|
+
const id = crypto5.randomUUID();
|
|
9750
9877
|
const creds = {
|
|
9751
9878
|
id,
|
|
9752
9879
|
name,
|
|
@@ -9778,7 +9905,7 @@ async function setupICloud() {
|
|
|
9778
9905
|
password = await askPassword("App-specific password:");
|
|
9779
9906
|
}
|
|
9780
9907
|
const name = await promptAccountName("iCloud");
|
|
9781
|
-
const id =
|
|
9908
|
+
const id = crypto5.randomUUID();
|
|
9782
9909
|
const creds = {
|
|
9783
9910
|
id,
|
|
9784
9911
|
name,
|
|
@@ -9819,7 +9946,7 @@ async function setupIMAP() {
|
|
|
9819
9946
|
smtpPort = parseInt(smtpPortStr, 10);
|
|
9820
9947
|
}
|
|
9821
9948
|
const name = await promptAccountName("IMAP");
|
|
9822
|
-
const id =
|
|
9949
|
+
const id = crypto5.randomUUID();
|
|
9823
9950
|
const creds = {
|
|
9824
9951
|
id,
|
|
9825
9952
|
name,
|
|
@@ -9883,6 +10010,17 @@ async function main() {
|
|
|
9883
10010
|
await removeAccount(id);
|
|
9884
10011
|
return;
|
|
9885
10012
|
}
|
|
10013
|
+
const scopeIdx = args.indexOf("--scope");
|
|
10014
|
+
let scopeModeFlag;
|
|
10015
|
+
if (scopeIdx !== -1) {
|
|
10016
|
+
const value = args[scopeIdx + 1];
|
|
10017
|
+
if (value === "full" || value === "restricted") {
|
|
10018
|
+
scopeModeFlag = value;
|
|
10019
|
+
} else {
|
|
10020
|
+
console.error("Usage: --scope <full|restricted>");
|
|
10021
|
+
process.exit(1);
|
|
10022
|
+
}
|
|
10023
|
+
}
|
|
9886
10024
|
console.log("@marlinjai/email-mcp Account Setup\n");
|
|
9887
10025
|
let addMore = true;
|
|
9888
10026
|
while (addMore) {
|
|
@@ -9894,7 +10032,7 @@ async function main() {
|
|
|
9894
10032
|
]);
|
|
9895
10033
|
switch (provider) {
|
|
9896
10034
|
case ProviderType.Gmail:
|
|
9897
|
-
await setupGmail();
|
|
10035
|
+
await setupGmail(scopeModeFlag);
|
|
9898
10036
|
break;
|
|
9899
10037
|
case ProviderType.Outlook:
|
|
9900
10038
|
await setupOutlook();
|
|
@@ -33094,7 +33232,7 @@ var StdioServerTransport = class {
|
|
|
33094
33232
|
init_account_manager();
|
|
33095
33233
|
|
|
33096
33234
|
// src/tools/accounts.ts
|
|
33097
|
-
import
|
|
33235
|
+
import crypto4 from "node:crypto";
|
|
33098
33236
|
function jsonResult(data) {
|
|
33099
33237
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
33100
33238
|
}
|
|
@@ -33149,7 +33287,7 @@ function registerAccountTools(server, accountManager) {
|
|
|
33149
33287
|
error: "IMAP host and port are required for non-iCloud providers"
|
|
33150
33288
|
});
|
|
33151
33289
|
}
|
|
33152
|
-
const accountId =
|
|
33290
|
+
const accountId = crypto4.randomUUID();
|
|
33153
33291
|
const creds = {
|
|
33154
33292
|
id: accountId,
|
|
33155
33293
|
name: args.name,
|
|
@@ -33538,6 +33676,32 @@ function registerSendingTools(server, accountManager) {
|
|
|
33538
33676
|
}
|
|
33539
33677
|
}
|
|
33540
33678
|
);
|
|
33679
|
+
server.tool(
|
|
33680
|
+
"email_draft_update",
|
|
33681
|
+
"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.",
|
|
33682
|
+
{
|
|
33683
|
+
accountId: external_exports3.string(),
|
|
33684
|
+
draftId: external_exports3.string(),
|
|
33685
|
+
to: external_exports3.array(ContactSchema),
|
|
33686
|
+
subject: external_exports3.string(),
|
|
33687
|
+
body: BodySchema,
|
|
33688
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when the draft is not in the default Drafts folder)")
|
|
33689
|
+
},
|
|
33690
|
+
async (args) => {
|
|
33691
|
+
try {
|
|
33692
|
+
const provider = await accountManager.getProvider(args.accountId);
|
|
33693
|
+
const params = {
|
|
33694
|
+
to: args.to,
|
|
33695
|
+
subject: args.subject,
|
|
33696
|
+
body: args.body
|
|
33697
|
+
};
|
|
33698
|
+
const result = await provider.updateDraft(args.draftId, params, args.sourceFolder);
|
|
33699
|
+
return jsonResult3(result);
|
|
33700
|
+
} catch (error48) {
|
|
33701
|
+
return jsonResult3({ error: error48.message });
|
|
33702
|
+
}
|
|
33703
|
+
}
|
|
33704
|
+
);
|
|
33541
33705
|
server.tool(
|
|
33542
33706
|
"email_draft_list",
|
|
33543
33707
|
"List email drafts",
|
|
@@ -33725,6 +33889,45 @@ function registerOrganizingTools(server, accountManager) {
|
|
|
33725
33889
|
}
|
|
33726
33890
|
}
|
|
33727
33891
|
);
|
|
33892
|
+
server.tool(
|
|
33893
|
+
"email_batch_label",
|
|
33894
|
+
"Add or remove labels on multiple emails at once (Gmail only). Much faster than individual email_label calls.",
|
|
33895
|
+
{
|
|
33896
|
+
accountId: external_exports3.string(),
|
|
33897
|
+
emailIds: external_exports3.array(external_exports3.string()).min(1).describe("Array of email IDs to label"),
|
|
33898
|
+
addLabels: external_exports3.array(external_exports3.string()).optional(),
|
|
33899
|
+
removeLabels: external_exports3.array(external_exports3.string()).optional()
|
|
33900
|
+
},
|
|
33901
|
+
async (args) => {
|
|
33902
|
+
try {
|
|
33903
|
+
const provider = await accountManager.getProvider(args.accountId);
|
|
33904
|
+
if (!provider.addLabels || !provider.removeLabels) {
|
|
33905
|
+
return jsonResult4({
|
|
33906
|
+
success: false,
|
|
33907
|
+
error: "email_batch_label is only supported on Gmail accounts",
|
|
33908
|
+
supportedProviders: ["gmail"]
|
|
33909
|
+
});
|
|
33910
|
+
}
|
|
33911
|
+
if (provider.batchLabel) {
|
|
33912
|
+
const result2 = await provider.batchLabel(args.emailIds, args.addLabels, args.removeLabels);
|
|
33913
|
+
return jsonResult4({ success: true, ...result2 });
|
|
33914
|
+
}
|
|
33915
|
+
const result = { succeeded: [], failed: [] };
|
|
33916
|
+
for (const id of args.emailIds) {
|
|
33917
|
+
try {
|
|
33918
|
+
if (args.addLabels && args.addLabels.length > 0) await provider.addLabels(id, args.addLabels);
|
|
33919
|
+
if (args.removeLabels && args.removeLabels.length > 0) await provider.removeLabels(id, args.removeLabels);
|
|
33920
|
+
result.succeeded.push(id);
|
|
33921
|
+
} catch (e) {
|
|
33922
|
+
result.failed.push({ id, error: e.message });
|
|
33923
|
+
}
|
|
33924
|
+
}
|
|
33925
|
+
return jsonResult4({ success: true, ...result });
|
|
33926
|
+
} catch (error48) {
|
|
33927
|
+
return jsonResult4({ success: false, error: error48.message });
|
|
33928
|
+
}
|
|
33929
|
+
}
|
|
33930
|
+
);
|
|
33728
33931
|
server.tool(
|
|
33729
33932
|
"email_transfer",
|
|
33730
33933
|
"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.",
|