@marlinjai/email-mcp 1.4.3 → 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.
@@ -108,6 +108,7 @@ function askPassword(message) {
108
108
 
109
109
  // src/auth/credential-store.ts
110
110
  import crypto from "node:crypto";
111
+ import { execFileSync } from "node:child_process";
111
112
  import fs from "node:fs";
112
113
  import path from "node:path";
113
114
  import os from "node:os";
@@ -116,15 +117,59 @@ var KEY_LENGTH = 32;
116
117
  var IV_LENGTH = 16;
117
118
  var SALT_LENGTH = 32;
118
119
  var PBKDF2_ITERATIONS = 1e5;
119
- function getMachineSeed() {
120
- return `email-mcp:${os.hostname()}:${os.userInfo().username}`;
120
+ var cachedMachineId;
121
+ function getStableMachineId() {
122
+ if (cachedMachineId !== void 0) return cachedMachineId;
123
+ cachedMachineId = computeStableMachineId();
124
+ return cachedMachineId;
121
125
  }
122
- function deriveKey(salt) {
123
- return crypto.pbkdf2Sync(getMachineSeed(), salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
126
+ function computeStableMachineId() {
127
+ try {
128
+ if (process.platform === "darwin") {
129
+ const out = execFileSync("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
130
+ encoding: "utf-8"
131
+ });
132
+ const match = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
133
+ return match ? match[1] : null;
134
+ }
135
+ if (process.platform === "linux") {
136
+ for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
137
+ if (fs.existsSync(p)) {
138
+ const id = fs.readFileSync(p, "utf-8").trim();
139
+ if (id) return id;
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+ if (process.platform === "win32") {
145
+ const out = execFileSync(
146
+ "reg",
147
+ ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"],
148
+ { encoding: "utf-8" }
149
+ );
150
+ const match = out.match(/MachineGuid\s+REG_SZ\s+([A-Za-z0-9-]+)/);
151
+ return match ? match[1] : null;
152
+ }
153
+ } catch {
154
+ }
155
+ return null;
156
+ }
157
+ function getSeedCandidates() {
158
+ const seeds = [];
159
+ const username = os.userInfo().username;
160
+ const envKey = process.env.EMAIL_MCP_KEY;
161
+ if (envKey) seeds.push(`email-mcp:env:${envKey}`);
162
+ const machineId = getStableMachineId();
163
+ if (machineId) seeds.push(`email-mcp:machine:${machineId}:${username}`);
164
+ seeds.push(`email-mcp:${os.hostname()}:${username}`);
165
+ return seeds;
124
166
  }
125
- function encrypt(plaintext) {
167
+ function deriveKey(seed, salt) {
168
+ return crypto.pbkdf2Sync(seed, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
169
+ }
170
+ function encrypt(plaintext, seed) {
126
171
  const salt = crypto.randomBytes(SALT_LENGTH);
127
- const key = deriveKey(salt);
172
+ const key = deriveKey(seed, salt);
128
173
  const iv = crypto.randomBytes(IV_LENGTH);
129
174
  const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
130
175
  let encrypted = cipher.update(plaintext, "utf-8", "hex");
@@ -137,14 +182,25 @@ function encrypt(plaintext) {
137
182
  data: encrypted
138
183
  });
139
184
  }
140
- function decrypt(ciphertext) {
185
+ function decrypt(ciphertext, seeds) {
141
186
  const { salt, iv, authTag, data } = JSON.parse(ciphertext);
142
- const key = deriveKey(Buffer.from(salt, "hex"));
143
- const decipher = crypto.createDecipheriv(ALGORITHM, key, Buffer.from(iv, "hex"));
144
- decipher.setAuthTag(Buffer.from(authTag, "hex"));
145
- let decrypted = decipher.update(data, "hex", "utf-8");
146
- decrypted += decipher.final("utf-8");
147
- return decrypted;
187
+ const saltBuf = Buffer.from(salt, "hex");
188
+ const ivBuf = Buffer.from(iv, "hex");
189
+ const authTagBuf = Buffer.from(authTag, "hex");
190
+ let lastError;
191
+ for (const seed of seeds) {
192
+ try {
193
+ const key = deriveKey(seed, saltBuf);
194
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, ivBuf);
195
+ decipher.setAuthTag(authTagBuf);
196
+ let decrypted = decipher.update(data, "hex", "utf-8");
197
+ decrypted += decipher.final("utf-8");
198
+ return { plaintext: decrypted, seedUsed: seed };
199
+ } catch (err) {
200
+ lastError = err;
201
+ }
202
+ }
203
+ throw lastError ?? new Error("Unable to decrypt credentials");
148
204
  }
149
205
  var CredentialStore = class {
150
206
  filePath;
@@ -160,12 +216,20 @@ var CredentialStore = class {
160
216
  return { accounts: {} };
161
217
  }
162
218
  const raw = fs.readFileSync(this.filePath, "utf-8");
163
- const json = decrypt(raw);
164
- return JSON.parse(json);
219
+ const seeds = getSeedCandidates();
220
+ const { plaintext, seedUsed } = decrypt(raw, seeds);
221
+ const data = JSON.parse(plaintext);
222
+ if (seedUsed !== seeds[0]) {
223
+ try {
224
+ this.write(data);
225
+ } catch {
226
+ }
227
+ }
228
+ return data;
165
229
  }
166
230
  write(data) {
167
231
  const json = JSON.stringify(data);
168
- const encrypted = encrypt(json);
232
+ const encrypted = encrypt(json, getSeedCandidates()[0]);
169
233
  fs.writeFileSync(this.filePath, encrypted, { mode: 384 });
170
234
  }
171
235
  async save(creds) {
@@ -1037,7 +1101,7 @@ var GmailAdapter = class {
1037
1101
  criteria.query = rule.value;
1038
1102
  break;
1039
1103
  }
1040
- const action = rule.action === "delete" ? { addLabelIds: ["TRASH"] } : { addLabelIds: ["SPAM"], removeLabelIds: ["INBOX"] };
1104
+ const action = rule.action === "delete" ? { addLabelIds: ["TRASH"] } : { removeLabelIds: ["INBOX"] };
1041
1105
  const res = await gmail.users.settings.filters.create({
1042
1106
  userId: "me",
1043
1107
  requestBody: { criteria, action }
@@ -1823,6 +1887,7 @@ var ImapAdapter = class {
1823
1887
  accountId = "";
1824
1888
  email = "";
1825
1889
  passwordCreds = null;
1890
+ connectingPromise = null;
1826
1891
  async connect(credentials) {
1827
1892
  if (!credentials.password) {
1828
1893
  throw new Error("IMAP adapter requires password credentials");
@@ -1830,23 +1895,87 @@ var ImapAdapter = class {
1830
1895
  this.accountId = credentials.id;
1831
1896
  this.email = credentials.email;
1832
1897
  this.passwordCreds = credentials.password;
1833
- this.client = new ImapFlow({
1834
- host: credentials.password.host,
1835
- port: credentials.password.port,
1836
- secure: credentials.password.tls,
1837
- auth: {
1838
- user: credentials.email,
1839
- pass: credentials.password.password
1840
- },
1841
- logger: false
1842
- });
1843
- await this.client.connect();
1898
+ await this.ensureConnected();
1844
1899
  }
1845
1900
  async disconnect() {
1846
1901
  if (this.client) {
1847
- await this.client.logout();
1902
+ try {
1903
+ await this.client.logout();
1904
+ } catch {
1905
+ }
1848
1906
  this.client = null;
1849
1907
  }
1908
+ this.passwordCreds = null;
1909
+ }
1910
+ /**
1911
+ * Builds a fresh ImapFlow client from stored credentials and wires up
1912
+ * close/error listeners so a dropped socket clears `this.client`, forcing
1913
+ * the next `ensureConnected()` call to reconnect.
1914
+ */
1915
+ buildClient() {
1916
+ if (!this.passwordCreds) {
1917
+ throw new Error("Cannot build IMAP client without stored credentials");
1918
+ }
1919
+ const client = new ImapFlow({
1920
+ host: this.passwordCreds.host,
1921
+ port: this.passwordCreds.port,
1922
+ secure: this.passwordCreds.tls,
1923
+ auth: { user: this.email, pass: this.passwordCreds.password },
1924
+ logger: false
1925
+ });
1926
+ const drop = () => {
1927
+ if (this.client === client) this.client = null;
1928
+ };
1929
+ client.on("close", drop);
1930
+ client.on("error", drop);
1931
+ return client;
1932
+ }
1933
+ /**
1934
+ * Returns true while the underlying ImapFlow socket is healthy.
1935
+ * ImapFlow sets `usable=false` when the connection is closed, in the
1936
+ * middle of logging out, or otherwise unsafe to issue commands on.
1937
+ */
1938
+ isClientUsable() {
1939
+ return !!this.client && this.client.usable === true;
1940
+ }
1941
+ /**
1942
+ * Guarantees a usable IMAP client and returns it. Reconnects transparently
1943
+ * if the previous client was dropped (idle timeout, server-side disconnect,
1944
+ * network flap). Concurrent callers share a single in-flight reconnect to
1945
+ * avoid duplicate sessions.
1946
+ *
1947
+ * Callers should bind the result to a local `const client` and use that
1948
+ * within the method instead of `this.client`, so a `'close'` event mid-call
1949
+ * does not race the active operation.
1950
+ */
1951
+ async ensureConnected() {
1952
+ if (this.connectingPromise) {
1953
+ await this.connectingPromise;
1954
+ }
1955
+ if (!this.isClientUsable()) {
1956
+ if (!this.passwordCreds) {
1957
+ throw new Error("Not connected");
1958
+ }
1959
+ this.connectingPromise = (async () => {
1960
+ if (this.client) {
1961
+ try {
1962
+ await this.client.logout();
1963
+ } catch {
1964
+ }
1965
+ this.client = null;
1966
+ }
1967
+ const client = this.buildClient();
1968
+ await client.connect();
1969
+ this.client = client;
1970
+ })().finally(() => {
1971
+ this.connectingPromise = null;
1972
+ });
1973
+ await this.connectingPromise;
1974
+ }
1975
+ if (!this.client) {
1976
+ throw new Error("IMAP client unavailable after reconnect attempt");
1977
+ }
1978
+ return this.client;
1850
1979
  }
1851
1980
  async testConnection() {
1852
1981
  try {
@@ -1857,14 +1986,14 @@ var ImapAdapter = class {
1857
1986
  }
1858
1987
  }
1859
1988
  async listFolders() {
1860
- if (!this.client) throw new Error("Not connected");
1861
- const imapFolders = await this.client.list();
1989
+ const client = await this.ensureConnected();
1990
+ const imapFolders = await client.list();
1862
1991
  return imapFolders.map(mapImapFolder);
1863
1992
  }
1864
1993
  async createFolder(name, parentPath) {
1865
- if (!this.client) throw new Error("Not connected");
1994
+ const client = await this.ensureConnected();
1866
1995
  const fullPath = parentPath ? `${parentPath}/${name}` : name;
1867
- const result = await this.client.mailboxCreate(fullPath);
1996
+ const result = await client.mailboxCreate(fullPath);
1868
1997
  return {
1869
1998
  id: result.path,
1870
1999
  name: result.name || name,
@@ -1893,7 +2022,7 @@ var ImapAdapter = class {
1893
2022
  * and matching by name or special-use type.
1894
2023
  */
1895
2024
  async resolveFolder(folder) {
1896
- if (!this.client) throw new Error("Not connected");
2025
+ const client = await this.ensureConnected();
1897
2026
  const FOLDER_ALIASES = {
1898
2027
  junk: ["Junk", "Spam", "Bulk Mail", "Junk E-mail", "Junk Email"],
1899
2028
  spam: ["Junk", "Spam", "Bulk Mail", "Junk E-mail", "Junk Email"],
@@ -1902,7 +2031,7 @@ var ImapAdapter = class {
1902
2031
  drafts: ["Drafts", "Draft"],
1903
2032
  archive: ["Archive", "All Mail"]
1904
2033
  };
1905
- const folders = await this.client.list();
2034
+ const folders = await client.list();
1906
2035
  const lowerFolder = folder.toLowerCase();
1907
2036
  const exactMatch = folders.find(
1908
2037
  (f) => f.path === folder || f.path.toLowerCase() === lowerFolder
@@ -1940,7 +2069,7 @@ var ImapAdapter = class {
1940
2069
  return folder;
1941
2070
  }
1942
2071
  async search(query) {
1943
- if (!this.client) throw new Error("Not connected");
2072
+ const client = await this.ensureConnected();
1944
2073
  const requestedFolder = query.folder || "INBOX";
1945
2074
  let folder;
1946
2075
  try {
@@ -1949,17 +2078,25 @@ var ImapAdapter = class {
1949
2078
  folder = requestedFolder;
1950
2079
  }
1951
2080
  try {
1952
- await this.client.noop();
2081
+ await client.noop();
1953
2082
  } catch {
1954
2083
  }
1955
2084
  let lock;
1956
2085
  try {
1957
- lock = await this.client.getMailboxLock(folder);
2086
+ lock = await client.getMailboxLock(folder);
1958
2087
  } catch (error) {
1959
2088
  throw formatImapError(error, `Failed to open folder "${folder}"`);
1960
2089
  }
1961
2090
  try {
1962
- const mailboxExists = this.client.mailbox?.exists ?? 0;
2091
+ let mailboxExists = client.mailbox?.exists ?? 0;
2092
+ if (mailboxExists === 0) {
2093
+ try {
2094
+ const status = await client.status(folder, { messages: true });
2095
+ mailboxExists = status?.messages ?? 0;
2096
+ } catch {
2097
+ mailboxExists = 0;
2098
+ }
2099
+ }
1963
2100
  if (mailboxExists === 0) {
1964
2101
  return [];
1965
2102
  }
@@ -1967,14 +2104,14 @@ var ImapAdapter = class {
1967
2104
  const hasCriteria = Object.keys(criteria).length > 0;
1968
2105
  let allUids = [];
1969
2106
  try {
1970
- const searchResult = await this.client.search(
2107
+ const searchResult = await client.search(
1971
2108
  hasCriteria ? criteria : { all: true },
1972
2109
  { uid: true }
1973
2110
  );
1974
2111
  allUids = Array.isArray(searchResult) ? searchResult : [];
1975
2112
  } catch {
1976
2113
  try {
1977
- allUids = await this.collectUidsViaFetch(query);
2114
+ allUids = await this.collectUidsViaFetch(client, query);
1978
2115
  } catch {
1979
2116
  return [];
1980
2117
  }
@@ -1982,7 +2119,7 @@ var ImapAdapter = class {
1982
2119
  const offset = query.offset || 0;
1983
2120
  const slicedUids = query.limit ? allUids.slice(offset, offset + query.limit) : allUids.slice(offset);
1984
2121
  if (slicedUids.length === 0) return [];
1985
- return await this.fetchEmails(slicedUids, folder, query.returnBody);
2122
+ return await this.fetchEmails(client, slicedUids, folder, query.returnBody);
1986
2123
  } catch (error) {
1987
2124
  throw formatImapError(error, `Search failed in folder "${folder}"`);
1988
2125
  } finally {
@@ -1996,13 +2133,12 @@ var ImapAdapter = class {
1996
2133
  * 2. FETCH 1:N using mailbox.exists as explicit range
1997
2134
  * 3. Individual sequence number fetches (slowest but most resilient)
1998
2135
  */
1999
- async collectUidsViaFetch(query) {
2000
- if (!this.client) return [];
2136
+ async collectUidsViaFetch(client, query) {
2001
2137
  const uids = [];
2002
- const exists = this.client.mailbox?.exists ?? 0;
2138
+ const exists = client.mailbox?.exists ?? 0;
2003
2139
  const safeFetch = async (range) => {
2004
2140
  try {
2005
- const messages = await this.client.fetchAll(range, { uid: true, flags: true });
2141
+ const messages = await client.fetchAll(range, { uid: true, flags: true });
2006
2142
  for (const msg of messages) {
2007
2143
  if (query.unreadOnly && msg.flags?.has("\\Seen")) continue;
2008
2144
  if (query.starredOnly && !msg.flags?.has("\\Flagged")) continue;
@@ -2022,7 +2158,7 @@ var ImapAdapter = class {
2022
2158
  const count = exists > 0 ? exists : 50;
2023
2159
  for (let seq = 1; seq <= count; seq++) {
2024
2160
  try {
2025
- const msgs = await this.client.fetchAll(String(seq), { uid: true, flags: true });
2161
+ const msgs = await client.fetchAll(String(seq), { uid: true, flags: true });
2026
2162
  for (const msg of msgs) {
2027
2163
  if (query.unreadOnly && msg.flags?.has("\\Seen")) continue;
2028
2164
  if (query.starredOnly && !msg.flags?.has("\\Flagged")) continue;
@@ -2036,19 +2172,18 @@ var ImapAdapter = class {
2036
2172
  /**
2037
2173
  * Fetches email data for a set of UIDs, either with full body or lightweight headers.
2038
2174
  */
2039
- async fetchEmails(uids, folder, returnBody) {
2040
- if (!this.client) return [];
2175
+ async fetchEmails(client, uids, folder, returnBody) {
2041
2176
  const emails = [];
2042
2177
  const fetchOpts = returnBody ? { source: true, uid: true, flags: true } : { envelope: true, uid: true, flags: true, bodyStructure: true };
2043
2178
  const uidRange = uids.join(",");
2044
2179
  let messages;
2045
2180
  try {
2046
- messages = await this.client.fetchAll(uidRange, fetchOpts, { uid: true });
2181
+ messages = await client.fetchAll(uidRange, fetchOpts, { uid: true });
2047
2182
  } catch {
2048
2183
  messages = [];
2049
2184
  for (const uid of uids) {
2050
2185
  try {
2051
- const batch = await this.client.fetchAll(String(uid), fetchOpts, { uid: true });
2186
+ const batch = await client.fetchAll(String(uid), fetchOpts, { uid: true });
2052
2187
  messages.push(...batch);
2053
2188
  } catch {
2054
2189
  }
@@ -2087,17 +2222,17 @@ var ImapAdapter = class {
2087
2222
  return emails;
2088
2223
  }
2089
2224
  async getEmail(id, folder) {
2090
- if (!this.client) throw new Error("Not connected");
2225
+ const client = await this.ensureConnected();
2091
2226
  const targetFolder = folder || "INBOX";
2092
2227
  let lock;
2093
2228
  try {
2094
- lock = await this.client.getMailboxLock(targetFolder);
2229
+ lock = await client.getMailboxLock(targetFolder);
2095
2230
  } catch (error) {
2096
2231
  throw formatImapError(error, `Failed to open folder "${targetFolder}"`);
2097
2232
  }
2098
2233
  try {
2099
2234
  const uid = parseInt(id, 10);
2100
- const msg = await this.client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
2235
+ const msg = await client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
2101
2236
  if (!msg) throw new Error(`Email ${id} not found`);
2102
2237
  const parsed = await simpleParser(msg.source);
2103
2238
  parsed.flags = msg.flags;
@@ -2107,22 +2242,22 @@ var ImapAdapter = class {
2107
2242
  }
2108
2243
  }
2109
2244
  async getThread(threadId) {
2110
- if (!this.client) throw new Error("Not connected");
2245
+ const client = await this.ensureConnected();
2111
2246
  let lock;
2112
2247
  try {
2113
- lock = await this.client.getMailboxLock("INBOX");
2248
+ lock = await client.getMailboxLock("INBOX");
2114
2249
  } catch (error) {
2115
2250
  throw formatImapError(error, 'Failed to open folder "INBOX"');
2116
2251
  }
2117
2252
  try {
2118
- const searchResult = await this.client.search(
2253
+ const searchResult = await client.search(
2119
2254
  { or: [{ header: { "message-id": threadId } }, { header: { references: threadId } }, { header: { "in-reply-to": threadId } }] },
2120
2255
  { uid: true }
2121
2256
  );
2122
2257
  const uids = Array.isArray(searchResult) ? searchResult : [];
2123
2258
  if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
2124
2259
  const messages = [];
2125
- for await (const msg of this.client.fetch(uids, { source: true, uid: true, flags: true })) {
2260
+ for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
2126
2261
  const parsed = await simpleParser(msg.source);
2127
2262
  parsed.flags = msg.flags;
2128
2263
  messages.push(mapParsedEmail(parsed, "INBOX", this.accountId, msg.uid));
@@ -2148,15 +2283,15 @@ var ImapAdapter = class {
2148
2283
  }
2149
2284
  }
2150
2285
  async getAttachment(emailId, attachmentId) {
2151
- if (!this.client) throw new Error("Not connected");
2286
+ const client = await this.ensureConnected();
2152
2287
  let lock;
2153
2288
  try {
2154
- lock = await this.client.getMailboxLock("INBOX");
2289
+ lock = await client.getMailboxLock("INBOX");
2155
2290
  } catch (error) {
2156
2291
  throw formatImapError(error, 'Failed to open folder "INBOX"');
2157
2292
  }
2158
2293
  try {
2159
- const msg = await this.client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
2294
+ const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
2160
2295
  if (!msg) throw new Error(`Email ${emailId} not found`);
2161
2296
  const parsed = await simpleParser(msg.source);
2162
2297
  const attachment = (parsed.attachments || []).find(
@@ -2183,7 +2318,7 @@ var ImapAdapter = class {
2183
2318
  return { id: messageId };
2184
2319
  }
2185
2320
  async createDraft(params) {
2186
- if (!this.client) throw new Error("Not connected");
2321
+ const client = await this.ensureConnected();
2187
2322
  const lines = [];
2188
2323
  lines.push(`From: ${this.email}`);
2189
2324
  lines.push(`To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`);
@@ -2199,7 +2334,9 @@ var ImapAdapter = class {
2199
2334
  lines.push("");
2200
2335
  lines.push(params.body.text || "");
2201
2336
  const rawMessage = lines.join("\r\n");
2202
- const result = await this.client.append("Drafts", rawMessage, ["\\Draft", "\\Seen"]);
2337
+ const draftsFolder = await this.resolveFolder("Drafts");
2338
+ const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
2339
+ if (!result) throw new Error("Failed to append draft");
2203
2340
  return { id: String(result.uid || result) };
2204
2341
  }
2205
2342
  async listDrafts(limit, offset) {
@@ -2210,16 +2347,16 @@ var ImapAdapter = class {
2210
2347
  });
2211
2348
  }
2212
2349
  async moveEmail(emailId, targetFolder, sourceFolder) {
2213
- if (!this.client) throw new Error("Not connected");
2350
+ const client = await this.ensureConnected();
2214
2351
  const folder = sourceFolder || "INBOX";
2215
2352
  let lock;
2216
2353
  try {
2217
- lock = await this.client.getMailboxLock(folder);
2354
+ lock = await client.getMailboxLock(folder);
2218
2355
  } catch (error) {
2219
2356
  throw formatImapError(error, `Failed to open folder "${folder}"`);
2220
2357
  }
2221
2358
  try {
2222
- await this.client.messageMove(emailId, targetFolder, { uid: true });
2359
+ await client.messageMove(emailId, targetFolder, { uid: true });
2223
2360
  } finally {
2224
2361
  lock.release();
2225
2362
  }
@@ -2249,44 +2386,44 @@ var ImapAdapter = class {
2249
2386
  }
2250
2387
  }
2251
2388
  async deleteEmail(emailId, permanent, sourceFolder) {
2252
- if (!this.client) throw new Error("Not connected");
2389
+ const client = await this.ensureConnected();
2253
2390
  const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
2254
2391
  const trashFolder = await this.resolveFolder("Trash");
2255
2392
  let lock;
2256
2393
  try {
2257
- lock = await this.client.getMailboxLock(folder);
2394
+ lock = await client.getMailboxLock(folder);
2258
2395
  } catch (error) {
2259
2396
  throw formatImapError(error, `Failed to open folder "${folder}"`);
2260
2397
  }
2261
2398
  try {
2262
2399
  if (permanent || folder === trashFolder) {
2263
- await this.client.messageDelete(emailId, { uid: true });
2400
+ await client.messageDelete(emailId, { uid: true });
2264
2401
  } else {
2265
- await this.client.messageMove(emailId, trashFolder, { uid: true });
2402
+ await client.messageMove(emailId, trashFolder, { uid: true });
2266
2403
  }
2267
2404
  } finally {
2268
2405
  lock.release();
2269
2406
  }
2270
2407
  }
2271
2408
  async markEmail(emailId, flags, sourceFolder) {
2272
- if (!this.client) throw new Error("Not connected");
2409
+ const client = await this.ensureConnected();
2273
2410
  const folder = sourceFolder || "INBOX";
2274
2411
  let lock;
2275
2412
  try {
2276
- lock = await this.client.getMailboxLock(folder);
2413
+ lock = await client.getMailboxLock(folder);
2277
2414
  } catch (error) {
2278
2415
  throw formatImapError(error, `Failed to open folder "${folder}"`);
2279
2416
  }
2280
2417
  try {
2281
2418
  if (flags.read === true) {
2282
- await this.client.messageFlagsAdd(emailId, ["\\Seen"], { uid: true });
2419
+ await client.messageFlagsAdd(emailId, ["\\Seen"], { uid: true });
2283
2420
  } else if (flags.read === false) {
2284
- await this.client.messageFlagsRemove(emailId, ["\\Seen"], { uid: true });
2421
+ await client.messageFlagsRemove(emailId, ["\\Seen"], { uid: true });
2285
2422
  }
2286
2423
  if (flags.starred === true || flags.flagged === true) {
2287
- await this.client.messageFlagsAdd(emailId, ["\\Flagged"], { uid: true });
2424
+ await client.messageFlagsAdd(emailId, ["\\Flagged"], { uid: true });
2288
2425
  } else if (flags.starred === false || flags.flagged === false) {
2289
- await this.client.messageFlagsRemove(emailId, ["\\Flagged"], { uid: true });
2426
+ await client.messageFlagsRemove(emailId, ["\\Flagged"], { uid: true });
2290
2427
  }
2291
2428
  } finally {
2292
2429
  lock.release();
@@ -2319,32 +2456,32 @@ var ImapAdapter = class {
2319
2456
  return { id: String(result?.uid ?? "") };
2320
2457
  }
2321
2458
  async batchDelete(emailIds, permanent, sourceFolder) {
2322
- if (!this.client) throw new Error("Not connected");
2459
+ const client = await this.ensureConnected();
2323
2460
  const result = { succeeded: [], failed: [] };
2324
2461
  const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
2325
2462
  const trashFolder = await this.resolveFolder("Trash");
2326
2463
  const shouldPermanentDelete = permanent || folder === trashFolder;
2327
2464
  let lock;
2328
2465
  try {
2329
- lock = await this.client.getMailboxLock(folder);
2466
+ lock = await client.getMailboxLock(folder);
2330
2467
  } catch (error) {
2331
2468
  throw formatImapError(error, `Failed to open folder "${folder}"`);
2332
2469
  }
2333
2470
  try {
2334
2471
  const uidRange = emailIds.join(",");
2335
2472
  if (shouldPermanentDelete) {
2336
- await this.client.messageDelete(uidRange, { uid: true });
2473
+ await client.messageDelete(uidRange, { uid: true });
2337
2474
  } else {
2338
- await this.client.messageMove(uidRange, trashFolder, { uid: true });
2475
+ await client.messageMove(uidRange, trashFolder, { uid: true });
2339
2476
  }
2340
2477
  result.succeeded = [...emailIds];
2341
2478
  } catch (error) {
2342
2479
  for (const id of emailIds) {
2343
2480
  try {
2344
2481
  if (shouldPermanentDelete) {
2345
- await this.client.messageDelete(id, { uid: true });
2482
+ await client.messageDelete(id, { uid: true });
2346
2483
  } else {
2347
- await this.client.messageMove(id, trashFolder, { uid: true });
2484
+ await client.messageMove(id, trashFolder, { uid: true });
2348
2485
  }
2349
2486
  result.succeeded.push(id);
2350
2487
  } catch (e) {
@@ -2357,23 +2494,23 @@ var ImapAdapter = class {
2357
2494
  return result;
2358
2495
  }
2359
2496
  async batchMove(emailIds, targetFolder, sourceFolder) {
2360
- if (!this.client) throw new Error("Not connected");
2497
+ const client = await this.ensureConnected();
2361
2498
  const result = { succeeded: [], failed: [] };
2362
2499
  const folder = sourceFolder || "INBOX";
2363
2500
  let lock;
2364
2501
  try {
2365
- lock = await this.client.getMailboxLock(folder);
2502
+ lock = await client.getMailboxLock(folder);
2366
2503
  } catch (error) {
2367
2504
  throw formatImapError(error, `Failed to open folder "${folder}"`);
2368
2505
  }
2369
2506
  try {
2370
2507
  const uidRange = emailIds.join(",");
2371
- await this.client.messageMove(uidRange, targetFolder, { uid: true });
2508
+ await client.messageMove(uidRange, targetFolder, { uid: true });
2372
2509
  result.succeeded = [...emailIds];
2373
2510
  } catch (error) {
2374
2511
  for (const id of emailIds) {
2375
2512
  try {
2376
- await this.client.messageMove(id, targetFolder, { uid: true });
2513
+ await client.messageMove(id, targetFolder, { uid: true });
2377
2514
  result.succeeded.push(id);
2378
2515
  } catch (e) {
2379
2516
  result.failed.push({ id, error: e.message });
@@ -2385,26 +2522,26 @@ var ImapAdapter = class {
2385
2522
  return result;
2386
2523
  }
2387
2524
  async batchMark(emailIds, flags, sourceFolder) {
2388
- if (!this.client) throw new Error("Not connected");
2525
+ const client = await this.ensureConnected();
2389
2526
  const result = { succeeded: [], failed: [] };
2390
2527
  const folder = sourceFolder || "INBOX";
2391
2528
  let lock;
2392
2529
  try {
2393
- lock = await this.client.getMailboxLock(folder);
2530
+ lock = await client.getMailboxLock(folder);
2394
2531
  } catch (error) {
2395
2532
  throw formatImapError(error, `Failed to open folder "${folder}"`);
2396
2533
  }
2397
2534
  try {
2398
2535
  const uidRange = emailIds.join(",");
2399
2536
  if (flags.read === true) {
2400
- await this.client.messageFlagsAdd(uidRange, ["\\Seen"], { uid: true });
2537
+ await client.messageFlagsAdd(uidRange, ["\\Seen"], { uid: true });
2401
2538
  } else if (flags.read === false) {
2402
- await this.client.messageFlagsRemove(uidRange, ["\\Seen"], { uid: true });
2539
+ await client.messageFlagsRemove(uidRange, ["\\Seen"], { uid: true });
2403
2540
  }
2404
2541
  if (flags.starred === true || flags.flagged === true) {
2405
- await this.client.messageFlagsAdd(uidRange, ["\\Flagged"], { uid: true });
2542
+ await client.messageFlagsAdd(uidRange, ["\\Flagged"], { uid: true });
2406
2543
  } else if (flags.starred === false || flags.flagged === false) {
2407
- await this.client.messageFlagsRemove(uidRange, ["\\Flagged"], { uid: true });
2544
+ await client.messageFlagsRemove(uidRange, ["\\Flagged"], { uid: true });
2408
2545
  }
2409
2546
  result.succeeded = [...emailIds];
2410
2547
  } catch (error) {