@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 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
- > **Note:** If you prefer to use your own OAuth app, create a Desktop OAuth 2.0 Client in the [Google Cloud Console](https://console.cloud.google.com/) with the Gmail API enabled.
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 (31)
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 (5)
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
- "https://mail.google.com/",
7635
- "https://www.googleapis.com/auth/gmail.modify",
7636
- // Mailbox content access (above) does NOT cover the Settings API —
7637
- // Google treats them as separate permission domains. Needed for
7638
- // users.settings.filters.* (email_create_block_rule/list/delete).
7639
- "https://www.googleapis.com/auth/gmail.settings.basic"
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
- async createDraft(params) {
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
- const rawMessage = lines.join("\r\n");
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
- if (code) {
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
- waitForCode() {
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 crypto4 from "node:crypto";
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 = crypto4.randomUUID();
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 = crypto4.randomUUID();
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 = crypto4.randomUUID();
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 = crypto4.randomUUID();
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 crypto3 from "node:crypto";
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 = crypto3.randomUUID();
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.",