@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.
@@ -342,14 +342,33 @@ var OAuthCallbackServer = class {
342
342
  // src/providers/gmail/auth.ts
343
343
  import { OAuth2Client } from "google-auth-library";
344
344
  import crypto2 from "node:crypto";
345
- var GMAIL_SCOPES = [
346
- "https://mail.google.com/",
347
- "https://www.googleapis.com/auth/gmail.modify",
348
- // Mailbox content access (above) does NOT cover the Settings API —
349
- // Google treats them as separate permission domains. Needed for
350
- // users.settings.filters.* (email_create_block_rule/list/delete).
351
- "https://www.googleapis.com/auth/gmail.settings.basic"
352
- ];
345
+ var GMAIL_SCOPES = {
346
+ full: [
347
+ // Gmail's maximum-permission scope. Superset of gmail.modify below;
348
+ // the only capability it adds is immediate, trash-bypassing permanent
349
+ // deletion (messages.delete / batchDelete with permanent: true).
350
+ "https://mail.google.com/",
351
+ "https://www.googleapis.com/auth/gmail.modify",
352
+ // Mailbox content access (above) does NOT cover the Settings API —
353
+ // Google treats them as separate permission domains. Needed for
354
+ // users.settings.filters.* (email_create_block_rule/list/delete).
355
+ "https://www.googleapis.com/auth/gmail.settings.basic"
356
+ ],
357
+ // Drops https://mail.google.com/ so an AI-triggered permanent delete
358
+ // can't happen: gmail.modify already covers every other read/write
359
+ // operation this server performs (read, send, label, archive, trash,
360
+ // drafts, filters). Choosing this mode means deleteEmail(..., permanent:
361
+ // true) and batchDelete(..., permanent: true) will fail with a Gmail API
362
+ // 403 instead of succeeding — everything else behaves identically.
363
+ restricted: [
364
+ "https://www.googleapis.com/auth/gmail.modify",
365
+ "https://www.googleapis.com/auth/gmail.settings.basic"
366
+ ]
367
+ };
368
+ function resolveScopeMode(explicit) {
369
+ if (explicit) return explicit;
370
+ return process.env.EMAIL_MCP_GMAIL_SCOPE?.toLowerCase() === "restricted" ? "restricted" : "full";
371
+ }
353
372
  var GmailAuth = class {
354
373
  clientId;
355
374
  clientSecret;
@@ -359,13 +378,13 @@ var GmailAuth = class {
359
378
  this.clientSecret = clientSecret;
360
379
  this.oauth2Client = new OAuth2Client(clientId, clientSecret, redirectUri);
361
380
  }
362
- getAuthUrl(redirectUri) {
381
+ getAuthUrl(redirectUri, scopeMode) {
363
382
  const codeVerifier = crypto2.randomBytes(32).toString("base64url");
364
383
  const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
365
384
  this.oauth2Client = new OAuth2Client(this.clientId, this.clientSecret, redirectUri);
366
385
  const url = this.oauth2Client.generateAuthUrl({
367
386
  access_type: "offline",
368
- scope: GMAIL_SCOPES,
387
+ scope: GMAIL_SCOPES[resolveScopeMode(scopeMode)],
369
388
  prompt: "consent",
370
389
  code_challenge: codeChallenge,
371
390
  code_challenge_method: "S256"
@@ -728,11 +747,12 @@ var GmailAdapter = class {
728
747
  const labels = res.data.labels || [];
729
748
  return labels.map(mapGmailLabel);
730
749
  }
731
- async createFolder(name) {
750
+ async createFolder(name, parentPath) {
732
751
  const gmail = this.ensureConnected();
752
+ const fullName = parentPath ? `${parentPath}/${name}` : name;
733
753
  const res = await gmail.users.labels.create({
734
754
  userId: "me",
735
- requestBody: { name }
755
+ requestBody: { name: fullName }
736
756
  });
737
757
  return mapGmailLabel(res.data);
738
758
  }
@@ -849,6 +869,18 @@ var GmailAdapter = class {
849
869
  });
850
870
  return { id: res.data.id || "" };
851
871
  }
872
+ async updateDraft(draftId, params) {
873
+ const gmail = this.ensureConnected();
874
+ const raw = this.buildRfc2822(params);
875
+ const res = await gmail.users.drafts.update({
876
+ userId: "me",
877
+ id: draftId,
878
+ requestBody: {
879
+ message: { raw }
880
+ }
881
+ });
882
+ return { id: res.data.id || draftId };
883
+ }
852
884
  async listDrafts(limit, _offset) {
853
885
  const gmail = this.ensureConnected();
854
886
  const res = await gmail.users.drafts.list({
@@ -1012,6 +1044,34 @@ var GmailAdapter = class {
1012
1044
  }
1013
1045
  return result;
1014
1046
  }
1047
+ async batchLabel(emailIds, addLabels, removeLabels) {
1048
+ const gmail = this.ensureConnected();
1049
+ const result = { succeeded: [], failed: [] };
1050
+ const addLabelIds = addLabels ?? [];
1051
+ const removeLabelIds = removeLabels ?? [];
1052
+ if (addLabelIds.length === 0 && removeLabelIds.length === 0) {
1053
+ result.succeeded = [...emailIds];
1054
+ return result;
1055
+ }
1056
+ try {
1057
+ await gmail.users.messages.batchModify({
1058
+ userId: "me",
1059
+ requestBody: { ids: emailIds, addLabelIds, removeLabelIds }
1060
+ });
1061
+ result.succeeded = [...emailIds];
1062
+ } catch (error) {
1063
+ for (const id of emailIds) {
1064
+ try {
1065
+ if (addLabelIds.length > 0) await this.addLabels(id, addLabelIds);
1066
+ if (removeLabelIds.length > 0) await this.removeLabels(id, removeLabelIds);
1067
+ result.succeeded.push(id);
1068
+ } catch (e) {
1069
+ result.failed.push({ id, error: e.message });
1070
+ }
1071
+ }
1072
+ }
1073
+ return result;
1074
+ }
1015
1075
  async getRawMessage(emailId, _sourceFolder) {
1016
1076
  const gmail = this.ensureConnected();
1017
1077
  const res = await gmail.users.messages.get({
@@ -1475,6 +1535,12 @@ var OutlookAdapter = class {
1475
1535
  const result = await client.api("/me/messages").post(message);
1476
1536
  return { id: result.id };
1477
1537
  }
1538
+ async updateDraft(draftId, params) {
1539
+ const client = this.ensureClient();
1540
+ const message = this.buildGraphMessage(params);
1541
+ await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(message);
1542
+ return { id: draftId };
1543
+ }
1478
1544
  async listDrafts(limit, offset) {
1479
1545
  const client = this.ensureClient();
1480
1546
  let request = client.api("/me/mailFolders/drafts/messages");
@@ -2317,8 +2383,7 @@ var ImapAdapter = class {
2317
2383
  const messageId = await sendViaSmtp(transport, this.email, params);
2318
2384
  return { id: messageId };
2319
2385
  }
2320
- async createDraft(params) {
2321
- const client = await this.ensureConnected();
2386
+ buildDraftRfc2822(params) {
2322
2387
  const lines = [];
2323
2388
  lines.push(`From: ${this.email}`);
2324
2389
  lines.push(`To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`);
@@ -2333,12 +2398,41 @@ var ImapAdapter = class {
2333
2398
  lines.push("Content-Type: text/plain; charset=utf-8");
2334
2399
  lines.push("");
2335
2400
  lines.push(params.body.text || "");
2336
- const rawMessage = lines.join("\r\n");
2401
+ return lines.join("\r\n");
2402
+ }
2403
+ async createDraft(params) {
2404
+ const client = await this.ensureConnected();
2405
+ const rawMessage = this.buildDraftRfc2822(params);
2337
2406
  const draftsFolder = await this.resolveFolder("Drafts");
2338
2407
  const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
2339
2408
  if (!result) throw new Error("Failed to append draft");
2340
2409
  return { id: String(result.uid || result) };
2341
2410
  }
2411
+ /**
2412
+ * IMAP has no in-place message update — a draft is just a message in the
2413
+ * Drafts folder, and IMAP messages are immutable once appended. This
2414
+ * deletes the old revision and appends a new one, so the returned `id`
2415
+ * is a NEW uid; the old id no longer resolves after this call.
2416
+ */
2417
+ async updateDraft(draftId, params, sourceFolder) {
2418
+ const client = await this.ensureConnected();
2419
+ const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
2420
+ let lock;
2421
+ try {
2422
+ lock = await client.getMailboxLock(draftsFolder);
2423
+ } catch (error) {
2424
+ throw formatImapError(error, `Failed to open folder "${draftsFolder}"`);
2425
+ }
2426
+ try {
2427
+ await client.messageDelete(draftId, { uid: true });
2428
+ } finally {
2429
+ lock.release();
2430
+ }
2431
+ const rawMessage = this.buildDraftRfc2822(params);
2432
+ const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
2433
+ if (!result) throw new Error("Failed to append updated draft");
2434
+ return { id: String(result.uid || result) };
2435
+ }
2342
2436
  async listDrafts(limit, offset) {
2343
2437
  return this.search({
2344
2438
  folder: "Drafts",
@@ -2580,9 +2674,9 @@ var ICloudAdapter = class extends ImapAdapter {
2580
2674
  };
2581
2675
 
2582
2676
  // src/oauth-config.ts
2583
- var GMAIL_CLIENT_ID = "1062266096576-lu2ukplhc3lcm0bvheml5i18urptdp4t.apps.googleusercontent.com";
2584
- var GMAIL_CLIENT_SECRET = "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";
2585
- var OUTLOOK_CLIENT_ID = "76d32225-ef10-4b49-951c-c80ae906595d";
2677
+ var GMAIL_CLIENT_ID = process.env.EMAIL_MCP_GMAIL_CLIENT_ID || "1062266096576-lu2ukplhc3lcm0bvheml5i18urptdp4t.apps.googleusercontent.com";
2678
+ var GMAIL_CLIENT_SECRET = process.env.EMAIL_MCP_GMAIL_CLIENT_SECRET || "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";
2679
+ var OUTLOOK_CLIENT_ID = process.env.EMAIL_MCP_OUTLOOK_CLIENT_ID || "76d32225-ef10-4b49-951c-c80ae906595d";
2586
2680
 
2587
2681
  // src/account-manager.ts
2588
2682
  function createProvider(provider) {
@@ -2770,14 +2864,30 @@ async function testAndSave(creds) {
2770
2864
  async function promptAccountName(defaultName) {
2771
2865
  return askText("Account name:", defaultName);
2772
2866
  }
2773
- async function setupGmail() {
2867
+ async function setupGmail(scopeModeFlag) {
2774
2868
  console.log("\n--- Gmail Setup ---");
2869
+ if (process.env.EMAIL_MCP_GMAIL_CLIENT_ID) {
2870
+ console.log("Using your own Google OAuth Client ID (EMAIL_MCP_GMAIL_CLIENT_ID).");
2871
+ }
2775
2872
  console.log("A browser window will open for Google authorization.\n");
2873
+ let scopeMode = scopeModeFlag;
2874
+ if (!scopeMode) {
2875
+ scopeMode = await askChoice("Gmail permission scope:", [
2876
+ {
2877
+ name: "Full (default) \u2014 includes permanent delete, bypassing Trash",
2878
+ value: "full"
2879
+ },
2880
+ {
2881
+ name: "Restricted \u2014 read/send/label/archive/trash only, no permanent delete",
2882
+ value: "restricted"
2883
+ }
2884
+ ]);
2885
+ }
2776
2886
  const server = new OAuthCallbackServer();
2777
2887
  const port = await server.start();
2778
2888
  const redirectUri = `http://localhost:${port}/callback`;
2779
2889
  const gmailAuth = new GmailAuth(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, redirectUri);
2780
- const { url, codeVerifier } = gmailAuth.getAuthUrl(redirectUri);
2890
+ const { url, codeVerifier } = gmailAuth.getAuthUrl(redirectUri, scopeMode);
2781
2891
  console.log("\nOpening browser for Google authorization...");
2782
2892
  console.log(`If the browser does not open, visit:
2783
2893
  ${url}
@@ -2976,6 +3086,17 @@ async function main() {
2976
3086
  await removeAccount(id);
2977
3087
  return;
2978
3088
  }
3089
+ const scopeIdx = args.indexOf("--scope");
3090
+ let scopeModeFlag;
3091
+ if (scopeIdx !== -1) {
3092
+ const value = args[scopeIdx + 1];
3093
+ if (value === "full" || value === "restricted") {
3094
+ scopeModeFlag = value;
3095
+ } else {
3096
+ console.error("Usage: --scope <full|restricted>");
3097
+ process.exit(1);
3098
+ }
3099
+ }
2979
3100
  console.log("@marlinjai/email-mcp Account Setup\n");
2980
3101
  let addMore = true;
2981
3102
  while (addMore) {
@@ -2987,7 +3108,7 @@ async function main() {
2987
3108
  ]);
2988
3109
  switch (provider) {
2989
3110
  case ProviderType.Gmail:
2990
- await setupGmail();
3111
+ await setupGmail(scopeModeFlag);
2991
3112
  break;
2992
3113
  case ProviderType.Outlook:
2993
3114
  await setupOutlook();