@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.
@@ -2,7 +2,7 @@
2
2
 
3
3
 
4
4
  // src/setup/wizard.ts
5
- import crypto3 from "node:crypto";
5
+ import crypto4 from "node:crypto";
6
6
  import { exec } from "node:child_process";
7
7
 
8
8
  // src/setup/prompts.ts
@@ -245,6 +245,18 @@ var CredentialStore = class {
245
245
  const data = this.read();
246
246
  return Object.values(data.accounts);
247
247
  }
248
+ /**
249
+ * The account already configured for this mailbox, if any. Provider plus
250
+ * address identifies a mailbox; the address comparison is case-insensitive
251
+ * because providers report it in varying case.
252
+ */
253
+ async findByProviderAndEmail(provider, email) {
254
+ const wanted = email.trim().toLowerCase();
255
+ const data = this.read();
256
+ return Object.values(data.accounts).find(
257
+ (a) => a.provider === provider && a.email.trim().toLowerCase() === wanted
258
+ ) ?? null;
259
+ }
248
260
  async remove(id) {
249
261
  const data = this.read();
250
262
  delete data.accounts[id];
@@ -276,6 +288,7 @@ var OAuthCallbackServer = class {
276
288
  rejectCode = null;
277
289
  timeoutMs;
278
290
  timeoutHandle = null;
291
+ expectedState = null;
279
292
  constructor(timeoutMs = 12e4) {
280
293
  this.timeoutMs = timeoutMs;
281
294
  }
@@ -286,7 +299,12 @@ var OAuthCallbackServer = class {
286
299
  if (url.pathname === "/callback" || url.pathname === "/") {
287
300
  const code = url.searchParams.get("code");
288
301
  const error = url.searchParams.get("error");
289
- if (code) {
302
+ const state = url.searchParams.get("state");
303
+ if (code && this.expectedState !== null && state !== this.expectedState) {
304
+ res.writeHead(400, { "Content-Type": "text/html" });
305
+ res.end(ERROR_HTML);
306
+ this.rejectCode?.(new Error("OAuth error: state mismatch, callback did not come from the authorization request we started"));
307
+ } else if (code) {
290
308
  res.writeHead(200, { "Content-Type": "text/html" });
291
309
  res.end(SUCCESS_HTML);
292
310
  this.resolveCode?.(code);
@@ -311,7 +329,13 @@ var OAuthCallbackServer = class {
311
329
  this.server.on("error", reject);
312
330
  });
313
331
  }
314
- waitForCode() {
332
+ /**
333
+ * Resolve with the authorization code once the browser is redirected back.
334
+ * Pass the `state` that was put into the authorization URL: callbacks whose
335
+ * state does not match are rejected instead of resolved (CSRF protection).
336
+ */
337
+ waitForCode(expectedState) {
338
+ this.expectedState = expectedState ?? null;
315
339
  return new Promise((resolve, reject) => {
316
340
  this.resolveCode = (code) => {
317
341
  if (this.timeoutHandle) clearTimeout(this.timeoutHandle);
@@ -381,15 +405,17 @@ var GmailAuth = class {
381
405
  getAuthUrl(redirectUri, scopeMode) {
382
406
  const codeVerifier = crypto2.randomBytes(32).toString("base64url");
383
407
  const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
408
+ const state = crypto2.randomBytes(16).toString("base64url");
384
409
  this.oauth2Client = new OAuth2Client(this.clientId, this.clientSecret, redirectUri);
385
410
  const url = this.oauth2Client.generateAuthUrl({
386
411
  access_type: "offline",
387
412
  scope: GMAIL_SCOPES[resolveScopeMode(scopeMode)],
388
413
  prompt: "consent",
389
414
  code_challenge: codeChallenge,
390
- code_challenge_method: "S256"
415
+ code_challenge_method: "S256",
416
+ state
391
417
  });
392
- return { url, codeVerifier };
418
+ return { url, codeVerifier, state };
393
419
  }
394
420
  async exchangeCode(code, codeVerifier) {
395
421
  const { tokens } = await this.oauth2Client.getToken({ code, codeVerifier });
@@ -421,6 +447,7 @@ var GmailAuth = class {
421
447
  };
422
448
 
423
449
  // src/providers/outlook/auth.ts
450
+ import crypto3 from "node:crypto";
424
451
  import fs2 from "node:fs";
425
452
  import path2 from "node:path";
426
453
  import os2 from "node:os";
@@ -471,13 +498,15 @@ var OutlookAuth = class _OutlookAuth {
471
498
  }
472
499
  async getAuthUrl(redirectUri) {
473
500
  const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
501
+ const state = crypto3.randomBytes(16).toString("base64url");
474
502
  const url = await this.pca.getAuthCodeUrl({
475
503
  scopes: _OutlookAuth.SCOPES,
476
504
  redirectUri,
477
505
  codeChallenge: challenge,
478
- codeChallengeMethod: "S256"
506
+ codeChallengeMethod: "S256",
507
+ state
479
508
  });
480
- return { url, codeVerifier: verifier };
509
+ return { url, codeVerifier: verifier, state };
481
510
  }
482
511
  async exchangeCode(code, codeVerifier, redirectUri) {
483
512
  const result = await this.pca.acquireTokenByCode({
@@ -2864,6 +2893,14 @@ async function testAndSave(creds) {
2864
2893
  async function promptAccountName(defaultName) {
2865
2894
  return askText("Account name:", defaultName);
2866
2895
  }
2896
+ async function resolveAccountIdentity(provider, email, defaultName) {
2897
+ const existing = await store.findByProviderAndEmail(provider, email);
2898
+ if (existing) {
2899
+ console.log(`An account for ${email} already exists ("${existing.name}"), updating it in place.`);
2900
+ return { id: existing.id, name: await promptAccountName(existing.name) };
2901
+ }
2902
+ return { id: crypto4.randomUUID(), name: await promptAccountName(defaultName) };
2903
+ }
2867
2904
  async function setupGmail(scopeModeFlag) {
2868
2905
  console.log("\n--- Gmail Setup ---");
2869
2906
  if (process.env.EMAIL_MCP_GMAIL_CLIENT_ID) {
@@ -2887,7 +2924,7 @@ async function setupGmail(scopeModeFlag) {
2887
2924
  const port = await server.start();
2888
2925
  const redirectUri = `http://localhost:${port}/callback`;
2889
2926
  const gmailAuth = new GmailAuth(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, redirectUri);
2890
- const { url, codeVerifier } = gmailAuth.getAuthUrl(redirectUri, scopeMode);
2927
+ const { url, codeVerifier, state } = gmailAuth.getAuthUrl(redirectUri, scopeMode);
2891
2928
  console.log("\nOpening browser for Google authorization...");
2892
2929
  console.log(`If the browser does not open, visit:
2893
2930
  ${url}
@@ -2895,7 +2932,7 @@ ${url}
2895
2932
  openBrowser(url);
2896
2933
  let code;
2897
2934
  try {
2898
- code = await server.waitForCode();
2935
+ code = await server.waitForCode(state);
2899
2936
  } catch (err) {
2900
2937
  server.shutdown();
2901
2938
  throw new Error(`OAuth flow failed: ${err.message}`);
@@ -2910,8 +2947,7 @@ ${url}
2910
2947
  const email = profile.emailAddress ?? "";
2911
2948
  if (!email) throw new Error("Could not retrieve Gmail address from profile");
2912
2949
  console.log(`Authenticated as ${email}`);
2913
- const name = await promptAccountName("Gmail");
2914
- const id = crypto3.randomUUID();
2950
+ const { id, name } = await resolveAccountIdentity(ProviderType.Gmail, email.trim(), "Gmail");
2915
2951
  const creds = {
2916
2952
  id,
2917
2953
  name,
@@ -2928,7 +2964,7 @@ async function setupOutlook() {
2928
2964
  const port = await server.start();
2929
2965
  const redirectUri = `http://localhost:${port}`;
2930
2966
  const outlookAuth = new OutlookAuth(OUTLOOK_CLIENT_ID);
2931
- const { url, codeVerifier } = await outlookAuth.getAuthUrl(redirectUri);
2967
+ const { url, codeVerifier, state } = await outlookAuth.getAuthUrl(redirectUri);
2932
2968
  console.log("\nOpening browser for Microsoft authorization...");
2933
2969
  console.log(`If the browser does not open, visit:
2934
2970
  ${url}
@@ -2936,7 +2972,7 @@ ${url}
2936
2972
  openBrowser(url);
2937
2973
  let code;
2938
2974
  try {
2939
- code = await server.waitForCode();
2975
+ code = await server.waitForCode(state);
2940
2976
  } catch (err) {
2941
2977
  server.shutdown();
2942
2978
  throw new Error(`OAuth flow failed: ${err.message}`);
@@ -2948,8 +2984,7 @@ ${url}
2948
2984
  const email = account?.username ?? "";
2949
2985
  if (!email) throw new Error("Could not retrieve email from Microsoft account");
2950
2986
  console.log(`Authenticated as ${email}`);
2951
- const name = await promptAccountName("Outlook");
2952
- const id = crypto3.randomUUID();
2987
+ const { id, name } = await resolveAccountIdentity(ProviderType.Outlook, email.trim(), "Outlook");
2953
2988
  const creds = {
2954
2989
  id,
2955
2990
  name,
@@ -2980,8 +3015,7 @@ async function setupICloud() {
2980
3015
  console.log(" Password is required");
2981
3016
  password = await askPassword("App-specific password:");
2982
3017
  }
2983
- const name = await promptAccountName("iCloud");
2984
- const id = crypto3.randomUUID();
3018
+ const { id, name } = await resolveAccountIdentity(ProviderType.ICloud, email.trim(), "iCloud");
2985
3019
  const creds = {
2986
3020
  id,
2987
3021
  name,
@@ -3021,8 +3055,7 @@ async function setupIMAP() {
3021
3055
  const smtpPortStr = await askValidated("SMTP port:", "587", portValidator);
3022
3056
  smtpPort = parseInt(smtpPortStr, 10);
3023
3057
  }
3024
- const name = await promptAccountName("IMAP");
3025
- const id = crypto3.randomUUID();
3058
+ const { id, name } = await resolveAccountIdentity(ProviderType.IMAP, email.trim(), "IMAP");
3026
3059
  const creds = {
3027
3060
  id,
3028
3061
  name,