@marlinjai/email-mcp 1.4.4 → 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.
package/dist/index.js CHANGED
@@ -35,18 +35,62 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
35
35
 
36
36
  // src/auth/credential-store.ts
37
37
  import crypto from "node:crypto";
38
+ import { execFileSync } from "node:child_process";
38
39
  import fs from "node:fs";
39
40
  import path from "node:path";
40
41
  import os from "node:os";
41
- function getMachineSeed() {
42
- return `email-mcp:${os.hostname()}:${os.userInfo().username}`;
42
+ function getStableMachineId() {
43
+ if (cachedMachineId !== void 0) return cachedMachineId;
44
+ cachedMachineId = computeStableMachineId();
45
+ return cachedMachineId;
43
46
  }
44
- function deriveKey(salt) {
45
- return crypto.pbkdf2Sync(getMachineSeed(), salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
47
+ function computeStableMachineId() {
48
+ try {
49
+ if (process.platform === "darwin") {
50
+ const out = execFileSync("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
51
+ encoding: "utf-8"
52
+ });
53
+ const match = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
54
+ return match ? match[1] : null;
55
+ }
56
+ if (process.platform === "linux") {
57
+ for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
58
+ if (fs.existsSync(p)) {
59
+ const id = fs.readFileSync(p, "utf-8").trim();
60
+ if (id) return id;
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+ if (process.platform === "win32") {
66
+ const out = execFileSync(
67
+ "reg",
68
+ ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"],
69
+ { encoding: "utf-8" }
70
+ );
71
+ const match = out.match(/MachineGuid\s+REG_SZ\s+([A-Za-z0-9-]+)/);
72
+ return match ? match[1] : null;
73
+ }
74
+ } catch {
75
+ }
76
+ return null;
46
77
  }
47
- function encrypt(plaintext) {
78
+ function getSeedCandidates() {
79
+ const seeds = [];
80
+ const username = os.userInfo().username;
81
+ const envKey = process.env.EMAIL_MCP_KEY;
82
+ if (envKey) seeds.push(`email-mcp:env:${envKey}`);
83
+ const machineId = getStableMachineId();
84
+ if (machineId) seeds.push(`email-mcp:machine:${machineId}:${username}`);
85
+ seeds.push(`email-mcp:${os.hostname()}:${username}`);
86
+ return seeds;
87
+ }
88
+ function deriveKey(seed, salt) {
89
+ return crypto.pbkdf2Sync(seed, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
90
+ }
91
+ function encrypt(plaintext, seed) {
48
92
  const salt = crypto.randomBytes(SALT_LENGTH);
49
- const key = deriveKey(salt);
93
+ const key = deriveKey(seed, salt);
50
94
  const iv = crypto.randomBytes(IV_LENGTH);
51
95
  const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
52
96
  let encrypted = cipher.update(plaintext, "utf-8", "hex");
@@ -59,16 +103,27 @@ function encrypt(plaintext) {
59
103
  data: encrypted
60
104
  });
61
105
  }
62
- function decrypt(ciphertext) {
106
+ function decrypt(ciphertext, seeds) {
63
107
  const { salt, iv, authTag, data } = JSON.parse(ciphertext);
64
- const key = deriveKey(Buffer.from(salt, "hex"));
65
- const decipher = crypto.createDecipheriv(ALGORITHM, key, Buffer.from(iv, "hex"));
66
- decipher.setAuthTag(Buffer.from(authTag, "hex"));
67
- let decrypted = decipher.update(data, "hex", "utf-8");
68
- decrypted += decipher.final("utf-8");
69
- return decrypted;
70
- }
71
- var ALGORITHM, KEY_LENGTH, IV_LENGTH, SALT_LENGTH, PBKDF2_ITERATIONS, CredentialStore;
108
+ const saltBuf = Buffer.from(salt, "hex");
109
+ const ivBuf = Buffer.from(iv, "hex");
110
+ const authTagBuf = Buffer.from(authTag, "hex");
111
+ let lastError;
112
+ for (const seed of seeds) {
113
+ try {
114
+ const key = deriveKey(seed, saltBuf);
115
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, ivBuf);
116
+ decipher.setAuthTag(authTagBuf);
117
+ let decrypted = decipher.update(data, "hex", "utf-8");
118
+ decrypted += decipher.final("utf-8");
119
+ return { plaintext: decrypted, seedUsed: seed };
120
+ } catch (err) {
121
+ lastError = err;
122
+ }
123
+ }
124
+ throw lastError ?? new Error("Unable to decrypt credentials");
125
+ }
126
+ var ALGORITHM, KEY_LENGTH, IV_LENGTH, SALT_LENGTH, PBKDF2_ITERATIONS, cachedMachineId, CredentialStore;
72
127
  var init_credential_store = __esm({
73
128
  "src/auth/credential-store.ts"() {
74
129
  "use strict";
@@ -91,12 +146,20 @@ var init_credential_store = __esm({
91
146
  return { accounts: {} };
92
147
  }
93
148
  const raw = fs.readFileSync(this.filePath, "utf-8");
94
- const json2 = decrypt(raw);
95
- return JSON.parse(json2);
149
+ const seeds = getSeedCandidates();
150
+ const { plaintext, seedUsed } = decrypt(raw, seeds);
151
+ const data = JSON.parse(plaintext);
152
+ if (seedUsed !== seeds[0]) {
153
+ try {
154
+ this.write(data);
155
+ } catch {
156
+ }
157
+ }
158
+ return data;
96
159
  }
97
160
  write(data) {
98
161
  const json2 = JSON.stringify(data);
99
- const encrypted = encrypt(json2);
162
+ const encrypted = encrypt(json2, getSeedCandidates()[0]);
100
163
  fs.writeFileSync(this.filePath, encrypted, { mode: 384 });
101
164
  }
102
165
  async save(creds) {
@@ -3314,8 +3377,8 @@ var require_utils = __commonJS({
3314
3377
  }
3315
3378
  return ind;
3316
3379
  }
3317
- function removeDotSegments(path3) {
3318
- let input2 = path3;
3380
+ function removeDotSegments(path4) {
3381
+ let input2 = path4;
3319
3382
  const output2 = [];
3320
3383
  let nextSlash = -1;
3321
3384
  let len = 0;
@@ -3514,8 +3577,8 @@ var require_schemes = __commonJS({
3514
3577
  wsComponent.secure = void 0;
3515
3578
  }
3516
3579
  if (wsComponent.resourceName) {
3517
- const [path3, query] = wsComponent.resourceName.split("?");
3518
- wsComponent.path = path3 && path3 !== "/" ? path3 : void 0;
3580
+ const [path4, query] = wsComponent.resourceName.split("?");
3581
+ wsComponent.path = path4 && path4 !== "/" ? path4 : void 0;
3519
3582
  wsComponent.query = query;
3520
3583
  wsComponent.resourceName = void 0;
3521
3584
  }
@@ -6877,12 +6940,12 @@ var require_dist = __commonJS({
6877
6940
  throw new Error(`Unknown format "${name}"`);
6878
6941
  return f;
6879
6942
  };
6880
- function addFormats(ajv, list, fs3, exportName) {
6943
+ function addFormats(ajv, list, fs4, exportName) {
6881
6944
  var _a2;
6882
6945
  var _b;
6883
6946
  (_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== void 0 ? _a2 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
6884
6947
  for (const f of list)
6885
- ajv.addFormat(f, fs3[f]);
6948
+ ajv.addFormat(f, fs4[f]);
6886
6949
  }
6887
6950
  module.exports = exports = formatsPlugin;
6888
6951
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -7119,11 +7182,12 @@ var init_adapter = __esm({
7119
7182
  const labels = res.data.labels || [];
7120
7183
  return labels.map(mapGmailLabel);
7121
7184
  }
7122
- async createFolder(name) {
7185
+ async createFolder(name, parentPath) {
7123
7186
  const gmail = this.ensureConnected();
7187
+ const fullName = parentPath ? `${parentPath}/${name}` : name;
7124
7188
  const res = await gmail.users.labels.create({
7125
7189
  userId: "me",
7126
- requestBody: { name }
7190
+ requestBody: { name: fullName }
7127
7191
  });
7128
7192
  return mapGmailLabel(res.data);
7129
7193
  }
@@ -7240,6 +7304,18 @@ var init_adapter = __esm({
7240
7304
  });
7241
7305
  return { id: res.data.id || "" };
7242
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
+ }
7243
7319
  async listDrafts(limit, _offset) {
7244
7320
  const gmail = this.ensureConnected();
7245
7321
  const res = await gmail.users.drafts.list({
@@ -7403,6 +7479,34 @@ var init_adapter = __esm({
7403
7479
  }
7404
7480
  return result;
7405
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
+ }
7406
7510
  async getRawMessage(emailId, _sourceFolder) {
7407
7511
  const gmail = this.ensureConnected();
7408
7512
  const res = await gmail.users.messages.get({
@@ -7563,18 +7667,37 @@ var init_adapter = __esm({
7563
7667
  // src/providers/gmail/auth.ts
7564
7668
  import { OAuth2Client as OAuth2Client2 } from "google-auth-library";
7565
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
+ }
7566
7674
  var GMAIL_SCOPES, GmailAuth;
7567
7675
  var init_auth = __esm({
7568
7676
  "src/providers/gmail/auth.ts"() {
7569
7677
  "use strict";
7570
- GMAIL_SCOPES = [
7571
- "https://mail.google.com/",
7572
- "https://www.googleapis.com/auth/gmail.modify",
7573
- // Mailbox content access (above) does NOT cover the Settings API —
7574
- // Google treats them as separate permission domains. Needed for
7575
- // users.settings.filters.* (email_create_block_rule/list/delete).
7576
- "https://www.googleapis.com/auth/gmail.settings.basic"
7577
- ];
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
+ };
7578
7701
  GmailAuth = class {
7579
7702
  clientId;
7580
7703
  clientSecret;
@@ -7584,13 +7707,13 @@ var init_auth = __esm({
7584
7707
  this.clientSecret = clientSecret;
7585
7708
  this.oauth2Client = new OAuth2Client2(clientId, clientSecret, redirectUri);
7586
7709
  }
7587
- getAuthUrl(redirectUri) {
7710
+ getAuthUrl(redirectUri, scopeMode) {
7588
7711
  const codeVerifier = crypto2.randomBytes(32).toString("base64url");
7589
7712
  const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
7590
7713
  this.oauth2Client = new OAuth2Client2(this.clientId, this.clientSecret, redirectUri);
7591
7714
  const url2 = this.oauth2Client.generateAuthUrl({
7592
7715
  access_type: "offline",
7593
- scope: GMAIL_SCOPES,
7716
+ scope: GMAIL_SCOPES[resolveScopeMode(scopeMode)],
7594
7717
  prompt: "consent",
7595
7718
  code_challenge: codeChallenge,
7596
7719
  code_challenge_method: "S256"
@@ -7805,8 +7928,8 @@ var init_adapter2 = __esm({
7805
7928
  }
7806
7929
  });
7807
7930
  const originalApi = client.api.bind(client);
7808
- client.api = (path3) => {
7809
- return originalApi(path3).header("Prefer", 'IdType="ImmutableId"');
7931
+ client.api = (path4) => {
7932
+ return originalApi(path4).header("Prefer", 'IdType="ImmutableId"');
7810
7933
  };
7811
7934
  this.client = client;
7812
7935
  }
@@ -7947,6 +8070,12 @@ var init_adapter2 = __esm({
7947
8070
  const result = await client.api("/me/messages").post(message);
7948
8071
  return { id: result.id };
7949
8072
  }
8073
+ async updateDraft(draftId, params) {
8074
+ const client = this.ensureClient();
8075
+ const message = this.buildGraphMessage(params);
8076
+ await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(message);
8077
+ return { id: draftId };
8078
+ }
7950
8079
  async listDrafts(limit, offset) {
7951
8080
  const client = this.ensureClient();
7952
8081
  let request = client.api("/me/mailFolders/drafts/messages");
@@ -8499,6 +8628,7 @@ var init_adapter3 = __esm({
8499
8628
  accountId = "";
8500
8629
  email = "";
8501
8630
  passwordCreds = null;
8631
+ connectingPromise = null;
8502
8632
  async connect(credentials) {
8503
8633
  if (!credentials.password) {
8504
8634
  throw new Error("IMAP adapter requires password credentials");
@@ -8506,23 +8636,87 @@ var init_adapter3 = __esm({
8506
8636
  this.accountId = credentials.id;
8507
8637
  this.email = credentials.email;
8508
8638
  this.passwordCreds = credentials.password;
8509
- this.client = new ImapFlow({
8510
- host: credentials.password.host,
8511
- port: credentials.password.port,
8512
- secure: credentials.password.tls,
8513
- auth: {
8514
- user: credentials.email,
8515
- pass: credentials.password.password
8516
- },
8517
- logger: false
8518
- });
8519
- await this.client.connect();
8639
+ await this.ensureConnected();
8520
8640
  }
8521
8641
  async disconnect() {
8522
8642
  if (this.client) {
8523
- await this.client.logout();
8643
+ try {
8644
+ await this.client.logout();
8645
+ } catch {
8646
+ }
8524
8647
  this.client = null;
8525
8648
  }
8649
+ this.passwordCreds = null;
8650
+ }
8651
+ /**
8652
+ * Builds a fresh ImapFlow client from stored credentials and wires up
8653
+ * close/error listeners so a dropped socket clears `this.client`, forcing
8654
+ * the next `ensureConnected()` call to reconnect.
8655
+ */
8656
+ buildClient() {
8657
+ if (!this.passwordCreds) {
8658
+ throw new Error("Cannot build IMAP client without stored credentials");
8659
+ }
8660
+ const client = new ImapFlow({
8661
+ host: this.passwordCreds.host,
8662
+ port: this.passwordCreds.port,
8663
+ secure: this.passwordCreds.tls,
8664
+ auth: { user: this.email, pass: this.passwordCreds.password },
8665
+ logger: false
8666
+ });
8667
+ const drop = () => {
8668
+ if (this.client === client) this.client = null;
8669
+ };
8670
+ client.on("close", drop);
8671
+ client.on("error", drop);
8672
+ return client;
8673
+ }
8674
+ /**
8675
+ * Returns true while the underlying ImapFlow socket is healthy.
8676
+ * ImapFlow sets `usable=false` when the connection is closed, in the
8677
+ * middle of logging out, or otherwise unsafe to issue commands on.
8678
+ */
8679
+ isClientUsable() {
8680
+ return !!this.client && this.client.usable === true;
8681
+ }
8682
+ /**
8683
+ * Guarantees a usable IMAP client and returns it. Reconnects transparently
8684
+ * if the previous client was dropped (idle timeout, server-side disconnect,
8685
+ * network flap). Concurrent callers share a single in-flight reconnect to
8686
+ * avoid duplicate sessions.
8687
+ *
8688
+ * Callers should bind the result to a local `const client` and use that
8689
+ * within the method instead of `this.client`, so a `'close'` event mid-call
8690
+ * does not race the active operation.
8691
+ */
8692
+ async ensureConnected() {
8693
+ if (this.connectingPromise) {
8694
+ await this.connectingPromise;
8695
+ }
8696
+ if (!this.isClientUsable()) {
8697
+ if (!this.passwordCreds) {
8698
+ throw new Error("Not connected");
8699
+ }
8700
+ this.connectingPromise = (async () => {
8701
+ if (this.client) {
8702
+ try {
8703
+ await this.client.logout();
8704
+ } catch {
8705
+ }
8706
+ this.client = null;
8707
+ }
8708
+ const client = this.buildClient();
8709
+ await client.connect();
8710
+ this.client = client;
8711
+ })().finally(() => {
8712
+ this.connectingPromise = null;
8713
+ });
8714
+ await this.connectingPromise;
8715
+ }
8716
+ if (!this.client) {
8717
+ throw new Error("IMAP client unavailable after reconnect attempt");
8718
+ }
8719
+ return this.client;
8526
8720
  }
8527
8721
  async testConnection() {
8528
8722
  try {
@@ -8533,14 +8727,14 @@ var init_adapter3 = __esm({
8533
8727
  }
8534
8728
  }
8535
8729
  async listFolders() {
8536
- if (!this.client) throw new Error("Not connected");
8537
- const imapFolders = await this.client.list();
8730
+ const client = await this.ensureConnected();
8731
+ const imapFolders = await client.list();
8538
8732
  return imapFolders.map(mapImapFolder);
8539
8733
  }
8540
8734
  async createFolder(name, parentPath) {
8541
- if (!this.client) throw new Error("Not connected");
8735
+ const client = await this.ensureConnected();
8542
8736
  const fullPath = parentPath ? `${parentPath}/${name}` : name;
8543
- const result = await this.client.mailboxCreate(fullPath);
8737
+ const result = await client.mailboxCreate(fullPath);
8544
8738
  return {
8545
8739
  id: result.path,
8546
8740
  name: result.name || name,
@@ -8569,7 +8763,7 @@ var init_adapter3 = __esm({
8569
8763
  * and matching by name or special-use type.
8570
8764
  */
8571
8765
  async resolveFolder(folder) {
8572
- if (!this.client) throw new Error("Not connected");
8766
+ const client = await this.ensureConnected();
8573
8767
  const FOLDER_ALIASES = {
8574
8768
  junk: ["Junk", "Spam", "Bulk Mail", "Junk E-mail", "Junk Email"],
8575
8769
  spam: ["Junk", "Spam", "Bulk Mail", "Junk E-mail", "Junk Email"],
@@ -8578,7 +8772,7 @@ var init_adapter3 = __esm({
8578
8772
  drafts: ["Drafts", "Draft"],
8579
8773
  archive: ["Archive", "All Mail"]
8580
8774
  };
8581
- const folders = await this.client.list();
8775
+ const folders = await client.list();
8582
8776
  const lowerFolder = folder.toLowerCase();
8583
8777
  const exactMatch = folders.find(
8584
8778
  (f) => f.path === folder || f.path.toLowerCase() === lowerFolder
@@ -8616,7 +8810,7 @@ var init_adapter3 = __esm({
8616
8810
  return folder;
8617
8811
  }
8618
8812
  async search(query) {
8619
- if (!this.client) throw new Error("Not connected");
8813
+ const client = await this.ensureConnected();
8620
8814
  const requestedFolder = query.folder || "INBOX";
8621
8815
  let folder;
8622
8816
  try {
@@ -8625,17 +8819,25 @@ var init_adapter3 = __esm({
8625
8819
  folder = requestedFolder;
8626
8820
  }
8627
8821
  try {
8628
- await this.client.noop();
8822
+ await client.noop();
8629
8823
  } catch {
8630
8824
  }
8631
8825
  let lock;
8632
8826
  try {
8633
- lock = await this.client.getMailboxLock(folder);
8827
+ lock = await client.getMailboxLock(folder);
8634
8828
  } catch (error48) {
8635
8829
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
8636
8830
  }
8637
8831
  try {
8638
- const mailboxExists = this.client.mailbox?.exists ?? 0;
8832
+ let mailboxExists = client.mailbox?.exists ?? 0;
8833
+ if (mailboxExists === 0) {
8834
+ try {
8835
+ const status = await client.status(folder, { messages: true });
8836
+ mailboxExists = status?.messages ?? 0;
8837
+ } catch {
8838
+ mailboxExists = 0;
8839
+ }
8840
+ }
8639
8841
  if (mailboxExists === 0) {
8640
8842
  return [];
8641
8843
  }
@@ -8643,14 +8845,14 @@ var init_adapter3 = __esm({
8643
8845
  const hasCriteria = Object.keys(criteria).length > 0;
8644
8846
  let allUids = [];
8645
8847
  try {
8646
- const searchResult = await this.client.search(
8848
+ const searchResult = await client.search(
8647
8849
  hasCriteria ? criteria : { all: true },
8648
8850
  { uid: true }
8649
8851
  );
8650
8852
  allUids = Array.isArray(searchResult) ? searchResult : [];
8651
8853
  } catch {
8652
8854
  try {
8653
- allUids = await this.collectUidsViaFetch(query);
8855
+ allUids = await this.collectUidsViaFetch(client, query);
8654
8856
  } catch {
8655
8857
  return [];
8656
8858
  }
@@ -8658,7 +8860,7 @@ var init_adapter3 = __esm({
8658
8860
  const offset = query.offset || 0;
8659
8861
  const slicedUids = query.limit ? allUids.slice(offset, offset + query.limit) : allUids.slice(offset);
8660
8862
  if (slicedUids.length === 0) return [];
8661
- return await this.fetchEmails(slicedUids, folder, query.returnBody);
8863
+ return await this.fetchEmails(client, slicedUids, folder, query.returnBody);
8662
8864
  } catch (error48) {
8663
8865
  throw formatImapError(error48, `Search failed in folder "${folder}"`);
8664
8866
  } finally {
@@ -8672,13 +8874,12 @@ var init_adapter3 = __esm({
8672
8874
  * 2. FETCH 1:N using mailbox.exists as explicit range
8673
8875
  * 3. Individual sequence number fetches (slowest but most resilient)
8674
8876
  */
8675
- async collectUidsViaFetch(query) {
8676
- if (!this.client) return [];
8877
+ async collectUidsViaFetch(client, query) {
8677
8878
  const uids = [];
8678
- const exists = this.client.mailbox?.exists ?? 0;
8879
+ const exists = client.mailbox?.exists ?? 0;
8679
8880
  const safeFetch = async (range) => {
8680
8881
  try {
8681
- const messages = await this.client.fetchAll(range, { uid: true, flags: true });
8882
+ const messages = await client.fetchAll(range, { uid: true, flags: true });
8682
8883
  for (const msg of messages) {
8683
8884
  if (query.unreadOnly && msg.flags?.has("\\Seen")) continue;
8684
8885
  if (query.starredOnly && !msg.flags?.has("\\Flagged")) continue;
@@ -8698,7 +8899,7 @@ var init_adapter3 = __esm({
8698
8899
  const count = exists > 0 ? exists : 50;
8699
8900
  for (let seq = 1; seq <= count; seq++) {
8700
8901
  try {
8701
- const msgs = await this.client.fetchAll(String(seq), { uid: true, flags: true });
8902
+ const msgs = await client.fetchAll(String(seq), { uid: true, flags: true });
8702
8903
  for (const msg of msgs) {
8703
8904
  if (query.unreadOnly && msg.flags?.has("\\Seen")) continue;
8704
8905
  if (query.starredOnly && !msg.flags?.has("\\Flagged")) continue;
@@ -8712,19 +8913,18 @@ var init_adapter3 = __esm({
8712
8913
  /**
8713
8914
  * Fetches email data for a set of UIDs, either with full body or lightweight headers.
8714
8915
  */
8715
- async fetchEmails(uids, folder, returnBody) {
8716
- if (!this.client) return [];
8916
+ async fetchEmails(client, uids, folder, returnBody) {
8717
8917
  const emails = [];
8718
8918
  const fetchOpts = returnBody ? { source: true, uid: true, flags: true } : { envelope: true, uid: true, flags: true, bodyStructure: true };
8719
8919
  const uidRange = uids.join(",");
8720
8920
  let messages;
8721
8921
  try {
8722
- messages = await this.client.fetchAll(uidRange, fetchOpts, { uid: true });
8922
+ messages = await client.fetchAll(uidRange, fetchOpts, { uid: true });
8723
8923
  } catch {
8724
8924
  messages = [];
8725
8925
  for (const uid of uids) {
8726
8926
  try {
8727
- const batch = await this.client.fetchAll(String(uid), fetchOpts, { uid: true });
8927
+ const batch = await client.fetchAll(String(uid), fetchOpts, { uid: true });
8728
8928
  messages.push(...batch);
8729
8929
  } catch {
8730
8930
  }
@@ -8763,17 +8963,17 @@ var init_adapter3 = __esm({
8763
8963
  return emails;
8764
8964
  }
8765
8965
  async getEmail(id, folder) {
8766
- if (!this.client) throw new Error("Not connected");
8966
+ const client = await this.ensureConnected();
8767
8967
  const targetFolder = folder || "INBOX";
8768
8968
  let lock;
8769
8969
  try {
8770
- lock = await this.client.getMailboxLock(targetFolder);
8970
+ lock = await client.getMailboxLock(targetFolder);
8771
8971
  } catch (error48) {
8772
8972
  throw formatImapError(error48, `Failed to open folder "${targetFolder}"`);
8773
8973
  }
8774
8974
  try {
8775
8975
  const uid = parseInt(id, 10);
8776
- const msg = await this.client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
8976
+ const msg = await client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
8777
8977
  if (!msg) throw new Error(`Email ${id} not found`);
8778
8978
  const parsed = await simpleParser(msg.source);
8779
8979
  parsed.flags = msg.flags;
@@ -8783,22 +8983,22 @@ var init_adapter3 = __esm({
8783
8983
  }
8784
8984
  }
8785
8985
  async getThread(threadId) {
8786
- if (!this.client) throw new Error("Not connected");
8986
+ const client = await this.ensureConnected();
8787
8987
  let lock;
8788
8988
  try {
8789
- lock = await this.client.getMailboxLock("INBOX");
8989
+ lock = await client.getMailboxLock("INBOX");
8790
8990
  } catch (error48) {
8791
8991
  throw formatImapError(error48, 'Failed to open folder "INBOX"');
8792
8992
  }
8793
8993
  try {
8794
- const searchResult = await this.client.search(
8994
+ const searchResult = await client.search(
8795
8995
  { or: [{ header: { "message-id": threadId } }, { header: { references: threadId } }, { header: { "in-reply-to": threadId } }] },
8796
8996
  { uid: true }
8797
8997
  );
8798
8998
  const uids = Array.isArray(searchResult) ? searchResult : [];
8799
8999
  if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
8800
9000
  const messages = [];
8801
- for await (const msg of this.client.fetch(uids, { source: true, uid: true, flags: true })) {
9001
+ for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
8802
9002
  const parsed = await simpleParser(msg.source);
8803
9003
  parsed.flags = msg.flags;
8804
9004
  messages.push(mapParsedEmail(parsed, "INBOX", this.accountId, msg.uid));
@@ -8824,15 +9024,15 @@ var init_adapter3 = __esm({
8824
9024
  }
8825
9025
  }
8826
9026
  async getAttachment(emailId, attachmentId) {
8827
- if (!this.client) throw new Error("Not connected");
9027
+ const client = await this.ensureConnected();
8828
9028
  let lock;
8829
9029
  try {
8830
- lock = await this.client.getMailboxLock("INBOX");
9030
+ lock = await client.getMailboxLock("INBOX");
8831
9031
  } catch (error48) {
8832
9032
  throw formatImapError(error48, 'Failed to open folder "INBOX"');
8833
9033
  }
8834
9034
  try {
8835
- const msg = await this.client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
9035
+ const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
8836
9036
  if (!msg) throw new Error(`Email ${emailId} not found`);
8837
9037
  const parsed = await simpleParser(msg.source);
8838
9038
  const attachment = (parsed.attachments || []).find(
@@ -8858,8 +9058,7 @@ var init_adapter3 = __esm({
8858
9058
  const messageId = await sendViaSmtp(transport, this.email, params);
8859
9059
  return { id: messageId };
8860
9060
  }
8861
- async createDraft(params) {
8862
- if (!this.client) throw new Error("Not connected");
9061
+ buildDraftRfc2822(params) {
8863
9062
  const lines = [];
8864
9063
  lines.push(`From: ${this.email}`);
8865
9064
  lines.push(`To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`);
@@ -8874,8 +9073,39 @@ var init_adapter3 = __esm({
8874
9073
  lines.push("Content-Type: text/plain; charset=utf-8");
8875
9074
  lines.push("");
8876
9075
  lines.push(params.body.text || "");
8877
- const rawMessage = lines.join("\r\n");
8878
- const result = await this.client.append("Drafts", rawMessage, ["\\Draft", "\\Seen"]);
9076
+ return lines.join("\r\n");
9077
+ }
9078
+ async createDraft(params) {
9079
+ const client = await this.ensureConnected();
9080
+ const rawMessage = this.buildDraftRfc2822(params);
9081
+ const draftsFolder = await this.resolveFolder("Drafts");
9082
+ const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
9083
+ if (!result) throw new Error("Failed to append draft");
9084
+ return { id: String(result.uid || result) };
9085
+ }
9086
+ /**
9087
+ * IMAP has no in-place message update — a draft is just a message in the
9088
+ * Drafts folder, and IMAP messages are immutable once appended. This
9089
+ * deletes the old revision and appends a new one, so the returned `id`
9090
+ * is a NEW uid; the old id no longer resolves after this call.
9091
+ */
9092
+ async updateDraft(draftId, params, sourceFolder) {
9093
+ const client = await this.ensureConnected();
9094
+ const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
9095
+ let lock;
9096
+ try {
9097
+ lock = await client.getMailboxLock(draftsFolder);
9098
+ } catch (error48) {
9099
+ throw formatImapError(error48, `Failed to open folder "${draftsFolder}"`);
9100
+ }
9101
+ try {
9102
+ await client.messageDelete(draftId, { uid: true });
9103
+ } finally {
9104
+ lock.release();
9105
+ }
9106
+ const rawMessage = this.buildDraftRfc2822(params);
9107
+ const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
9108
+ if (!result) throw new Error("Failed to append updated draft");
8879
9109
  return { id: String(result.uid || result) };
8880
9110
  }
8881
9111
  async listDrafts(limit, offset) {
@@ -8886,16 +9116,16 @@ var init_adapter3 = __esm({
8886
9116
  });
8887
9117
  }
8888
9118
  async moveEmail(emailId, targetFolder, sourceFolder) {
8889
- if (!this.client) throw new Error("Not connected");
9119
+ const client = await this.ensureConnected();
8890
9120
  const folder = sourceFolder || "INBOX";
8891
9121
  let lock;
8892
9122
  try {
8893
- lock = await this.client.getMailboxLock(folder);
9123
+ lock = await client.getMailboxLock(folder);
8894
9124
  } catch (error48) {
8895
9125
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
8896
9126
  }
8897
9127
  try {
8898
- await this.client.messageMove(emailId, targetFolder, { uid: true });
9128
+ await client.messageMove(emailId, targetFolder, { uid: true });
8899
9129
  } finally {
8900
9130
  lock.release();
8901
9131
  }
@@ -8925,44 +9155,44 @@ var init_adapter3 = __esm({
8925
9155
  }
8926
9156
  }
8927
9157
  async deleteEmail(emailId, permanent, sourceFolder) {
8928
- if (!this.client) throw new Error("Not connected");
9158
+ const client = await this.ensureConnected();
8929
9159
  const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
8930
9160
  const trashFolder = await this.resolveFolder("Trash");
8931
9161
  let lock;
8932
9162
  try {
8933
- lock = await this.client.getMailboxLock(folder);
9163
+ lock = await client.getMailboxLock(folder);
8934
9164
  } catch (error48) {
8935
9165
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
8936
9166
  }
8937
9167
  try {
8938
9168
  if (permanent || folder === trashFolder) {
8939
- await this.client.messageDelete(emailId, { uid: true });
9169
+ await client.messageDelete(emailId, { uid: true });
8940
9170
  } else {
8941
- await this.client.messageMove(emailId, trashFolder, { uid: true });
9171
+ await client.messageMove(emailId, trashFolder, { uid: true });
8942
9172
  }
8943
9173
  } finally {
8944
9174
  lock.release();
8945
9175
  }
8946
9176
  }
8947
9177
  async markEmail(emailId, flags, sourceFolder) {
8948
- if (!this.client) throw new Error("Not connected");
9178
+ const client = await this.ensureConnected();
8949
9179
  const folder = sourceFolder || "INBOX";
8950
9180
  let lock;
8951
9181
  try {
8952
- lock = await this.client.getMailboxLock(folder);
9182
+ lock = await client.getMailboxLock(folder);
8953
9183
  } catch (error48) {
8954
9184
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
8955
9185
  }
8956
9186
  try {
8957
9187
  if (flags.read === true) {
8958
- await this.client.messageFlagsAdd(emailId, ["\\Seen"], { uid: true });
9188
+ await client.messageFlagsAdd(emailId, ["\\Seen"], { uid: true });
8959
9189
  } else if (flags.read === false) {
8960
- await this.client.messageFlagsRemove(emailId, ["\\Seen"], { uid: true });
9190
+ await client.messageFlagsRemove(emailId, ["\\Seen"], { uid: true });
8961
9191
  }
8962
9192
  if (flags.starred === true || flags.flagged === true) {
8963
- await this.client.messageFlagsAdd(emailId, ["\\Flagged"], { uid: true });
9193
+ await client.messageFlagsAdd(emailId, ["\\Flagged"], { uid: true });
8964
9194
  } else if (flags.starred === false || flags.flagged === false) {
8965
- await this.client.messageFlagsRemove(emailId, ["\\Flagged"], { uid: true });
9195
+ await client.messageFlagsRemove(emailId, ["\\Flagged"], { uid: true });
8966
9196
  }
8967
9197
  } finally {
8968
9198
  lock.release();
@@ -8995,32 +9225,32 @@ var init_adapter3 = __esm({
8995
9225
  return { id: String(result?.uid ?? "") };
8996
9226
  }
8997
9227
  async batchDelete(emailIds, permanent, sourceFolder) {
8998
- if (!this.client) throw new Error("Not connected");
9228
+ const client = await this.ensureConnected();
8999
9229
  const result = { succeeded: [], failed: [] };
9000
9230
  const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
9001
9231
  const trashFolder = await this.resolveFolder("Trash");
9002
9232
  const shouldPermanentDelete = permanent || folder === trashFolder;
9003
9233
  let lock;
9004
9234
  try {
9005
- lock = await this.client.getMailboxLock(folder);
9235
+ lock = await client.getMailboxLock(folder);
9006
9236
  } catch (error48) {
9007
9237
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
9008
9238
  }
9009
9239
  try {
9010
9240
  const uidRange = emailIds.join(",");
9011
9241
  if (shouldPermanentDelete) {
9012
- await this.client.messageDelete(uidRange, { uid: true });
9242
+ await client.messageDelete(uidRange, { uid: true });
9013
9243
  } else {
9014
- await this.client.messageMove(uidRange, trashFolder, { uid: true });
9244
+ await client.messageMove(uidRange, trashFolder, { uid: true });
9015
9245
  }
9016
9246
  result.succeeded = [...emailIds];
9017
9247
  } catch (error48) {
9018
9248
  for (const id of emailIds) {
9019
9249
  try {
9020
9250
  if (shouldPermanentDelete) {
9021
- await this.client.messageDelete(id, { uid: true });
9251
+ await client.messageDelete(id, { uid: true });
9022
9252
  } else {
9023
- await this.client.messageMove(id, trashFolder, { uid: true });
9253
+ await client.messageMove(id, trashFolder, { uid: true });
9024
9254
  }
9025
9255
  result.succeeded.push(id);
9026
9256
  } catch (e) {
@@ -9033,23 +9263,23 @@ var init_adapter3 = __esm({
9033
9263
  return result;
9034
9264
  }
9035
9265
  async batchMove(emailIds, targetFolder, sourceFolder) {
9036
- if (!this.client) throw new Error("Not connected");
9266
+ const client = await this.ensureConnected();
9037
9267
  const result = { succeeded: [], failed: [] };
9038
9268
  const folder = sourceFolder || "INBOX";
9039
9269
  let lock;
9040
9270
  try {
9041
- lock = await this.client.getMailboxLock(folder);
9271
+ lock = await client.getMailboxLock(folder);
9042
9272
  } catch (error48) {
9043
9273
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
9044
9274
  }
9045
9275
  try {
9046
9276
  const uidRange = emailIds.join(",");
9047
- await this.client.messageMove(uidRange, targetFolder, { uid: true });
9277
+ await client.messageMove(uidRange, targetFolder, { uid: true });
9048
9278
  result.succeeded = [...emailIds];
9049
9279
  } catch (error48) {
9050
9280
  for (const id of emailIds) {
9051
9281
  try {
9052
- await this.client.messageMove(id, targetFolder, { uid: true });
9282
+ await client.messageMove(id, targetFolder, { uid: true });
9053
9283
  result.succeeded.push(id);
9054
9284
  } catch (e) {
9055
9285
  result.failed.push({ id, error: e.message });
@@ -9061,26 +9291,26 @@ var init_adapter3 = __esm({
9061
9291
  return result;
9062
9292
  }
9063
9293
  async batchMark(emailIds, flags, sourceFolder) {
9064
- if (!this.client) throw new Error("Not connected");
9294
+ const client = await this.ensureConnected();
9065
9295
  const result = { succeeded: [], failed: [] };
9066
9296
  const folder = sourceFolder || "INBOX";
9067
9297
  let lock;
9068
9298
  try {
9069
- lock = await this.client.getMailboxLock(folder);
9299
+ lock = await client.getMailboxLock(folder);
9070
9300
  } catch (error48) {
9071
9301
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
9072
9302
  }
9073
9303
  try {
9074
9304
  const uidRange = emailIds.join(",");
9075
9305
  if (flags.read === true) {
9076
- await this.client.messageFlagsAdd(uidRange, ["\\Seen"], { uid: true });
9306
+ await client.messageFlagsAdd(uidRange, ["\\Seen"], { uid: true });
9077
9307
  } else if (flags.read === false) {
9078
- await this.client.messageFlagsRemove(uidRange, ["\\Seen"], { uid: true });
9308
+ await client.messageFlagsRemove(uidRange, ["\\Seen"], { uid: true });
9079
9309
  }
9080
9310
  if (flags.starred === true || flags.flagged === true) {
9081
- await this.client.messageFlagsAdd(uidRange, ["\\Flagged"], { uid: true });
9311
+ await client.messageFlagsAdd(uidRange, ["\\Flagged"], { uid: true });
9082
9312
  } else if (flags.starred === false || flags.flagged === false) {
9083
- await this.client.messageFlagsRemove(uidRange, ["\\Flagged"], { uid: true });
9313
+ await client.messageFlagsRemove(uidRange, ["\\Flagged"], { uid: true });
9084
9314
  }
9085
9315
  result.succeeded = [...emailIds];
9086
9316
  } catch (error48) {
@@ -9133,9 +9363,9 @@ var GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, OUTLOOK_CLIENT_ID;
9133
9363
  var init_oauth_config = __esm({
9134
9364
  "src/oauth-config.ts"() {
9135
9365
  "use strict";
9136
- GMAIL_CLIENT_ID = "1062266096576-lu2ukplhc3lcm0bvheml5i18urptdp4t.apps.googleusercontent.com";
9137
- GMAIL_CLIENT_SECRET = "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";
9138
- OUTLOOK_CLIENT_ID = "76d32225-ef10-4b49-951c-c80ae906595d";
9366
+ GMAIL_CLIENT_ID = process.env.EMAIL_MCP_GMAIL_CLIENT_ID || "1062266096576-lu2ukplhc3lcm0bvheml5i18urptdp4t.apps.googleusercontent.com";
9367
+ GMAIL_CLIENT_SECRET = process.env.EMAIL_MCP_GMAIL_CLIENT_SECRET || "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";
9368
+ OUTLOOK_CLIENT_ID = process.env.EMAIL_MCP_OUTLOOK_CLIENT_ID || "76d32225-ef10-4b49-951c-c80ae906595d";
9139
9369
  }
9140
9370
  });
9141
9371
 
@@ -9541,14 +9771,30 @@ async function testAndSave(creds) {
9541
9771
  async function promptAccountName(defaultName) {
9542
9772
  return askText("Account name:", defaultName);
9543
9773
  }
9544
- async function setupGmail() {
9774
+ async function setupGmail(scopeModeFlag) {
9545
9775
  console.log("\n--- Gmail Setup ---");
9776
+ if (process.env.EMAIL_MCP_GMAIL_CLIENT_ID) {
9777
+ console.log("Using your own Google OAuth Client ID (EMAIL_MCP_GMAIL_CLIENT_ID).");
9778
+ }
9546
9779
  console.log("A browser window will open for Google authorization.\n");
9780
+ let scopeMode = scopeModeFlag;
9781
+ if (!scopeMode) {
9782
+ scopeMode = await askChoice("Gmail permission scope:", [
9783
+ {
9784
+ name: "Full (default) \u2014 includes permanent delete, bypassing Trash",
9785
+ value: "full"
9786
+ },
9787
+ {
9788
+ name: "Restricted \u2014 read/send/label/archive/trash only, no permanent delete",
9789
+ value: "restricted"
9790
+ }
9791
+ ]);
9792
+ }
9547
9793
  const server = new OAuthCallbackServer();
9548
9794
  const port = await server.start();
9549
9795
  const redirectUri = `http://localhost:${port}/callback`;
9550
9796
  const gmailAuth = new GmailAuth(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, redirectUri);
9551
- const { url: url2, codeVerifier } = gmailAuth.getAuthUrl(redirectUri);
9797
+ const { url: url2, codeVerifier } = gmailAuth.getAuthUrl(redirectUri, scopeMode);
9552
9798
  console.log("\nOpening browser for Google authorization...");
9553
9799
  console.log(`If the browser does not open, visit:
9554
9800
  ${url2}
@@ -9747,6 +9993,17 @@ async function main() {
9747
9993
  await removeAccount(id);
9748
9994
  return;
9749
9995
  }
9996
+ const scopeIdx = args.indexOf("--scope");
9997
+ let scopeModeFlag;
9998
+ if (scopeIdx !== -1) {
9999
+ const value = args[scopeIdx + 1];
10000
+ if (value === "full" || value === "restricted") {
10001
+ scopeModeFlag = value;
10002
+ } else {
10003
+ console.error("Usage: --scope <full|restricted>");
10004
+ process.exit(1);
10005
+ }
10006
+ }
9750
10007
  console.log("@marlinjai/email-mcp Account Setup\n");
9751
10008
  let addMore = true;
9752
10009
  while (addMore) {
@@ -9758,7 +10015,7 @@ async function main() {
9758
10015
  ]);
9759
10016
  switch (provider) {
9760
10017
  case ProviderType.Gmail:
9761
- await setupGmail();
10018
+ await setupGmail(scopeModeFlag);
9762
10019
  break;
9763
10020
  case ProviderType.Outlook:
9764
10021
  await setupOutlook();
@@ -10164,8 +10421,8 @@ function getErrorMap() {
10164
10421
 
10165
10422
  // node_modules/zod/v3/helpers/parseUtil.js
10166
10423
  var makeIssue = (params) => {
10167
- const { data, path: path3, errorMaps, issueData } = params;
10168
- const fullPath = [...path3, ...issueData.path || []];
10424
+ const { data, path: path4, errorMaps, issueData } = params;
10425
+ const fullPath = [...path4, ...issueData.path || []];
10169
10426
  const fullIssue = {
10170
10427
  ...issueData,
10171
10428
  path: fullPath
@@ -10280,11 +10537,11 @@ var errorUtil;
10280
10537
 
10281
10538
  // node_modules/zod/v3/types.js
10282
10539
  var ParseInputLazyPath = class {
10283
- constructor(parent, value, path3, key) {
10540
+ constructor(parent, value, path4, key) {
10284
10541
  this._cachedPath = [];
10285
10542
  this.parent = parent;
10286
10543
  this.data = value;
10287
- this._path = path3;
10544
+ this._path = path4;
10288
10545
  this._key = key;
10289
10546
  }
10290
10547
  get path() {
@@ -14207,10 +14464,10 @@ function mergeDefs(...defs) {
14207
14464
  function cloneDef(schema) {
14208
14465
  return mergeDefs(schema._zod.def);
14209
14466
  }
14210
- function getElementAtPath(obj, path3) {
14211
- if (!path3)
14467
+ function getElementAtPath(obj, path4) {
14468
+ if (!path4)
14212
14469
  return obj;
14213
- return path3.reduce((acc, key) => acc?.[key], obj);
14470
+ return path4.reduce((acc, key) => acc?.[key], obj);
14214
14471
  }
14215
14472
  function promiseAllObject(promisesObj) {
14216
14473
  const keys = Object.keys(promisesObj);
@@ -14593,11 +14850,11 @@ function aborted(x, startIndex = 0) {
14593
14850
  }
14594
14851
  return false;
14595
14852
  }
14596
- function prefixIssues(path3, issues) {
14853
+ function prefixIssues(path4, issues) {
14597
14854
  return issues.map((iss) => {
14598
14855
  var _a2;
14599
14856
  (_a2 = iss).path ?? (_a2.path = []);
14600
- iss.path.unshift(path3);
14857
+ iss.path.unshift(path4);
14601
14858
  return iss;
14602
14859
  });
14603
14860
  }
@@ -14780,7 +15037,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
14780
15037
  }
14781
15038
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
14782
15039
  const result = { errors: [] };
14783
- const processError = (error49, path3 = []) => {
15040
+ const processError = (error49, path4 = []) => {
14784
15041
  var _a2, _b;
14785
15042
  for (const issue2 of error49.issues) {
14786
15043
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -14790,7 +15047,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
14790
15047
  } else if (issue2.code === "invalid_element") {
14791
15048
  processError({ issues: issue2.issues }, issue2.path);
14792
15049
  } else {
14793
- const fullpath = [...path3, ...issue2.path];
15050
+ const fullpath = [...path4, ...issue2.path];
14794
15051
  if (fullpath.length === 0) {
14795
15052
  result.errors.push(mapper(issue2));
14796
15053
  continue;
@@ -14822,8 +15079,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
14822
15079
  }
14823
15080
  function toDotPath(_path) {
14824
15081
  const segs = [];
14825
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
14826
- for (const seg of path3) {
15082
+ const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
15083
+ for (const seg of path4) {
14827
15084
  if (typeof seg === "number")
14828
15085
  segs.push(`[${seg}]`);
14829
15086
  else if (typeof seg === "symbol")
@@ -27229,13 +27486,13 @@ function resolveRef(ref, ctx) {
27229
27486
  if (!ref.startsWith("#")) {
27230
27487
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
27231
27488
  }
27232
- const path3 = ref.slice(1).split("/").filter(Boolean);
27233
- if (path3.length === 0) {
27489
+ const path4 = ref.slice(1).split("/").filter(Boolean);
27490
+ if (path4.length === 0) {
27234
27491
  return ctx.rootSchema;
27235
27492
  }
27236
27493
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
27237
- if (path3[0] === defsKey) {
27238
- const key = path3[1];
27494
+ if (path4[0] === defsKey) {
27495
+ const key = path4[1];
27239
27496
  if (!key || !ctx.defs[key]) {
27240
27497
  throw new Error(`Reference not found: ${ref}`);
27241
27498
  }
@@ -33068,9 +33325,28 @@ function registerAccountTools(server, accountManager) {
33068
33325
  }
33069
33326
 
33070
33327
  // src/tools/reading.ts
33328
+ import fs3 from "node:fs";
33329
+ import os3 from "node:os";
33330
+ import path3 from "node:path";
33071
33331
  function jsonResult2(data) {
33072
33332
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
33073
33333
  }
33334
+ function getDownloadsBaseDir() {
33335
+ return path3.resolve(
33336
+ process.env.EMAIL_MCP_DOWNLOADS_DIR || path3.join(os3.homedir(), ".email-mcp", "downloads")
33337
+ );
33338
+ }
33339
+ function resolveDownloadPath(outputPath) {
33340
+ const baseDir = getDownloadsBaseDir();
33341
+ const resolved = path3.resolve(baseDir, outputPath);
33342
+ const relative = path3.relative(baseDir, resolved);
33343
+ if (relative.startsWith("..") || path3.isAbsolute(relative)) {
33344
+ throw new Error(
33345
+ `outputPath must resolve inside ${baseDir} (set EMAIL_MCP_DOWNLOADS_DIR to change the base directory)`
33346
+ );
33347
+ }
33348
+ return resolved;
33349
+ }
33074
33350
  function stripBodies(emails) {
33075
33351
  return emails.map((email3) => ({
33076
33352
  ...email3,
@@ -33195,6 +33471,33 @@ function registerReadingTools(server, accountManager) {
33195
33471
  }
33196
33472
  }
33197
33473
  );
33474
+ server.tool(
33475
+ "email_save_attachment",
33476
+ `Download an email attachment and save it directly to disk, returning metadata only (no base64 in the response) \u2014 avoids paying the token cost of round-tripping large attachments through the calling model. outputPath is relative to a fixed downloads directory (${getDownloadsBaseDir()}, override with EMAIL_MCP_DOWNLOADS_DIR) and cannot escape it.`,
33477
+ {
33478
+ accountId: external_exports3.string(),
33479
+ emailId: external_exports3.string(),
33480
+ attachmentId: external_exports3.string(),
33481
+ outputPath: external_exports3.string().describe("Relative file path (within the downloads directory) to save the attachment as")
33482
+ },
33483
+ async (args) => {
33484
+ try {
33485
+ const absPath = resolveDownloadPath(args.outputPath);
33486
+ const provider = await accountManager.getProvider(args.accountId);
33487
+ const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
33488
+ fs3.mkdirSync(path3.dirname(absPath), { recursive: true });
33489
+ fs3.writeFileSync(absPath, Buffer.from(data));
33490
+ return jsonResult2({
33491
+ path: absPath,
33492
+ bytes: data.length,
33493
+ filename: meta3.filename,
33494
+ mimeType: meta3.contentType
33495
+ });
33496
+ } catch (error48) {
33497
+ return jsonResult2({ error: error48.message });
33498
+ }
33499
+ }
33500
+ );
33198
33501
  }
33199
33502
 
33200
33503
  // src/tools/sending.ts
@@ -33356,6 +33659,32 @@ function registerSendingTools(server, accountManager) {
33356
33659
  }
33357
33660
  }
33358
33661
  );
33662
+ server.tool(
33663
+ "email_draft_update",
33664
+ "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.",
33665
+ {
33666
+ accountId: external_exports3.string(),
33667
+ draftId: external_exports3.string(),
33668
+ to: external_exports3.array(ContactSchema),
33669
+ subject: external_exports3.string(),
33670
+ body: BodySchema,
33671
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when the draft is not in the default Drafts folder)")
33672
+ },
33673
+ async (args) => {
33674
+ try {
33675
+ const provider = await accountManager.getProvider(args.accountId);
33676
+ const params = {
33677
+ to: args.to,
33678
+ subject: args.subject,
33679
+ body: args.body
33680
+ };
33681
+ const result = await provider.updateDraft(args.draftId, params, args.sourceFolder);
33682
+ return jsonResult3(result);
33683
+ } catch (error48) {
33684
+ return jsonResult3({ error: error48.message });
33685
+ }
33686
+ }
33687
+ );
33359
33688
  server.tool(
33360
33689
  "email_draft_list",
33361
33690
  "List email drafts",
@@ -33543,6 +33872,45 @@ function registerOrganizingTools(server, accountManager) {
33543
33872
  }
33544
33873
  }
33545
33874
  );
33875
+ server.tool(
33876
+ "email_batch_label",
33877
+ "Add or remove labels on multiple emails at once (Gmail only). Much faster than individual email_label calls.",
33878
+ {
33879
+ accountId: external_exports3.string(),
33880
+ emailIds: external_exports3.array(external_exports3.string()).min(1).describe("Array of email IDs to label"),
33881
+ addLabels: external_exports3.array(external_exports3.string()).optional(),
33882
+ removeLabels: external_exports3.array(external_exports3.string()).optional()
33883
+ },
33884
+ async (args) => {
33885
+ try {
33886
+ const provider = await accountManager.getProvider(args.accountId);
33887
+ if (!provider.addLabels || !provider.removeLabels) {
33888
+ return jsonResult4({
33889
+ success: false,
33890
+ error: "email_batch_label is only supported on Gmail accounts",
33891
+ supportedProviders: ["gmail"]
33892
+ });
33893
+ }
33894
+ if (provider.batchLabel) {
33895
+ const result2 = await provider.batchLabel(args.emailIds, args.addLabels, args.removeLabels);
33896
+ return jsonResult4({ success: true, ...result2 });
33897
+ }
33898
+ const result = { succeeded: [], failed: [] };
33899
+ for (const id of args.emailIds) {
33900
+ try {
33901
+ if (args.addLabels && args.addLabels.length > 0) await provider.addLabels(id, args.addLabels);
33902
+ if (args.removeLabels && args.removeLabels.length > 0) await provider.removeLabels(id, args.removeLabels);
33903
+ result.succeeded.push(id);
33904
+ } catch (e) {
33905
+ result.failed.push({ id, error: e.message });
33906
+ }
33907
+ }
33908
+ return jsonResult4({ success: true, ...result });
33909
+ } catch (error48) {
33910
+ return jsonResult4({ success: false, error: error48.message });
33911
+ }
33912
+ }
33913
+ );
33546
33914
  server.tool(
33547
33915
  "email_transfer",
33548
33916
  "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.",