@marlinjai/email-mcp 1.4.4 → 1.5.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;
77
+ }
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");
46
90
  }
47
- function encrypt(plaintext) {
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 });
@@ -7805,8 +7868,8 @@ var init_adapter2 = __esm({
7805
7868
  }
7806
7869
  });
7807
7870
  const originalApi = client.api.bind(client);
7808
- client.api = (path3) => {
7809
- return originalApi(path3).header("Prefer", 'IdType="ImmutableId"');
7871
+ client.api = (path4) => {
7872
+ return originalApi(path4).header("Prefer", 'IdType="ImmutableId"');
7810
7873
  };
7811
7874
  this.client = client;
7812
7875
  }
@@ -8499,6 +8562,7 @@ var init_adapter3 = __esm({
8499
8562
  accountId = "";
8500
8563
  email = "";
8501
8564
  passwordCreds = null;
8565
+ connectingPromise = null;
8502
8566
  async connect(credentials) {
8503
8567
  if (!credentials.password) {
8504
8568
  throw new Error("IMAP adapter requires password credentials");
@@ -8506,23 +8570,87 @@ var init_adapter3 = __esm({
8506
8570
  this.accountId = credentials.id;
8507
8571
  this.email = credentials.email;
8508
8572
  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();
8573
+ await this.ensureConnected();
8520
8574
  }
8521
8575
  async disconnect() {
8522
8576
  if (this.client) {
8523
- await this.client.logout();
8577
+ try {
8578
+ await this.client.logout();
8579
+ } catch {
8580
+ }
8524
8581
  this.client = null;
8525
8582
  }
8583
+ this.passwordCreds = null;
8584
+ }
8585
+ /**
8586
+ * Builds a fresh ImapFlow client from stored credentials and wires up
8587
+ * close/error listeners so a dropped socket clears `this.client`, forcing
8588
+ * the next `ensureConnected()` call to reconnect.
8589
+ */
8590
+ buildClient() {
8591
+ if (!this.passwordCreds) {
8592
+ throw new Error("Cannot build IMAP client without stored credentials");
8593
+ }
8594
+ const client = new ImapFlow({
8595
+ host: this.passwordCreds.host,
8596
+ port: this.passwordCreds.port,
8597
+ secure: this.passwordCreds.tls,
8598
+ auth: { user: this.email, pass: this.passwordCreds.password },
8599
+ logger: false
8600
+ });
8601
+ const drop = () => {
8602
+ if (this.client === client) this.client = null;
8603
+ };
8604
+ client.on("close", drop);
8605
+ client.on("error", drop);
8606
+ return client;
8607
+ }
8608
+ /**
8609
+ * Returns true while the underlying ImapFlow socket is healthy.
8610
+ * ImapFlow sets `usable=false` when the connection is closed, in the
8611
+ * middle of logging out, or otherwise unsafe to issue commands on.
8612
+ */
8613
+ isClientUsable() {
8614
+ return !!this.client && this.client.usable === true;
8615
+ }
8616
+ /**
8617
+ * Guarantees a usable IMAP client and returns it. Reconnects transparently
8618
+ * if the previous client was dropped (idle timeout, server-side disconnect,
8619
+ * network flap). Concurrent callers share a single in-flight reconnect to
8620
+ * avoid duplicate sessions.
8621
+ *
8622
+ * Callers should bind the result to a local `const client` and use that
8623
+ * within the method instead of `this.client`, so a `'close'` event mid-call
8624
+ * does not race the active operation.
8625
+ */
8626
+ async ensureConnected() {
8627
+ if (this.connectingPromise) {
8628
+ await this.connectingPromise;
8629
+ }
8630
+ if (!this.isClientUsable()) {
8631
+ if (!this.passwordCreds) {
8632
+ throw new Error("Not connected");
8633
+ }
8634
+ this.connectingPromise = (async () => {
8635
+ if (this.client) {
8636
+ try {
8637
+ await this.client.logout();
8638
+ } catch {
8639
+ }
8640
+ this.client = null;
8641
+ }
8642
+ const client = this.buildClient();
8643
+ await client.connect();
8644
+ this.client = client;
8645
+ })().finally(() => {
8646
+ this.connectingPromise = null;
8647
+ });
8648
+ await this.connectingPromise;
8649
+ }
8650
+ if (!this.client) {
8651
+ throw new Error("IMAP client unavailable after reconnect attempt");
8652
+ }
8653
+ return this.client;
8526
8654
  }
8527
8655
  async testConnection() {
8528
8656
  try {
@@ -8533,14 +8661,14 @@ var init_adapter3 = __esm({
8533
8661
  }
8534
8662
  }
8535
8663
  async listFolders() {
8536
- if (!this.client) throw new Error("Not connected");
8537
- const imapFolders = await this.client.list();
8664
+ const client = await this.ensureConnected();
8665
+ const imapFolders = await client.list();
8538
8666
  return imapFolders.map(mapImapFolder);
8539
8667
  }
8540
8668
  async createFolder(name, parentPath) {
8541
- if (!this.client) throw new Error("Not connected");
8669
+ const client = await this.ensureConnected();
8542
8670
  const fullPath = parentPath ? `${parentPath}/${name}` : name;
8543
- const result = await this.client.mailboxCreate(fullPath);
8671
+ const result = await client.mailboxCreate(fullPath);
8544
8672
  return {
8545
8673
  id: result.path,
8546
8674
  name: result.name || name,
@@ -8569,7 +8697,7 @@ var init_adapter3 = __esm({
8569
8697
  * and matching by name or special-use type.
8570
8698
  */
8571
8699
  async resolveFolder(folder) {
8572
- if (!this.client) throw new Error("Not connected");
8700
+ const client = await this.ensureConnected();
8573
8701
  const FOLDER_ALIASES = {
8574
8702
  junk: ["Junk", "Spam", "Bulk Mail", "Junk E-mail", "Junk Email"],
8575
8703
  spam: ["Junk", "Spam", "Bulk Mail", "Junk E-mail", "Junk Email"],
@@ -8578,7 +8706,7 @@ var init_adapter3 = __esm({
8578
8706
  drafts: ["Drafts", "Draft"],
8579
8707
  archive: ["Archive", "All Mail"]
8580
8708
  };
8581
- const folders = await this.client.list();
8709
+ const folders = await client.list();
8582
8710
  const lowerFolder = folder.toLowerCase();
8583
8711
  const exactMatch = folders.find(
8584
8712
  (f) => f.path === folder || f.path.toLowerCase() === lowerFolder
@@ -8616,7 +8744,7 @@ var init_adapter3 = __esm({
8616
8744
  return folder;
8617
8745
  }
8618
8746
  async search(query) {
8619
- if (!this.client) throw new Error("Not connected");
8747
+ const client = await this.ensureConnected();
8620
8748
  const requestedFolder = query.folder || "INBOX";
8621
8749
  let folder;
8622
8750
  try {
@@ -8625,17 +8753,25 @@ var init_adapter3 = __esm({
8625
8753
  folder = requestedFolder;
8626
8754
  }
8627
8755
  try {
8628
- await this.client.noop();
8756
+ await client.noop();
8629
8757
  } catch {
8630
8758
  }
8631
8759
  let lock;
8632
8760
  try {
8633
- lock = await this.client.getMailboxLock(folder);
8761
+ lock = await client.getMailboxLock(folder);
8634
8762
  } catch (error48) {
8635
8763
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
8636
8764
  }
8637
8765
  try {
8638
- const mailboxExists = this.client.mailbox?.exists ?? 0;
8766
+ let mailboxExists = client.mailbox?.exists ?? 0;
8767
+ if (mailboxExists === 0) {
8768
+ try {
8769
+ const status = await client.status(folder, { messages: true });
8770
+ mailboxExists = status?.messages ?? 0;
8771
+ } catch {
8772
+ mailboxExists = 0;
8773
+ }
8774
+ }
8639
8775
  if (mailboxExists === 0) {
8640
8776
  return [];
8641
8777
  }
@@ -8643,14 +8779,14 @@ var init_adapter3 = __esm({
8643
8779
  const hasCriteria = Object.keys(criteria).length > 0;
8644
8780
  let allUids = [];
8645
8781
  try {
8646
- const searchResult = await this.client.search(
8782
+ const searchResult = await client.search(
8647
8783
  hasCriteria ? criteria : { all: true },
8648
8784
  { uid: true }
8649
8785
  );
8650
8786
  allUids = Array.isArray(searchResult) ? searchResult : [];
8651
8787
  } catch {
8652
8788
  try {
8653
- allUids = await this.collectUidsViaFetch(query);
8789
+ allUids = await this.collectUidsViaFetch(client, query);
8654
8790
  } catch {
8655
8791
  return [];
8656
8792
  }
@@ -8658,7 +8794,7 @@ var init_adapter3 = __esm({
8658
8794
  const offset = query.offset || 0;
8659
8795
  const slicedUids = query.limit ? allUids.slice(offset, offset + query.limit) : allUids.slice(offset);
8660
8796
  if (slicedUids.length === 0) return [];
8661
- return await this.fetchEmails(slicedUids, folder, query.returnBody);
8797
+ return await this.fetchEmails(client, slicedUids, folder, query.returnBody);
8662
8798
  } catch (error48) {
8663
8799
  throw formatImapError(error48, `Search failed in folder "${folder}"`);
8664
8800
  } finally {
@@ -8672,13 +8808,12 @@ var init_adapter3 = __esm({
8672
8808
  * 2. FETCH 1:N using mailbox.exists as explicit range
8673
8809
  * 3. Individual sequence number fetches (slowest but most resilient)
8674
8810
  */
8675
- async collectUidsViaFetch(query) {
8676
- if (!this.client) return [];
8811
+ async collectUidsViaFetch(client, query) {
8677
8812
  const uids = [];
8678
- const exists = this.client.mailbox?.exists ?? 0;
8813
+ const exists = client.mailbox?.exists ?? 0;
8679
8814
  const safeFetch = async (range) => {
8680
8815
  try {
8681
- const messages = await this.client.fetchAll(range, { uid: true, flags: true });
8816
+ const messages = await client.fetchAll(range, { uid: true, flags: true });
8682
8817
  for (const msg of messages) {
8683
8818
  if (query.unreadOnly && msg.flags?.has("\\Seen")) continue;
8684
8819
  if (query.starredOnly && !msg.flags?.has("\\Flagged")) continue;
@@ -8698,7 +8833,7 @@ var init_adapter3 = __esm({
8698
8833
  const count = exists > 0 ? exists : 50;
8699
8834
  for (let seq = 1; seq <= count; seq++) {
8700
8835
  try {
8701
- const msgs = await this.client.fetchAll(String(seq), { uid: true, flags: true });
8836
+ const msgs = await client.fetchAll(String(seq), { uid: true, flags: true });
8702
8837
  for (const msg of msgs) {
8703
8838
  if (query.unreadOnly && msg.flags?.has("\\Seen")) continue;
8704
8839
  if (query.starredOnly && !msg.flags?.has("\\Flagged")) continue;
@@ -8712,19 +8847,18 @@ var init_adapter3 = __esm({
8712
8847
  /**
8713
8848
  * Fetches email data for a set of UIDs, either with full body or lightweight headers.
8714
8849
  */
8715
- async fetchEmails(uids, folder, returnBody) {
8716
- if (!this.client) return [];
8850
+ async fetchEmails(client, uids, folder, returnBody) {
8717
8851
  const emails = [];
8718
8852
  const fetchOpts = returnBody ? { source: true, uid: true, flags: true } : { envelope: true, uid: true, flags: true, bodyStructure: true };
8719
8853
  const uidRange = uids.join(",");
8720
8854
  let messages;
8721
8855
  try {
8722
- messages = await this.client.fetchAll(uidRange, fetchOpts, { uid: true });
8856
+ messages = await client.fetchAll(uidRange, fetchOpts, { uid: true });
8723
8857
  } catch {
8724
8858
  messages = [];
8725
8859
  for (const uid of uids) {
8726
8860
  try {
8727
- const batch = await this.client.fetchAll(String(uid), fetchOpts, { uid: true });
8861
+ const batch = await client.fetchAll(String(uid), fetchOpts, { uid: true });
8728
8862
  messages.push(...batch);
8729
8863
  } catch {
8730
8864
  }
@@ -8763,17 +8897,17 @@ var init_adapter3 = __esm({
8763
8897
  return emails;
8764
8898
  }
8765
8899
  async getEmail(id, folder) {
8766
- if (!this.client) throw new Error("Not connected");
8900
+ const client = await this.ensureConnected();
8767
8901
  const targetFolder = folder || "INBOX";
8768
8902
  let lock;
8769
8903
  try {
8770
- lock = await this.client.getMailboxLock(targetFolder);
8904
+ lock = await client.getMailboxLock(targetFolder);
8771
8905
  } catch (error48) {
8772
8906
  throw formatImapError(error48, `Failed to open folder "${targetFolder}"`);
8773
8907
  }
8774
8908
  try {
8775
8909
  const uid = parseInt(id, 10);
8776
- const msg = await this.client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
8910
+ const msg = await client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
8777
8911
  if (!msg) throw new Error(`Email ${id} not found`);
8778
8912
  const parsed = await simpleParser(msg.source);
8779
8913
  parsed.flags = msg.flags;
@@ -8783,22 +8917,22 @@ var init_adapter3 = __esm({
8783
8917
  }
8784
8918
  }
8785
8919
  async getThread(threadId) {
8786
- if (!this.client) throw new Error("Not connected");
8920
+ const client = await this.ensureConnected();
8787
8921
  let lock;
8788
8922
  try {
8789
- lock = await this.client.getMailboxLock("INBOX");
8923
+ lock = await client.getMailboxLock("INBOX");
8790
8924
  } catch (error48) {
8791
8925
  throw formatImapError(error48, 'Failed to open folder "INBOX"');
8792
8926
  }
8793
8927
  try {
8794
- const searchResult = await this.client.search(
8928
+ const searchResult = await client.search(
8795
8929
  { or: [{ header: { "message-id": threadId } }, { header: { references: threadId } }, { header: { "in-reply-to": threadId } }] },
8796
8930
  { uid: true }
8797
8931
  );
8798
8932
  const uids = Array.isArray(searchResult) ? searchResult : [];
8799
8933
  if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
8800
8934
  const messages = [];
8801
- for await (const msg of this.client.fetch(uids, { source: true, uid: true, flags: true })) {
8935
+ for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
8802
8936
  const parsed = await simpleParser(msg.source);
8803
8937
  parsed.flags = msg.flags;
8804
8938
  messages.push(mapParsedEmail(parsed, "INBOX", this.accountId, msg.uid));
@@ -8824,15 +8958,15 @@ var init_adapter3 = __esm({
8824
8958
  }
8825
8959
  }
8826
8960
  async getAttachment(emailId, attachmentId) {
8827
- if (!this.client) throw new Error("Not connected");
8961
+ const client = await this.ensureConnected();
8828
8962
  let lock;
8829
8963
  try {
8830
- lock = await this.client.getMailboxLock("INBOX");
8964
+ lock = await client.getMailboxLock("INBOX");
8831
8965
  } catch (error48) {
8832
8966
  throw formatImapError(error48, 'Failed to open folder "INBOX"');
8833
8967
  }
8834
8968
  try {
8835
- const msg = await this.client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
8969
+ const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
8836
8970
  if (!msg) throw new Error(`Email ${emailId} not found`);
8837
8971
  const parsed = await simpleParser(msg.source);
8838
8972
  const attachment = (parsed.attachments || []).find(
@@ -8859,7 +8993,7 @@ var init_adapter3 = __esm({
8859
8993
  return { id: messageId };
8860
8994
  }
8861
8995
  async createDraft(params) {
8862
- if (!this.client) throw new Error("Not connected");
8996
+ const client = await this.ensureConnected();
8863
8997
  const lines = [];
8864
8998
  lines.push(`From: ${this.email}`);
8865
8999
  lines.push(`To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`);
@@ -8875,7 +9009,9 @@ var init_adapter3 = __esm({
8875
9009
  lines.push("");
8876
9010
  lines.push(params.body.text || "");
8877
9011
  const rawMessage = lines.join("\r\n");
8878
- const result = await this.client.append("Drafts", rawMessage, ["\\Draft", "\\Seen"]);
9012
+ const draftsFolder = await this.resolveFolder("Drafts");
9013
+ const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
9014
+ if (!result) throw new Error("Failed to append draft");
8879
9015
  return { id: String(result.uid || result) };
8880
9016
  }
8881
9017
  async listDrafts(limit, offset) {
@@ -8886,16 +9022,16 @@ var init_adapter3 = __esm({
8886
9022
  });
8887
9023
  }
8888
9024
  async moveEmail(emailId, targetFolder, sourceFolder) {
8889
- if (!this.client) throw new Error("Not connected");
9025
+ const client = await this.ensureConnected();
8890
9026
  const folder = sourceFolder || "INBOX";
8891
9027
  let lock;
8892
9028
  try {
8893
- lock = await this.client.getMailboxLock(folder);
9029
+ lock = await client.getMailboxLock(folder);
8894
9030
  } catch (error48) {
8895
9031
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
8896
9032
  }
8897
9033
  try {
8898
- await this.client.messageMove(emailId, targetFolder, { uid: true });
9034
+ await client.messageMove(emailId, targetFolder, { uid: true });
8899
9035
  } finally {
8900
9036
  lock.release();
8901
9037
  }
@@ -8925,44 +9061,44 @@ var init_adapter3 = __esm({
8925
9061
  }
8926
9062
  }
8927
9063
  async deleteEmail(emailId, permanent, sourceFolder) {
8928
- if (!this.client) throw new Error("Not connected");
9064
+ const client = await this.ensureConnected();
8929
9065
  const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
8930
9066
  const trashFolder = await this.resolveFolder("Trash");
8931
9067
  let lock;
8932
9068
  try {
8933
- lock = await this.client.getMailboxLock(folder);
9069
+ lock = await client.getMailboxLock(folder);
8934
9070
  } catch (error48) {
8935
9071
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
8936
9072
  }
8937
9073
  try {
8938
9074
  if (permanent || folder === trashFolder) {
8939
- await this.client.messageDelete(emailId, { uid: true });
9075
+ await client.messageDelete(emailId, { uid: true });
8940
9076
  } else {
8941
- await this.client.messageMove(emailId, trashFolder, { uid: true });
9077
+ await client.messageMove(emailId, trashFolder, { uid: true });
8942
9078
  }
8943
9079
  } finally {
8944
9080
  lock.release();
8945
9081
  }
8946
9082
  }
8947
9083
  async markEmail(emailId, flags, sourceFolder) {
8948
- if (!this.client) throw new Error("Not connected");
9084
+ const client = await this.ensureConnected();
8949
9085
  const folder = sourceFolder || "INBOX";
8950
9086
  let lock;
8951
9087
  try {
8952
- lock = await this.client.getMailboxLock(folder);
9088
+ lock = await client.getMailboxLock(folder);
8953
9089
  } catch (error48) {
8954
9090
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
8955
9091
  }
8956
9092
  try {
8957
9093
  if (flags.read === true) {
8958
- await this.client.messageFlagsAdd(emailId, ["\\Seen"], { uid: true });
9094
+ await client.messageFlagsAdd(emailId, ["\\Seen"], { uid: true });
8959
9095
  } else if (flags.read === false) {
8960
- await this.client.messageFlagsRemove(emailId, ["\\Seen"], { uid: true });
9096
+ await client.messageFlagsRemove(emailId, ["\\Seen"], { uid: true });
8961
9097
  }
8962
9098
  if (flags.starred === true || flags.flagged === true) {
8963
- await this.client.messageFlagsAdd(emailId, ["\\Flagged"], { uid: true });
9099
+ await client.messageFlagsAdd(emailId, ["\\Flagged"], { uid: true });
8964
9100
  } else if (flags.starred === false || flags.flagged === false) {
8965
- await this.client.messageFlagsRemove(emailId, ["\\Flagged"], { uid: true });
9101
+ await client.messageFlagsRemove(emailId, ["\\Flagged"], { uid: true });
8966
9102
  }
8967
9103
  } finally {
8968
9104
  lock.release();
@@ -8995,32 +9131,32 @@ var init_adapter3 = __esm({
8995
9131
  return { id: String(result?.uid ?? "") };
8996
9132
  }
8997
9133
  async batchDelete(emailIds, permanent, sourceFolder) {
8998
- if (!this.client) throw new Error("Not connected");
9134
+ const client = await this.ensureConnected();
8999
9135
  const result = { succeeded: [], failed: [] };
9000
9136
  const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
9001
9137
  const trashFolder = await this.resolveFolder("Trash");
9002
9138
  const shouldPermanentDelete = permanent || folder === trashFolder;
9003
9139
  let lock;
9004
9140
  try {
9005
- lock = await this.client.getMailboxLock(folder);
9141
+ lock = await client.getMailboxLock(folder);
9006
9142
  } catch (error48) {
9007
9143
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
9008
9144
  }
9009
9145
  try {
9010
9146
  const uidRange = emailIds.join(",");
9011
9147
  if (shouldPermanentDelete) {
9012
- await this.client.messageDelete(uidRange, { uid: true });
9148
+ await client.messageDelete(uidRange, { uid: true });
9013
9149
  } else {
9014
- await this.client.messageMove(uidRange, trashFolder, { uid: true });
9150
+ await client.messageMove(uidRange, trashFolder, { uid: true });
9015
9151
  }
9016
9152
  result.succeeded = [...emailIds];
9017
9153
  } catch (error48) {
9018
9154
  for (const id of emailIds) {
9019
9155
  try {
9020
9156
  if (shouldPermanentDelete) {
9021
- await this.client.messageDelete(id, { uid: true });
9157
+ await client.messageDelete(id, { uid: true });
9022
9158
  } else {
9023
- await this.client.messageMove(id, trashFolder, { uid: true });
9159
+ await client.messageMove(id, trashFolder, { uid: true });
9024
9160
  }
9025
9161
  result.succeeded.push(id);
9026
9162
  } catch (e) {
@@ -9033,23 +9169,23 @@ var init_adapter3 = __esm({
9033
9169
  return result;
9034
9170
  }
9035
9171
  async batchMove(emailIds, targetFolder, sourceFolder) {
9036
- if (!this.client) throw new Error("Not connected");
9172
+ const client = await this.ensureConnected();
9037
9173
  const result = { succeeded: [], failed: [] };
9038
9174
  const folder = sourceFolder || "INBOX";
9039
9175
  let lock;
9040
9176
  try {
9041
- lock = await this.client.getMailboxLock(folder);
9177
+ lock = await client.getMailboxLock(folder);
9042
9178
  } catch (error48) {
9043
9179
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
9044
9180
  }
9045
9181
  try {
9046
9182
  const uidRange = emailIds.join(",");
9047
- await this.client.messageMove(uidRange, targetFolder, { uid: true });
9183
+ await client.messageMove(uidRange, targetFolder, { uid: true });
9048
9184
  result.succeeded = [...emailIds];
9049
9185
  } catch (error48) {
9050
9186
  for (const id of emailIds) {
9051
9187
  try {
9052
- await this.client.messageMove(id, targetFolder, { uid: true });
9188
+ await client.messageMove(id, targetFolder, { uid: true });
9053
9189
  result.succeeded.push(id);
9054
9190
  } catch (e) {
9055
9191
  result.failed.push({ id, error: e.message });
@@ -9061,26 +9197,26 @@ var init_adapter3 = __esm({
9061
9197
  return result;
9062
9198
  }
9063
9199
  async batchMark(emailIds, flags, sourceFolder) {
9064
- if (!this.client) throw new Error("Not connected");
9200
+ const client = await this.ensureConnected();
9065
9201
  const result = { succeeded: [], failed: [] };
9066
9202
  const folder = sourceFolder || "INBOX";
9067
9203
  let lock;
9068
9204
  try {
9069
- lock = await this.client.getMailboxLock(folder);
9205
+ lock = await client.getMailboxLock(folder);
9070
9206
  } catch (error48) {
9071
9207
  throw formatImapError(error48, `Failed to open folder "${folder}"`);
9072
9208
  }
9073
9209
  try {
9074
9210
  const uidRange = emailIds.join(",");
9075
9211
  if (flags.read === true) {
9076
- await this.client.messageFlagsAdd(uidRange, ["\\Seen"], { uid: true });
9212
+ await client.messageFlagsAdd(uidRange, ["\\Seen"], { uid: true });
9077
9213
  } else if (flags.read === false) {
9078
- await this.client.messageFlagsRemove(uidRange, ["\\Seen"], { uid: true });
9214
+ await client.messageFlagsRemove(uidRange, ["\\Seen"], { uid: true });
9079
9215
  }
9080
9216
  if (flags.starred === true || flags.flagged === true) {
9081
- await this.client.messageFlagsAdd(uidRange, ["\\Flagged"], { uid: true });
9217
+ await client.messageFlagsAdd(uidRange, ["\\Flagged"], { uid: true });
9082
9218
  } else if (flags.starred === false || flags.flagged === false) {
9083
- await this.client.messageFlagsRemove(uidRange, ["\\Flagged"], { uid: true });
9219
+ await client.messageFlagsRemove(uidRange, ["\\Flagged"], { uid: true });
9084
9220
  }
9085
9221
  result.succeeded = [...emailIds];
9086
9222
  } catch (error48) {
@@ -10164,8 +10300,8 @@ function getErrorMap() {
10164
10300
 
10165
10301
  // node_modules/zod/v3/helpers/parseUtil.js
10166
10302
  var makeIssue = (params) => {
10167
- const { data, path: path3, errorMaps, issueData } = params;
10168
- const fullPath = [...path3, ...issueData.path || []];
10303
+ const { data, path: path4, errorMaps, issueData } = params;
10304
+ const fullPath = [...path4, ...issueData.path || []];
10169
10305
  const fullIssue = {
10170
10306
  ...issueData,
10171
10307
  path: fullPath
@@ -10280,11 +10416,11 @@ var errorUtil;
10280
10416
 
10281
10417
  // node_modules/zod/v3/types.js
10282
10418
  var ParseInputLazyPath = class {
10283
- constructor(parent, value, path3, key) {
10419
+ constructor(parent, value, path4, key) {
10284
10420
  this._cachedPath = [];
10285
10421
  this.parent = parent;
10286
10422
  this.data = value;
10287
- this._path = path3;
10423
+ this._path = path4;
10288
10424
  this._key = key;
10289
10425
  }
10290
10426
  get path() {
@@ -14207,10 +14343,10 @@ function mergeDefs(...defs) {
14207
14343
  function cloneDef(schema) {
14208
14344
  return mergeDefs(schema._zod.def);
14209
14345
  }
14210
- function getElementAtPath(obj, path3) {
14211
- if (!path3)
14346
+ function getElementAtPath(obj, path4) {
14347
+ if (!path4)
14212
14348
  return obj;
14213
- return path3.reduce((acc, key) => acc?.[key], obj);
14349
+ return path4.reduce((acc, key) => acc?.[key], obj);
14214
14350
  }
14215
14351
  function promiseAllObject(promisesObj) {
14216
14352
  const keys = Object.keys(promisesObj);
@@ -14593,11 +14729,11 @@ function aborted(x, startIndex = 0) {
14593
14729
  }
14594
14730
  return false;
14595
14731
  }
14596
- function prefixIssues(path3, issues) {
14732
+ function prefixIssues(path4, issues) {
14597
14733
  return issues.map((iss) => {
14598
14734
  var _a2;
14599
14735
  (_a2 = iss).path ?? (_a2.path = []);
14600
- iss.path.unshift(path3);
14736
+ iss.path.unshift(path4);
14601
14737
  return iss;
14602
14738
  });
14603
14739
  }
@@ -14780,7 +14916,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
14780
14916
  }
14781
14917
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
14782
14918
  const result = { errors: [] };
14783
- const processError = (error49, path3 = []) => {
14919
+ const processError = (error49, path4 = []) => {
14784
14920
  var _a2, _b;
14785
14921
  for (const issue2 of error49.issues) {
14786
14922
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -14790,7 +14926,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
14790
14926
  } else if (issue2.code === "invalid_element") {
14791
14927
  processError({ issues: issue2.issues }, issue2.path);
14792
14928
  } else {
14793
- const fullpath = [...path3, ...issue2.path];
14929
+ const fullpath = [...path4, ...issue2.path];
14794
14930
  if (fullpath.length === 0) {
14795
14931
  result.errors.push(mapper(issue2));
14796
14932
  continue;
@@ -14822,8 +14958,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
14822
14958
  }
14823
14959
  function toDotPath(_path) {
14824
14960
  const segs = [];
14825
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
14826
- for (const seg of path3) {
14961
+ const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
14962
+ for (const seg of path4) {
14827
14963
  if (typeof seg === "number")
14828
14964
  segs.push(`[${seg}]`);
14829
14965
  else if (typeof seg === "symbol")
@@ -27229,13 +27365,13 @@ function resolveRef(ref, ctx) {
27229
27365
  if (!ref.startsWith("#")) {
27230
27366
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
27231
27367
  }
27232
- const path3 = ref.slice(1).split("/").filter(Boolean);
27233
- if (path3.length === 0) {
27368
+ const path4 = ref.slice(1).split("/").filter(Boolean);
27369
+ if (path4.length === 0) {
27234
27370
  return ctx.rootSchema;
27235
27371
  }
27236
27372
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
27237
- if (path3[0] === defsKey) {
27238
- const key = path3[1];
27373
+ if (path4[0] === defsKey) {
27374
+ const key = path4[1];
27239
27375
  if (!key || !ctx.defs[key]) {
27240
27376
  throw new Error(`Reference not found: ${ref}`);
27241
27377
  }
@@ -33068,9 +33204,28 @@ function registerAccountTools(server, accountManager) {
33068
33204
  }
33069
33205
 
33070
33206
  // src/tools/reading.ts
33207
+ import fs3 from "node:fs";
33208
+ import os3 from "node:os";
33209
+ import path3 from "node:path";
33071
33210
  function jsonResult2(data) {
33072
33211
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
33073
33212
  }
33213
+ function getDownloadsBaseDir() {
33214
+ return path3.resolve(
33215
+ process.env.EMAIL_MCP_DOWNLOADS_DIR || path3.join(os3.homedir(), ".email-mcp", "downloads")
33216
+ );
33217
+ }
33218
+ function resolveDownloadPath(outputPath) {
33219
+ const baseDir = getDownloadsBaseDir();
33220
+ const resolved = path3.resolve(baseDir, outputPath);
33221
+ const relative = path3.relative(baseDir, resolved);
33222
+ if (relative.startsWith("..") || path3.isAbsolute(relative)) {
33223
+ throw new Error(
33224
+ `outputPath must resolve inside ${baseDir} (set EMAIL_MCP_DOWNLOADS_DIR to change the base directory)`
33225
+ );
33226
+ }
33227
+ return resolved;
33228
+ }
33074
33229
  function stripBodies(emails) {
33075
33230
  return emails.map((email3) => ({
33076
33231
  ...email3,
@@ -33195,6 +33350,33 @@ function registerReadingTools(server, accountManager) {
33195
33350
  }
33196
33351
  }
33197
33352
  );
33353
+ server.tool(
33354
+ "email_save_attachment",
33355
+ `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.`,
33356
+ {
33357
+ accountId: external_exports3.string(),
33358
+ emailId: external_exports3.string(),
33359
+ attachmentId: external_exports3.string(),
33360
+ outputPath: external_exports3.string().describe("Relative file path (within the downloads directory) to save the attachment as")
33361
+ },
33362
+ async (args) => {
33363
+ try {
33364
+ const absPath = resolveDownloadPath(args.outputPath);
33365
+ const provider = await accountManager.getProvider(args.accountId);
33366
+ const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
33367
+ fs3.mkdirSync(path3.dirname(absPath), { recursive: true });
33368
+ fs3.writeFileSync(absPath, Buffer.from(data));
33369
+ return jsonResult2({
33370
+ path: absPath,
33371
+ bytes: data.length,
33372
+ filename: meta3.filename,
33373
+ mimeType: meta3.contentType
33374
+ });
33375
+ } catch (error48) {
33376
+ return jsonResult2({ error: error48.message });
33377
+ }
33378
+ }
33379
+ );
33198
33380
  }
33199
33381
 
33200
33382
  // src/tools/sending.ts