@marlinjai/email-mcp 1.7.0 → 1.7.2

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
package/dist/index.js CHANGED
@@ -175,6 +175,18 @@ var init_credential_store = __esm({
175
175
  const data = this.read();
176
176
  return Object.values(data.accounts);
177
177
  }
178
+ /**
179
+ * The account already configured for this mailbox, if any. Provider plus
180
+ * address identifies a mailbox; the address comparison is case-insensitive
181
+ * because providers report it in varying case.
182
+ */
183
+ async findByProviderAndEmail(provider, email3) {
184
+ const wanted = email3.trim().toLowerCase();
185
+ const data = this.read();
186
+ return Object.values(data.accounts).find(
187
+ (a) => a.provider === provider && a.email.trim().toLowerCase() === wanted
188
+ ) ?? null;
189
+ }
178
190
  async remove(id) {
179
191
  const data = this.read();
180
192
  delete data.accounts[id];
@@ -7710,15 +7722,17 @@ var init_auth = __esm({
7710
7722
  getAuthUrl(redirectUri, scopeMode) {
7711
7723
  const codeVerifier = crypto2.randomBytes(32).toString("base64url");
7712
7724
  const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
7725
+ const state = crypto2.randomBytes(16).toString("base64url");
7713
7726
  this.oauth2Client = new OAuth2Client2(this.clientId, this.clientSecret, redirectUri);
7714
7727
  const url2 = this.oauth2Client.generateAuthUrl({
7715
7728
  access_type: "offline",
7716
7729
  scope: GMAIL_SCOPES[resolveScopeMode(scopeMode)],
7717
7730
  prompt: "consent",
7718
7731
  code_challenge: codeChallenge,
7719
- code_challenge_method: "S256"
7732
+ code_challenge_method: "S256",
7733
+ state
7720
7734
  });
7721
- return { url: url2, codeVerifier };
7735
+ return { url: url2, codeVerifier, state };
7722
7736
  }
7723
7737
  async exchangeCode(code, codeVerifier) {
7724
7738
  const { tokens } = await this.oauth2Client.getToken({ code, codeVerifier });
@@ -8369,6 +8383,7 @@ var init_adapter2 = __esm({
8369
8383
  });
8370
8384
 
8371
8385
  // src/providers/outlook/auth.ts
8386
+ import crypto3 from "node:crypto";
8372
8387
  import fs2 from "node:fs";
8373
8388
  import path2 from "node:path";
8374
8389
  import os2 from "node:os";
@@ -8423,13 +8438,15 @@ var init_auth2 = __esm({
8423
8438
  }
8424
8439
  async getAuthUrl(redirectUri) {
8425
8440
  const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
8441
+ const state = crypto3.randomBytes(16).toString("base64url");
8426
8442
  const url2 = await this.pca.getAuthCodeUrl({
8427
8443
  scopes: _OutlookAuth.SCOPES,
8428
8444
  redirectUri,
8429
8445
  codeChallenge: challenge,
8430
- codeChallengeMethod: "S256"
8446
+ codeChallengeMethod: "S256",
8447
+ state
8431
8448
  });
8432
- return { url: url2, codeVerifier: verifier };
8449
+ return { url: url2, codeVerifier: verifier, state };
8433
8450
  }
8434
8451
  async exchangeCode(code, codeVerifier, redirectUri) {
8435
8452
  const result = await this.pca.acquireTokenByCode({
@@ -9660,6 +9677,7 @@ var init_oauth_server = __esm({
9660
9677
  rejectCode = null;
9661
9678
  timeoutMs;
9662
9679
  timeoutHandle = null;
9680
+ expectedState = null;
9663
9681
  constructor(timeoutMs = 12e4) {
9664
9682
  this.timeoutMs = timeoutMs;
9665
9683
  }
@@ -9670,7 +9688,12 @@ var init_oauth_server = __esm({
9670
9688
  if (url2.pathname === "/callback" || url2.pathname === "/") {
9671
9689
  const code = url2.searchParams.get("code");
9672
9690
  const error48 = url2.searchParams.get("error");
9673
- if (code) {
9691
+ const state = url2.searchParams.get("state");
9692
+ if (code && this.expectedState !== null && state !== this.expectedState) {
9693
+ res.writeHead(400, { "Content-Type": "text/html" });
9694
+ res.end(ERROR_HTML);
9695
+ this.rejectCode?.(new Error("OAuth error: state mismatch, callback did not come from the authorization request we started"));
9696
+ } else if (code) {
9674
9697
  res.writeHead(200, { "Content-Type": "text/html" });
9675
9698
  res.end(SUCCESS_HTML);
9676
9699
  this.resolveCode?.(code);
@@ -9695,7 +9718,13 @@ var init_oauth_server = __esm({
9695
9718
  this.server.on("error", reject);
9696
9719
  });
9697
9720
  }
9698
- waitForCode() {
9721
+ /**
9722
+ * Resolve with the authorization code once the browser is redirected back.
9723
+ * Pass the `state` that was put into the authorization URL: callbacks whose
9724
+ * state does not match are rejected instead of resolved (CSRF protection).
9725
+ */
9726
+ waitForCode(expectedState) {
9727
+ this.expectedState = expectedState ?? null;
9699
9728
  return new Promise((resolve, reject) => {
9700
9729
  this.resolveCode = (code) => {
9701
9730
  if (this.timeoutHandle) clearTimeout(this.timeoutHandle);
@@ -9727,7 +9756,7 @@ var init_oauth_server = __esm({
9727
9756
 
9728
9757
  // src/setup/wizard.ts
9729
9758
  var wizard_exports = {};
9730
- import crypto4 from "node:crypto";
9759
+ import crypto5 from "node:crypto";
9731
9760
  import { exec } from "node:child_process";
9732
9761
  function openBrowser(url2) {
9733
9762
  const platform = process.platform;
@@ -9771,6 +9800,14 @@ async function testAndSave(creds) {
9771
9800
  async function promptAccountName(defaultName) {
9772
9801
  return askText("Account name:", defaultName);
9773
9802
  }
9803
+ async function resolveAccountIdentity(provider, email3, defaultName) {
9804
+ const existing = await store.findByProviderAndEmail(provider, email3);
9805
+ if (existing) {
9806
+ console.log(`An account for ${email3} already exists ("${existing.name}"), updating it in place.`);
9807
+ return { id: existing.id, name: await promptAccountName(existing.name) };
9808
+ }
9809
+ return { id: crypto5.randomUUID(), name: await promptAccountName(defaultName) };
9810
+ }
9774
9811
  async function setupGmail(scopeModeFlag) {
9775
9812
  console.log("\n--- Gmail Setup ---");
9776
9813
  if (process.env.EMAIL_MCP_GMAIL_CLIENT_ID) {
@@ -9794,7 +9831,7 @@ async function setupGmail(scopeModeFlag) {
9794
9831
  const port = await server.start();
9795
9832
  const redirectUri = `http://localhost:${port}/callback`;
9796
9833
  const gmailAuth = new GmailAuth(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, redirectUri);
9797
- const { url: url2, codeVerifier } = gmailAuth.getAuthUrl(redirectUri, scopeMode);
9834
+ const { url: url2, codeVerifier, state } = gmailAuth.getAuthUrl(redirectUri, scopeMode);
9798
9835
  console.log("\nOpening browser for Google authorization...");
9799
9836
  console.log(`If the browser does not open, visit:
9800
9837
  ${url2}
@@ -9802,7 +9839,7 @@ ${url2}
9802
9839
  openBrowser(url2);
9803
9840
  let code;
9804
9841
  try {
9805
- code = await server.waitForCode();
9842
+ code = await server.waitForCode(state);
9806
9843
  } catch (err) {
9807
9844
  server.shutdown();
9808
9845
  throw new Error(`OAuth flow failed: ${err.message}`);
@@ -9817,8 +9854,7 @@ ${url2}
9817
9854
  const email3 = profile.emailAddress ?? "";
9818
9855
  if (!email3) throw new Error("Could not retrieve Gmail address from profile");
9819
9856
  console.log(`Authenticated as ${email3}`);
9820
- const name = await promptAccountName("Gmail");
9821
- const id = crypto4.randomUUID();
9857
+ const { id, name } = await resolveAccountIdentity(ProviderType.Gmail, email3.trim(), "Gmail");
9822
9858
  const creds = {
9823
9859
  id,
9824
9860
  name,
@@ -9835,7 +9871,7 @@ async function setupOutlook() {
9835
9871
  const port = await server.start();
9836
9872
  const redirectUri = `http://localhost:${port}`;
9837
9873
  const outlookAuth = new OutlookAuth(OUTLOOK_CLIENT_ID);
9838
- const { url: url2, codeVerifier } = await outlookAuth.getAuthUrl(redirectUri);
9874
+ const { url: url2, codeVerifier, state } = await outlookAuth.getAuthUrl(redirectUri);
9839
9875
  console.log("\nOpening browser for Microsoft authorization...");
9840
9876
  console.log(`If the browser does not open, visit:
9841
9877
  ${url2}
@@ -9843,7 +9879,7 @@ ${url2}
9843
9879
  openBrowser(url2);
9844
9880
  let code;
9845
9881
  try {
9846
- code = await server.waitForCode();
9882
+ code = await server.waitForCode(state);
9847
9883
  } catch (err) {
9848
9884
  server.shutdown();
9849
9885
  throw new Error(`OAuth flow failed: ${err.message}`);
@@ -9855,8 +9891,7 @@ ${url2}
9855
9891
  const email3 = account?.username ?? "";
9856
9892
  if (!email3) throw new Error("Could not retrieve email from Microsoft account");
9857
9893
  console.log(`Authenticated as ${email3}`);
9858
- const name = await promptAccountName("Outlook");
9859
- const id = crypto4.randomUUID();
9894
+ const { id, name } = await resolveAccountIdentity(ProviderType.Outlook, email3.trim(), "Outlook");
9860
9895
  const creds = {
9861
9896
  id,
9862
9897
  name,
@@ -9887,8 +9922,7 @@ async function setupICloud() {
9887
9922
  console.log(" Password is required");
9888
9923
  password = await askPassword("App-specific password:");
9889
9924
  }
9890
- const name = await promptAccountName("iCloud");
9891
- const id = crypto4.randomUUID();
9925
+ const { id, name } = await resolveAccountIdentity(ProviderType.ICloud, email3.trim(), "iCloud");
9892
9926
  const creds = {
9893
9927
  id,
9894
9928
  name,
@@ -9928,8 +9962,7 @@ async function setupIMAP() {
9928
9962
  const smtpPortStr = await askValidated("SMTP port:", "587", portValidator);
9929
9963
  smtpPort = parseInt(smtpPortStr, 10);
9930
9964
  }
9931
- const name = await promptAccountName("IMAP");
9932
- const id = crypto4.randomUUID();
9965
+ const { id, name } = await resolveAccountIdentity(ProviderType.IMAP, email3.trim(), "IMAP");
9933
9966
  const creds = {
9934
9967
  id,
9935
9968
  name,
@@ -33215,7 +33248,7 @@ var StdioServerTransport = class {
33215
33248
  init_account_manager();
33216
33249
 
33217
33250
  // src/tools/accounts.ts
33218
- import crypto3 from "node:crypto";
33251
+ import crypto4 from "node:crypto";
33219
33252
  function jsonResult(data) {
33220
33253
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
33221
33254
  }
@@ -33270,7 +33303,7 @@ function registerAccountTools(server, accountManager) {
33270
33303
  error: "IMAP host and port are required for non-iCloud providers"
33271
33304
  });
33272
33305
  }
33273
- const accountId = crypto3.randomUUID();
33306
+ const accountId = crypto4.randomUUID();
33274
33307
  const creds = {
33275
33308
  id: accountId,
33276
33309
  name: args.name,