@marlinjai/email-mcp 1.7.2 → 1.8.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/dist/index.js CHANGED
@@ -123,6 +123,14 @@ function decrypt(ciphertext, seeds) {
123
123
  }
124
124
  throw lastError ?? new Error("Unable to decrypt credentials");
125
125
  }
126
+ function encryptWithMachineKey(plaintext) {
127
+ return encrypt(plaintext, getSeedCandidates()[0]);
128
+ }
129
+ function decryptWithMachineKey(ciphertext) {
130
+ const seeds = getSeedCandidates();
131
+ const { plaintext, seedUsed } = decrypt(ciphertext, seeds);
132
+ return { plaintext, usedLegacyKey: seedUsed !== seeds[0] };
133
+ }
126
134
  var ALGORITHM, KEY_LENGTH, IV_LENGTH, SALT_LENGTH, PBKDF2_ITERATIONS, cachedMachineId, CredentialStore;
127
135
  var init_credential_store = __esm({
128
136
  "src/auth/credential-store.ts"() {
@@ -146,10 +154,9 @@ var init_credential_store = __esm({
146
154
  return { accounts: {} };
147
155
  }
148
156
  const raw = fs.readFileSync(this.filePath, "utf-8");
149
- const seeds = getSeedCandidates();
150
- const { plaintext, seedUsed } = decrypt(raw, seeds);
157
+ const { plaintext, usedLegacyKey } = decryptWithMachineKey(raw);
151
158
  const data = JSON.parse(plaintext);
152
- if (seedUsed !== seeds[0]) {
159
+ if (usedLegacyKey) {
153
160
  try {
154
161
  this.write(data);
155
162
  } catch {
@@ -159,7 +166,7 @@ var init_credential_store = __esm({
159
166
  }
160
167
  write(data) {
161
168
  const json2 = JSON.stringify(data);
162
- const encrypted = encrypt(json2, getSeedCandidates()[0]);
169
+ const encrypted = encryptWithMachineKey(json2);
163
170
  fs.writeFileSync(this.filePath, encrypted, { mode: 384 });
164
171
  }
165
172
  async save(creds) {
@@ -7144,6 +7151,58 @@ var init_mapper = __esm({
7144
7151
  }
7145
7152
  });
7146
7153
 
7154
+ // src/providers/mime.ts
7155
+ import MailComposer from "nodemailer/lib/mail-composer/index.js";
7156
+ import { convert } from "html-to-text";
7157
+ function htmlToText(html) {
7158
+ return convert(html, {
7159
+ wordwrap: false,
7160
+ selectors: [
7161
+ { selector: "img", format: "skip" },
7162
+ { selector: "a", options: { hideLinkHrefIfSameAsText: true } }
7163
+ ]
7164
+ }).trim();
7165
+ }
7166
+ function normalizeBody(body) {
7167
+ const html = body.html ? body.html : void 0;
7168
+ if (body.text) return { text: body.text, html };
7169
+ if (html) return { text: htmlToText(html), html };
7170
+ return { text: "" };
7171
+ }
7172
+ function toAddress(contact) {
7173
+ return contact.name ? { name: contact.name, address: contact.email } : contact.email;
7174
+ }
7175
+ function toAddressList(contacts) {
7176
+ return contacts?.length ? contacts.map(toAddress) : void 0;
7177
+ }
7178
+ async function buildMimeMessage(from, params) {
7179
+ const { text, html } = normalizeBody(params.body);
7180
+ const mail = {
7181
+ from,
7182
+ to: toAddressList(params.to),
7183
+ cc: toAddressList(params.cc),
7184
+ bcc: toAddressList(params.bcc),
7185
+ subject: params.subject,
7186
+ text,
7187
+ html,
7188
+ inReplyTo: params.inReplyTo,
7189
+ references: params.references?.length ? params.references : void 0,
7190
+ attachments: params.attachments?.map((a) => ({
7191
+ filename: a.filename,
7192
+ content: a.content,
7193
+ contentType: a.contentType
7194
+ }))
7195
+ };
7196
+ const node = new MailComposer(mail).compile();
7197
+ node.keepBcc = true;
7198
+ return node.build();
7199
+ }
7200
+ var init_mime = __esm({
7201
+ "src/providers/mime.ts"() {
7202
+ "use strict";
7203
+ }
7204
+ });
7205
+
7147
7206
  // src/providers/gmail/adapter.ts
7148
7207
  import { Readable } from "node:stream";
7149
7208
  import { google } from "googleapis";
@@ -7154,6 +7213,7 @@ var init_adapter = __esm({
7154
7213
  "use strict";
7155
7214
  init_types();
7156
7215
  init_mapper();
7216
+ init_mime();
7157
7217
  GmailAdapter = class {
7158
7218
  providerType = ProviderType.Gmail;
7159
7219
  gmail = null;
@@ -7295,7 +7355,7 @@ var init_adapter = __esm({
7295
7355
  }
7296
7356
  async sendEmail(params) {
7297
7357
  const gmail = this.ensureConnected();
7298
- const raw = this.buildRfc2822(params);
7358
+ const raw = await this.buildRaw(params);
7299
7359
  const res = await gmail.users.messages.send({
7300
7360
  userId: "me",
7301
7361
  requestBody: { raw }
@@ -7307,7 +7367,7 @@ var init_adapter = __esm({
7307
7367
  }
7308
7368
  async createDraft(params) {
7309
7369
  const gmail = this.ensureConnected();
7310
- const raw = this.buildRfc2822(params);
7370
+ const raw = await this.buildRaw(params);
7311
7371
  const res = await gmail.users.drafts.create({
7312
7372
  userId: "me",
7313
7373
  requestBody: {
@@ -7318,7 +7378,7 @@ var init_adapter = __esm({
7318
7378
  }
7319
7379
  async updateDraft(draftId, params) {
7320
7380
  const gmail = this.ensureConnected();
7321
- const raw = this.buildRfc2822(params);
7381
+ const raw = await this.buildRaw(params);
7322
7382
  const res = await gmail.users.drafts.update({
7323
7383
  userId: "me",
7324
7384
  id: draftId,
@@ -7647,30 +7707,15 @@ var init_adapter = __esm({
7647
7707
  const gmail = this.ensureConnected();
7648
7708
  await gmail.users.settings.filters.delete({ userId: "me", id: ruleId });
7649
7709
  }
7650
- buildRfc2822(params) {
7651
- const lines = [];
7652
- lines.push(`From: ${this.email}`);
7653
- lines.push(
7654
- `To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`
7655
- );
7656
- if (params.cc?.length) {
7657
- lines.push(`Cc: ${params.cc.map((c) => c.email).join(", ")}`);
7658
- }
7659
- if (params.bcc?.length) {
7660
- lines.push(`Bcc: ${params.bcc.map((c) => c.email).join(", ")}`);
7661
- }
7662
- lines.push(`Subject: ${params.subject}`);
7663
- if (params.inReplyTo) {
7664
- lines.push(`In-Reply-To: ${params.inReplyTo}`);
7665
- }
7666
- if (params.references?.length) {
7667
- lines.push(`References: ${params.references.join(" ")}`);
7668
- }
7669
- lines.push("MIME-Version: 1.0");
7670
- lines.push("Content-Type: text/plain; charset=UTF-8");
7671
- lines.push("");
7672
- lines.push(params.body.text || params.body.html || "");
7673
- return Buffer.from(lines.join("\r\n")).toString("base64url");
7710
+ /**
7711
+ * Builds the base64url `raw` payload Gmail expects for send, draft create and
7712
+ * draft update: a full MIME message with a text part, an HTML part when one
7713
+ * was given (or a text part derived from HTML-only input), encoded headers
7714
+ * and any attachments. See `buildMimeMessage`.
7715
+ */
7716
+ async buildRaw(params) {
7717
+ const message = await buildMimeMessage(this.email, params);
7718
+ return message.toString("base64url");
7674
7719
  }
7675
7720
  };
7676
7721
  }
@@ -7683,7 +7728,41 @@ function resolveScopeMode(explicit) {
7683
7728
  if (explicit) return explicit;
7684
7729
  return process.env.EMAIL_MCP_GMAIL_SCOPE?.toLowerCase() === "restricted" ? "restricted" : "full";
7685
7730
  }
7686
- var GMAIL_SCOPES, GmailAuth;
7731
+ async function revokeGoogleGrant(token) {
7732
+ if (!token) {
7733
+ return {
7734
+ ok: false,
7735
+ detail: `No Google token was stored for this account, so nothing could be revoked. Remove email-mcp manually at ${GOOGLE_PERMISSIONS_URL}.`
7736
+ };
7737
+ }
7738
+ try {
7739
+ const res = await fetch(GOOGLE_REVOKE_ENDPOINT, {
7740
+ method: "POST",
7741
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
7742
+ body: new URLSearchParams({ token }).toString(),
7743
+ signal: AbortSignal.timeout(REVOKE_TIMEOUT_MS)
7744
+ });
7745
+ if (res.ok) {
7746
+ return { ok: true, detail: "Google revoked the grant: email-mcp no longer has access to this Gmail account." };
7747
+ }
7748
+ let reason = "";
7749
+ try {
7750
+ const body = await res.json();
7751
+ reason = [body.error, body.error_description].filter(Boolean).join(": ");
7752
+ } catch {
7753
+ }
7754
+ return {
7755
+ ok: false,
7756
+ detail: `Google refused the revocation (HTTP ${res.status}${reason ? `, ${reason}` : ""}). The grant may already be revoked or expired. Check and remove email-mcp manually at ${GOOGLE_PERMISSIONS_URL}.`
7757
+ };
7758
+ } catch (err) {
7759
+ return {
7760
+ ok: false,
7761
+ detail: `Could not reach Google's revocation endpoint (${err?.message ?? String(err)}). Remove email-mcp manually at ${GOOGLE_PERMISSIONS_URL}.`
7762
+ };
7763
+ }
7764
+ }
7765
+ var GMAIL_SCOPES, GOOGLE_REVOKE_ENDPOINT, GOOGLE_PERMISSIONS_URL, REVOKE_TIMEOUT_MS, GmailAuth;
7687
7766
  var init_auth = __esm({
7688
7767
  "src/providers/gmail/auth.ts"() {
7689
7768
  "use strict";
@@ -7710,6 +7789,9 @@ var init_auth = __esm({
7710
7789
  "https://www.googleapis.com/auth/gmail.settings.basic"
7711
7790
  ]
7712
7791
  };
7792
+ GOOGLE_REVOKE_ENDPOINT = "https://oauth2.googleapis.com/revoke";
7793
+ GOOGLE_PERMISSIONS_URL = "https://myaccount.google.com/permissions";
7794
+ REVOKE_TIMEOUT_MS = 1e4;
7713
7795
  GmailAuth = class {
7714
7796
  clientId;
7715
7797
  clientSecret;
@@ -8391,31 +8473,82 @@ import {
8391
8473
  PublicClientApplication,
8392
8474
  CryptoProvider
8393
8475
  } from "@azure/msal-node";
8394
- function createFileCachePlugin(cacheFilePath) {
8476
+ function writeEncryptedCache(cacheFilePath, serialized) {
8477
+ const dir = path2.dirname(cacheFilePath);
8478
+ if (!fs2.existsSync(dir)) {
8479
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
8480
+ }
8481
+ const tmpPath = `${cacheFilePath}.${process.pid}.tmp`;
8482
+ fs2.writeFileSync(tmpPath, encryptWithMachineKey(serialized), { mode: 384 });
8483
+ fs2.chmodSync(tmpPath, 384);
8484
+ fs2.renameSync(tmpPath, cacheFilePath);
8485
+ }
8486
+ function loadSerializedCache(cacheFilePath, legacyPlaintextPath) {
8487
+ const hasEncrypted = fs2.existsSync(cacheFilePath);
8488
+ if (fs2.existsSync(legacyPlaintextPath)) {
8489
+ const plaintextIsNewer = !hasEncrypted || fs2.statSync(legacyPlaintextPath).mtimeMs > fs2.statSync(cacheFilePath).mtimeMs;
8490
+ if (plaintextIsNewer) {
8491
+ const serialized = fs2.readFileSync(legacyPlaintextPath, "utf-8");
8492
+ writeEncryptedCache(cacheFilePath, serialized);
8493
+ fs2.unlinkSync(legacyPlaintextPath);
8494
+ return serialized;
8495
+ }
8496
+ fs2.unlinkSync(legacyPlaintextPath);
8497
+ }
8498
+ if (!hasEncrypted) return null;
8499
+ const raw = fs2.readFileSync(cacheFilePath, "utf-8");
8500
+ let plaintext;
8501
+ let usedLegacyKey;
8502
+ try {
8503
+ ({ plaintext, usedLegacyKey } = decryptWithMachineKey(raw));
8504
+ } catch {
8505
+ throw new MsalCacheDecryptError(cacheFilePath);
8506
+ }
8507
+ if (usedLegacyKey) {
8508
+ try {
8509
+ writeEncryptedCache(cacheFilePath, plaintext);
8510
+ } catch {
8511
+ }
8512
+ }
8513
+ return plaintext;
8514
+ }
8515
+ function createEncryptedFileCachePlugin(cacheFilePath, legacyPlaintextPath = path2.join(path2.dirname(cacheFilePath), LEGACY_PLAINTEXT_CACHE_NAME)) {
8516
+ let loadFailed = false;
8395
8517
  return {
8396
8518
  async beforeCacheAccess(context) {
8397
- if (fs2.existsSync(cacheFilePath)) {
8398
- const data = fs2.readFileSync(cacheFilePath, "utf-8");
8399
- context.tokenCache.deserialize(data);
8519
+ loadFailed = false;
8520
+ try {
8521
+ const serialized = loadSerializedCache(cacheFilePath, legacyPlaintextPath);
8522
+ if (serialized !== null) {
8523
+ context.tokenCache.deserialize(serialized);
8524
+ }
8525
+ } catch (err) {
8526
+ loadFailed = true;
8527
+ throw err;
8400
8528
  }
8401
8529
  },
8402
8530
  async afterCacheAccess(context) {
8403
- if (context.cacheHasChanged) {
8404
- const dir = path2.dirname(cacheFilePath);
8405
- if (!fs2.existsSync(dir)) {
8406
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
8407
- }
8408
- fs2.writeFileSync(cacheFilePath, context.tokenCache.serialize(), { mode: 384 });
8409
- }
8531
+ if (loadFailed || !context.cacheHasChanged) return;
8532
+ writeEncryptedCache(cacheFilePath, context.tokenCache.serialize());
8410
8533
  }
8411
8534
  };
8412
8535
  }
8413
- var DEFAULT_CACHE_DIR, DEFAULT_CACHE_FILE, OutlookAuth;
8536
+ var DEFAULT_CACHE_DIR, DEFAULT_CACHE_FILE, LEGACY_PLAINTEXT_CACHE_NAME, MsalCacheDecryptError, OutlookAuth;
8414
8537
  var init_auth2 = __esm({
8415
8538
  "src/providers/outlook/auth.ts"() {
8416
8539
  "use strict";
8540
+ init_credential_store();
8417
8541
  DEFAULT_CACHE_DIR = path2.join(os2.homedir(), ".email-mcp");
8418
- DEFAULT_CACHE_FILE = path2.join(DEFAULT_CACHE_DIR, "msal-cache.json");
8542
+ DEFAULT_CACHE_FILE = path2.join(DEFAULT_CACHE_DIR, "msal-cache.enc");
8543
+ LEGACY_PLAINTEXT_CACHE_NAME = "msal-cache.json";
8544
+ MsalCacheDecryptError = class extends Error {
8545
+ constructor(cacheFilePath) {
8546
+ super(
8547
+ `The Outlook token cache ${cacheFilePath} could not be decrypted: it was modified, is corrupt, or was written with a different key (EMAIL_MCP_KEY or another machine). Delete that file and re-run email-mcp-setup to sign in to Outlook again.`
8548
+ );
8549
+ this.name = "MsalCacheDecryptError";
8550
+ }
8551
+ };
8419
8552
  OutlookAuth = class _OutlookAuth {
8420
8553
  static AUTHORITY = "https://login.microsoftonline.com/consumers";
8421
8554
  // MailboxSettings.ReadWrite is required for the messageRules API (block
@@ -8431,7 +8564,7 @@ var init_auth2 = __esm({
8431
8564
  authority: _OutlookAuth.AUTHORITY
8432
8565
  },
8433
8566
  cache: {
8434
- cachePlugin: createFileCachePlugin(cacheFilePath ?? DEFAULT_CACHE_FILE)
8567
+ cachePlugin: createEncryptedFileCachePlugin(cacheFilePath ?? DEFAULT_CACHE_FILE)
8435
8568
  }
8436
8569
  });
8437
8570
  this.cryptoProvider = new CryptoProvider();
@@ -8485,6 +8618,27 @@ var init_auth2 = __esm({
8485
8618
  homeAccountId: result.account?.homeAccountId ?? homeAccountId
8486
8619
  };
8487
8620
  }
8621
+ /**
8622
+ * Removes one account (its refresh, access and ID tokens) from the MSAL
8623
+ * cache. Matches by home account id, or by username for credentials saved
8624
+ * before the home account id was recorded. Returns whether an entry was
8625
+ * found and removed.
8626
+ */
8627
+ async removeCachedAccount(match) {
8628
+ const cache = this.pca.getTokenCache();
8629
+ let account = null;
8630
+ if (match.homeAccountId) {
8631
+ account = await cache.getAccountByHomeId(match.homeAccountId);
8632
+ }
8633
+ if (!account && match.username) {
8634
+ const wanted = match.username.trim().toLowerCase();
8635
+ const all = await cache.getAllAccounts();
8636
+ account = all.find((a) => (a.username ?? "").trim().toLowerCase() === wanted) ?? null;
8637
+ }
8638
+ if (!account) return false;
8639
+ await cache.removeAccount(account);
8640
+ return true;
8641
+ }
8488
8642
  /**
8489
8643
  * @deprecated Use refreshTokenSilent() instead.
8490
8644
  * Kept for backward compatibility with credentials that have a stored refresh token.
@@ -8586,14 +8740,15 @@ function createSmtpTransport(email3, creds) {
8586
8740
  });
8587
8741
  }
8588
8742
  async function sendViaSmtp(transport, from, params) {
8743
+ const { text, html } = normalizeBody(params.body);
8589
8744
  const result = await transport.sendMail({
8590
8745
  from,
8591
- to: params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", "),
8592
- cc: params.cc?.map((c) => c.email).join(", "),
8593
- bcc: params.bcc?.map((c) => c.email).join(", "),
8746
+ to: params.to.map(toAddress),
8747
+ cc: params.cc?.length ? params.cc.map(toAddress) : void 0,
8748
+ bcc: params.bcc?.length ? params.bcc.map(toAddress) : void 0,
8594
8749
  subject: params.subject,
8595
- text: params.body.text,
8596
- html: params.body.html,
8750
+ text,
8751
+ html,
8597
8752
  inReplyTo: params.inReplyTo,
8598
8753
  references: params.references?.join(" "),
8599
8754
  attachments: params.attachments?.map((a) => ({
@@ -8607,6 +8762,7 @@ async function sendViaSmtp(transport, from, params) {
8607
8762
  var init_smtp = __esm({
8608
8763
  "src/providers/imap/smtp.ts"() {
8609
8764
  "use strict";
8765
+ init_mime();
8610
8766
  }
8611
8767
  });
8612
8768
 
@@ -8639,6 +8795,7 @@ var init_adapter3 = __esm({
8639
8795
  init_types();
8640
8796
  init_mapper3();
8641
8797
  init_smtp();
8798
+ init_mime();
8642
8799
  ImapAdapter = class {
8643
8800
  providerType = ProviderType.IMAP;
8644
8801
  client = null;
@@ -8754,7 +8911,7 @@ var init_adapter3 = __esm({
8754
8911
  const result = await client.mailboxCreate(fullPath);
8755
8912
  return {
8756
8913
  id: result.path,
8757
- name: result.name || name,
8914
+ name,
8758
8915
  path: result.path,
8759
8916
  type: FolderType.Other
8760
8917
  };
@@ -8992,6 +9149,7 @@ var init_adapter3 = __esm({
8992
9149
  const uid = parseInt(id, 10);
8993
9150
  const msg = await client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
8994
9151
  if (!msg) throw new Error(`Email ${id} not found`);
9152
+ if (!msg.source) throw new Error(`Email ${id} has no source`);
8995
9153
  const parsed = await simpleParser(msg.source);
8996
9154
  parsed.flags = msg.flags;
8997
9155
  return mapParsedEmail(parsed, targetFolder, this.accountId, msg.uid);
@@ -9016,6 +9174,7 @@ var init_adapter3 = __esm({
9016
9174
  if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
9017
9175
  const messages = [];
9018
9176
  for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
9177
+ if (!msg.source) continue;
9019
9178
  const parsed = await simpleParser(msg.source);
9020
9179
  parsed.flags = msg.flags;
9021
9180
  messages.push(mapParsedEmail(parsed, "INBOX", this.accountId, msg.uid));
@@ -9051,6 +9210,7 @@ var init_adapter3 = __esm({
9051
9210
  try {
9052
9211
  const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
9053
9212
  if (!msg) throw new Error(`Email ${emailId} not found`);
9213
+ if (!msg.source) throw new Error(`Email ${emailId} has no source`);
9054
9214
  const parsed = await simpleParser(msg.source);
9055
9215
  const attachment = (parsed.attachments || []).find(
9056
9216
  (att) => att.contentId === attachmentId || att.filename === attachmentId
@@ -9075,26 +9235,17 @@ var init_adapter3 = __esm({
9075
9235
  const messageId = await sendViaSmtp(transport, this.email, params);
9076
9236
  return { id: messageId };
9077
9237
  }
9078
- buildDraftRfc2822(params) {
9079
- const lines = [];
9080
- lines.push(`From: ${this.email}`);
9081
- lines.push(`To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`);
9082
- if (params.cc?.length) {
9083
- lines.push(`Cc: ${params.cc.map((c) => c.email).join(", ")}`);
9084
- }
9085
- lines.push(`Subject: ${params.subject}`);
9086
- lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
9087
- if (params.inReplyTo) lines.push(`In-Reply-To: ${params.inReplyTo}`);
9088
- if (params.references?.length) lines.push(`References: ${params.references.join(" ")}`);
9089
- lines.push("MIME-Version: 1.0");
9090
- lines.push("Content-Type: text/plain; charset=utf-8");
9091
- lines.push("");
9092
- lines.push(params.body.text || "");
9093
- return lines.join("\r\n");
9238
+ /**
9239
+ * Builds the full MIME message a draft is stored as: text and HTML parts,
9240
+ * Bcc kept so the draft remembers its recipients, encoded headers and any
9241
+ * attachments. See `buildMimeMessage`.
9242
+ */
9243
+ buildDraftMessage(params) {
9244
+ return buildMimeMessage(this.email, params);
9094
9245
  }
9095
9246
  async createDraft(params) {
9096
9247
  const client = await this.ensureConnected();
9097
- const rawMessage = this.buildDraftRfc2822(params);
9248
+ const rawMessage = await this.buildDraftMessage(params);
9098
9249
  const draftsFolder = await this.resolveFolder("Drafts");
9099
9250
  const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
9100
9251
  if (!result) throw new Error("Failed to append draft");
@@ -9109,6 +9260,7 @@ var init_adapter3 = __esm({
9109
9260
  async updateDraft(draftId, params, sourceFolder) {
9110
9261
  const client = await this.ensureConnected();
9111
9262
  const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
9263
+ const rawMessage = await this.buildDraftMessage(params);
9112
9264
  let lock;
9113
9265
  try {
9114
9266
  lock = await client.getMailboxLock(draftsFolder);
@@ -9120,7 +9272,6 @@ var init_adapter3 = __esm({
9120
9272
  } finally {
9121
9273
  lock.release();
9122
9274
  }
9123
- const rawMessage = this.buildDraftRfc2822(params);
9124
9275
  const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
9125
9276
  if (!result) throw new Error("Failed to append updated draft");
9126
9277
  return { id: String(result.uid || result) };
@@ -9358,16 +9509,16 @@ var init_adapter4 = __esm({
9358
9509
  ICloudAdapter = class extends ImapAdapter {
9359
9510
  providerType = ProviderType.ICloud;
9360
9511
  async connect(credentials) {
9512
+ const passwordDefaults = {
9513
+ host: "imap.mail.me.com",
9514
+ port: 993,
9515
+ tls: true,
9516
+ smtpHost: "smtp.mail.me.com",
9517
+ smtpPort: 587
9518
+ };
9361
9519
  const withDefaults = {
9362
9520
  ...credentials,
9363
- password: {
9364
- host: "imap.mail.me.com",
9365
- port: 993,
9366
- tls: true,
9367
- smtpHost: "smtp.mail.me.com",
9368
- smtpPort: 587,
9369
- ...credentials.password
9370
- }
9521
+ password: { ...passwordDefaults, ...credentials.password }
9371
9522
  };
9372
9523
  return super.connect(withDefaults);
9373
9524
  }
@@ -9401,7 +9552,7 @@ function createProvider(provider) {
9401
9552
  throw new Error(`Unknown provider: ${provider}`);
9402
9553
  }
9403
9554
  }
9404
- var AccountManager;
9555
+ var MICROSOFT_CONSENT_URL, AccountManager;
9405
9556
  var init_account_manager = __esm({
9406
9557
  "src/account-manager.ts"() {
9407
9558
  "use strict";
@@ -9414,6 +9565,7 @@ var init_account_manager = __esm({
9414
9565
  init_adapter3();
9415
9566
  init_types();
9416
9567
  init_oauth_config();
9568
+ MICROSOFT_CONSENT_URL = "https://account.live.com/consent/Manage";
9417
9569
  AccountManager = class {
9418
9570
  store;
9419
9571
  providers = /* @__PURE__ */ new Map();
@@ -9468,9 +9620,55 @@ var init_account_manager = __esm({
9468
9620
  await this.store.save(creds);
9469
9621
  await this.connectAccount(creds.id);
9470
9622
  }
9623
+ /**
9624
+ * Removes an account locally, then cleans up at the provider as far as the
9625
+ * provider allows. Local removal always happens first and never depends on
9626
+ * the network; a failed provider step is reported in the result, not thrown.
9627
+ */
9471
9628
  async removeAccount(accountId) {
9629
+ const creds = await this.store.get(accountId);
9630
+ if (!creds) throw new Error(`Account ${accountId} not found`);
9472
9631
  await this.disconnectAccount(accountId);
9473
9632
  await this.store.remove(accountId);
9633
+ const result = {
9634
+ success: true,
9635
+ accountId,
9636
+ provider: creds.provider,
9637
+ email: creds.email,
9638
+ revocation: { status: "not_applicable", detail: "" }
9639
+ };
9640
+ if (creds.provider === ProviderType.Gmail) {
9641
+ const token = creds.oauth?.refresh_token || creds.oauth?.access_token || "";
9642
+ const { ok, detail } = await revokeGoogleGrant(token);
9643
+ result.revocation = { status: ok ? "revoked" : "failed", detail };
9644
+ } else if (creds.provider === ProviderType.Outlook) {
9645
+ result.tokenCache = await this.removeFromMsalCache(creds);
9646
+ result.revocation = {
9647
+ status: "local_only",
9648
+ detail: `Microsoft offers no endpoint to revoke a refresh token for a personal Microsoft account, so the grant still exists at Microsoft until it expires. To cancel it now, remove email-mcp at ${MICROSOFT_CONSENT_URL}.`
9649
+ };
9650
+ } else {
9651
+ result.revocation = {
9652
+ status: "not_applicable",
9653
+ detail: "This account used a password, not an OAuth grant, so there is nothing to revoke. The password itself stays valid at your provider; if it was an app-specific password (for example from Apple), delete it there if you no longer need it."
9654
+ };
9655
+ }
9656
+ return result;
9657
+ }
9658
+ async removeFromMsalCache(creds) {
9659
+ try {
9660
+ const outlookAuth = new OutlookAuth(OUTLOOK_CLIENT_ID);
9661
+ const removed = await outlookAuth.removeCachedAccount({
9662
+ homeAccountId: creds.oauth?.msal_home_account_id,
9663
+ username: creds.email
9664
+ });
9665
+ return removed ? { status: "removed", detail: "The account's refresh and access tokens were removed from the local Outlook token cache." } : { status: "not_found", detail: "The local Outlook token cache held no tokens for this account." };
9666
+ } catch (err) {
9667
+ return {
9668
+ status: "failed",
9669
+ detail: `The account's tokens could not be removed from the local Outlook token cache: ${err?.message ?? String(err)}`
9670
+ };
9671
+ }
9474
9672
  }
9475
9673
  async disconnectAccount(accountId) {
9476
9674
  const provider = this.providers.get(accountId);
@@ -9671,53 +9869,95 @@ var init_oauth_server = __esm({
9671
9869
  <p>An error occurred during authorization. Please try again.</p>
9672
9870
  </body>
9673
9871
  </html>`;
9674
- OAuthCallbackServer = class {
9675
- server = null;
9872
+ OAuthCallbackServer = class _OAuthCallbackServer {
9873
+ servers = [];
9676
9874
  resolveCode = null;
9677
9875
  rejectCode = null;
9678
9876
  timeoutMs;
9679
9877
  timeoutHandle = null;
9680
9878
  expectedState = null;
9879
+ static IPV4_LOOPBACK = "127.0.0.1";
9880
+ static IPV6_LOOPBACK = "::1";
9881
+ /** Attempts at finding a port that is free on both loopback addresses. */
9882
+ static MAX_PORT_ATTEMPTS = 5;
9681
9883
  constructor(timeoutMs = 12e4) {
9682
9884
  this.timeoutMs = timeoutMs;
9683
9885
  }
9684
- start() {
9886
+ handleRequest = (req, res) => {
9887
+ const url2 = new URL(req.url || "/", `http://localhost`);
9888
+ if (url2.pathname === "/callback" || url2.pathname === "/") {
9889
+ const code = url2.searchParams.get("code");
9890
+ const error48 = url2.searchParams.get("error");
9891
+ const state = url2.searchParams.get("state");
9892
+ if (code && this.expectedState !== null && state !== this.expectedState) {
9893
+ res.writeHead(400, { "Content-Type": "text/html" });
9894
+ res.end(ERROR_HTML);
9895
+ this.rejectCode?.(new Error("OAuth error: state mismatch, callback did not come from the authorization request we started"));
9896
+ } else if (code) {
9897
+ res.writeHead(200, { "Content-Type": "text/html" });
9898
+ res.end(SUCCESS_HTML);
9899
+ this.resolveCode?.(code);
9900
+ } else {
9901
+ res.writeHead(400, { "Content-Type": "text/html" });
9902
+ res.end(ERROR_HTML);
9903
+ this.rejectCode?.(new Error(`OAuth error: ${error48 || "no code received"}`));
9904
+ }
9905
+ } else {
9906
+ res.writeHead(404);
9907
+ res.end("Not Found");
9908
+ }
9909
+ };
9910
+ listenOn(host, port) {
9685
9911
  return new Promise((resolve, reject) => {
9686
- this.server = http.createServer((req, res) => {
9687
- const url2 = new URL(req.url || "/", `http://localhost`);
9688
- if (url2.pathname === "/callback" || url2.pathname === "/") {
9689
- const code = url2.searchParams.get("code");
9690
- const error48 = url2.searchParams.get("error");
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) {
9697
- res.writeHead(200, { "Content-Type": "text/html" });
9698
- res.end(SUCCESS_HTML);
9699
- this.resolveCode?.(code);
9700
- } else {
9701
- res.writeHead(400, { "Content-Type": "text/html" });
9702
- res.end(ERROR_HTML);
9703
- this.rejectCode?.(new Error(`OAuth error: ${error48 || "no code received"}`));
9704
- }
9705
- } else {
9706
- res.writeHead(404);
9707
- res.end("Not Found");
9708
- }
9709
- });
9710
- this.server.listen(0, () => {
9711
- const addr = this.server.address();
9712
- if (addr && typeof addr === "object") {
9713
- resolve(addr.port);
9714
- } else {
9715
- reject(new Error("Failed to get server port"));
9716
- }
9912
+ const server = http.createServer(this.handleRequest);
9913
+ const onError = (err) => {
9914
+ server.close();
9915
+ reject(err);
9916
+ };
9917
+ server.once("error", onError);
9918
+ server.listen(port, host, () => {
9919
+ server.off("error", onError);
9920
+ server.on("error", (err) => this.rejectCode?.(err));
9921
+ resolve(server);
9717
9922
  });
9718
- this.server.on("error", reject);
9719
9923
  });
9720
9924
  }
9925
+ /**
9926
+ * Starts listening on an OS-assigned port on 127.0.0.1 and, when available,
9927
+ * the same port on ::1. Resolves with the port.
9928
+ */
9929
+ async start() {
9930
+ let lastError;
9931
+ for (let attempt = 0; attempt < _OAuthCallbackServer.MAX_PORT_ATTEMPTS; attempt++) {
9932
+ const v4 = await this.listenOn(_OAuthCallbackServer.IPV4_LOOPBACK, 0);
9933
+ const addr = v4.address();
9934
+ if (!addr || typeof addr !== "object") {
9935
+ v4.close();
9936
+ throw new Error("Failed to get server port");
9937
+ }
9938
+ const port = addr.port;
9939
+ try {
9940
+ const v6 = await this.listenOn(_OAuthCallbackServer.IPV6_LOOPBACK, port);
9941
+ this.servers = [v4, v6];
9942
+ return port;
9943
+ } catch (err) {
9944
+ if (err?.code === "EADDRINUSE") {
9945
+ v4.close();
9946
+ lastError = err;
9947
+ continue;
9948
+ }
9949
+ this.servers = [v4];
9950
+ return port;
9951
+ }
9952
+ }
9953
+ throw new Error(
9954
+ `Could not find a port free on both loopback addresses: ${lastError?.message ?? "unknown error"}`
9955
+ );
9956
+ }
9957
+ /** The addresses the callback server is listening on (for diagnostics and tests). */
9958
+ boundAddresses() {
9959
+ return this.servers.map((s) => s.address()).filter((a) => !!a && typeof a === "object").map((a) => a.address);
9960
+ }
9721
9961
  /**
9722
9962
  * Resolve with the authorization code once the browser is redirected back.
9723
9963
  * Pass the `state` that was put into the authorization URL: callbacks whose
@@ -9745,10 +9985,10 @@ var init_oauth_server = __esm({
9745
9985
  clearTimeout(this.timeoutHandle);
9746
9986
  this.timeoutHandle = null;
9747
9987
  }
9748
- if (this.server) {
9749
- this.server.close();
9750
- this.server = null;
9988
+ for (const server of this.servers) {
9989
+ server.close();
9751
9990
  }
9991
+ this.servers = [];
9752
9992
  }
9753
9993
  };
9754
9994
  }
@@ -33327,14 +33567,14 @@ function registerAccountTools(server, accountManager) {
33327
33567
  );
33328
33568
  server.tool(
33329
33569
  "email_remove_account",
33330
- "Remove an email account",
33570
+ "Remove an email account: deletes its stored credentials, revokes the Google grant for Gmail accounts, and removes Outlook tokens from the local token cache. The result reports whether provider-side revocation succeeded.",
33331
33571
  {
33332
33572
  accountId: external_exports3.string().describe("ID of the account to remove")
33333
33573
  },
33334
33574
  async (args) => {
33335
33575
  try {
33336
- await accountManager.removeAccount(args.accountId);
33337
- return jsonResult({ success: true });
33576
+ const result = await accountManager.removeAccount(args.accountId);
33577
+ return jsonResult(result);
33338
33578
  } catch (error48) {
33339
33579
  return jsonResult({ error: error48.message });
33340
33580
  }
@@ -33518,8 +33758,9 @@ function registerReadingTools(server, accountManager) {
33518
33758
  const absPath = resolveDownloadPath(args.outputPath);
33519
33759
  const provider = await accountManager.getProvider(args.accountId);
33520
33760
  const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
33521
- fs3.mkdirSync(path3.dirname(absPath), { recursive: true });
33522
- fs3.writeFileSync(absPath, Buffer.from(data));
33761
+ fs3.mkdirSync(path3.dirname(absPath), { recursive: true, mode: 448 });
33762
+ fs3.writeFileSync(absPath, Buffer.from(data), { mode: 384 });
33763
+ fs3.chmodSync(absPath, 384);
33523
33764
  return jsonResult2({
33524
33765
  path: absPath,
33525
33766
  bytes: data.length,
@@ -34242,11 +34483,12 @@ function registerModerationTools(server, accountManager) {
34242
34483
  }
34243
34484
 
34244
34485
  // src/server.ts
34486
+ var SERVER_VERSION = "1.8.1";
34245
34487
  async function createServer(accountManager) {
34246
34488
  const mgr = accountManager ?? new AccountManager();
34247
34489
  const server = new McpServer({
34248
34490
  name: "email-mcp",
34249
- version: "0.1.0"
34491
+ version: SERVER_VERSION
34250
34492
  });
34251
34493
  registerAccountTools(server, mgr);
34252
34494
  registerReadingTools(server, mgr);