@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.
@@ -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) {
@@ -278,14 +342,33 @@ var OAuthCallbackServer = class {
278
342
  // src/providers/gmail/auth.ts
279
343
  import { OAuth2Client } from "google-auth-library";
280
344
  import crypto2 from "node:crypto";
281
- var GMAIL_SCOPES = [
282
- "https://mail.google.com/",
283
- "https://www.googleapis.com/auth/gmail.modify",
284
- // Mailbox content access (above) does NOT cover the Settings API —
285
- // Google treats them as separate permission domains. Needed for
286
- // users.settings.filters.* (email_create_block_rule/list/delete).
287
- "https://www.googleapis.com/auth/gmail.settings.basic"
288
- ];
345
+ var GMAIL_SCOPES = {
346
+ full: [
347
+ // Gmail's maximum-permission scope. Superset of gmail.modify below;
348
+ // the only capability it adds is immediate, trash-bypassing permanent
349
+ // deletion (messages.delete / batchDelete with permanent: true).
350
+ "https://mail.google.com/",
351
+ "https://www.googleapis.com/auth/gmail.modify",
352
+ // Mailbox content access (above) does NOT cover the Settings API —
353
+ // Google treats them as separate permission domains. Needed for
354
+ // users.settings.filters.* (email_create_block_rule/list/delete).
355
+ "https://www.googleapis.com/auth/gmail.settings.basic"
356
+ ],
357
+ // Drops https://mail.google.com/ so an AI-triggered permanent delete
358
+ // can't happen: gmail.modify already covers every other read/write
359
+ // operation this server performs (read, send, label, archive, trash,
360
+ // drafts, filters). Choosing this mode means deleteEmail(..., permanent:
361
+ // true) and batchDelete(..., permanent: true) will fail with a Gmail API
362
+ // 403 instead of succeeding — everything else behaves identically.
363
+ restricted: [
364
+ "https://www.googleapis.com/auth/gmail.modify",
365
+ "https://www.googleapis.com/auth/gmail.settings.basic"
366
+ ]
367
+ };
368
+ function resolveScopeMode(explicit) {
369
+ if (explicit) return explicit;
370
+ return process.env.EMAIL_MCP_GMAIL_SCOPE?.toLowerCase() === "restricted" ? "restricted" : "full";
371
+ }
289
372
  var GmailAuth = class {
290
373
  clientId;
291
374
  clientSecret;
@@ -295,13 +378,13 @@ var GmailAuth = class {
295
378
  this.clientSecret = clientSecret;
296
379
  this.oauth2Client = new OAuth2Client(clientId, clientSecret, redirectUri);
297
380
  }
298
- getAuthUrl(redirectUri) {
381
+ getAuthUrl(redirectUri, scopeMode) {
299
382
  const codeVerifier = crypto2.randomBytes(32).toString("base64url");
300
383
  const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
301
384
  this.oauth2Client = new OAuth2Client(this.clientId, this.clientSecret, redirectUri);
302
385
  const url = this.oauth2Client.generateAuthUrl({
303
386
  access_type: "offline",
304
- scope: GMAIL_SCOPES,
387
+ scope: GMAIL_SCOPES[resolveScopeMode(scopeMode)],
305
388
  prompt: "consent",
306
389
  code_challenge: codeChallenge,
307
390
  code_challenge_method: "S256"
@@ -664,11 +747,12 @@ var GmailAdapter = class {
664
747
  const labels = res.data.labels || [];
665
748
  return labels.map(mapGmailLabel);
666
749
  }
667
- async createFolder(name) {
750
+ async createFolder(name, parentPath) {
668
751
  const gmail = this.ensureConnected();
752
+ const fullName = parentPath ? `${parentPath}/${name}` : name;
669
753
  const res = await gmail.users.labels.create({
670
754
  userId: "me",
671
- requestBody: { name }
755
+ requestBody: { name: fullName }
672
756
  });
673
757
  return mapGmailLabel(res.data);
674
758
  }
@@ -785,6 +869,18 @@ var GmailAdapter = class {
785
869
  });
786
870
  return { id: res.data.id || "" };
787
871
  }
872
+ async updateDraft(draftId, params) {
873
+ const gmail = this.ensureConnected();
874
+ const raw = this.buildRfc2822(params);
875
+ const res = await gmail.users.drafts.update({
876
+ userId: "me",
877
+ id: draftId,
878
+ requestBody: {
879
+ message: { raw }
880
+ }
881
+ });
882
+ return { id: res.data.id || draftId };
883
+ }
788
884
  async listDrafts(limit, _offset) {
789
885
  const gmail = this.ensureConnected();
790
886
  const res = await gmail.users.drafts.list({
@@ -948,6 +1044,34 @@ var GmailAdapter = class {
948
1044
  }
949
1045
  return result;
950
1046
  }
1047
+ async batchLabel(emailIds, addLabels, removeLabels) {
1048
+ const gmail = this.ensureConnected();
1049
+ const result = { succeeded: [], failed: [] };
1050
+ const addLabelIds = addLabels ?? [];
1051
+ const removeLabelIds = removeLabels ?? [];
1052
+ if (addLabelIds.length === 0 && removeLabelIds.length === 0) {
1053
+ result.succeeded = [...emailIds];
1054
+ return result;
1055
+ }
1056
+ try {
1057
+ await gmail.users.messages.batchModify({
1058
+ userId: "me",
1059
+ requestBody: { ids: emailIds, addLabelIds, removeLabelIds }
1060
+ });
1061
+ result.succeeded = [...emailIds];
1062
+ } catch (error) {
1063
+ for (const id of emailIds) {
1064
+ try {
1065
+ if (addLabelIds.length > 0) await this.addLabels(id, addLabelIds);
1066
+ if (removeLabelIds.length > 0) await this.removeLabels(id, removeLabelIds);
1067
+ result.succeeded.push(id);
1068
+ } catch (e) {
1069
+ result.failed.push({ id, error: e.message });
1070
+ }
1071
+ }
1072
+ }
1073
+ return result;
1074
+ }
951
1075
  async getRawMessage(emailId, _sourceFolder) {
952
1076
  const gmail = this.ensureConnected();
953
1077
  const res = await gmail.users.messages.get({
@@ -1411,6 +1535,12 @@ var OutlookAdapter = class {
1411
1535
  const result = await client.api("/me/messages").post(message);
1412
1536
  return { id: result.id };
1413
1537
  }
1538
+ async updateDraft(draftId, params) {
1539
+ const client = this.ensureClient();
1540
+ const message = this.buildGraphMessage(params);
1541
+ await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(message);
1542
+ return { id: draftId };
1543
+ }
1414
1544
  async listDrafts(limit, offset) {
1415
1545
  const client = this.ensureClient();
1416
1546
  let request = client.api("/me/mailFolders/drafts/messages");
@@ -1823,6 +1953,7 @@ var ImapAdapter = class {
1823
1953
  accountId = "";
1824
1954
  email = "";
1825
1955
  passwordCreds = null;
1956
+ connectingPromise = null;
1826
1957
  async connect(credentials) {
1827
1958
  if (!credentials.password) {
1828
1959
  throw new Error("IMAP adapter requires password credentials");
@@ -1830,23 +1961,87 @@ var ImapAdapter = class {
1830
1961
  this.accountId = credentials.id;
1831
1962
  this.email = credentials.email;
1832
1963
  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();
1964
+ await this.ensureConnected();
1844
1965
  }
1845
1966
  async disconnect() {
1846
1967
  if (this.client) {
1847
- await this.client.logout();
1968
+ try {
1969
+ await this.client.logout();
1970
+ } catch {
1971
+ }
1848
1972
  this.client = null;
1849
1973
  }
1974
+ this.passwordCreds = null;
1975
+ }
1976
+ /**
1977
+ * Builds a fresh ImapFlow client from stored credentials and wires up
1978
+ * close/error listeners so a dropped socket clears `this.client`, forcing
1979
+ * the next `ensureConnected()` call to reconnect.
1980
+ */
1981
+ buildClient() {
1982
+ if (!this.passwordCreds) {
1983
+ throw new Error("Cannot build IMAP client without stored credentials");
1984
+ }
1985
+ const client = new ImapFlow({
1986
+ host: this.passwordCreds.host,
1987
+ port: this.passwordCreds.port,
1988
+ secure: this.passwordCreds.tls,
1989
+ auth: { user: this.email, pass: this.passwordCreds.password },
1990
+ logger: false
1991
+ });
1992
+ const drop = () => {
1993
+ if (this.client === client) this.client = null;
1994
+ };
1995
+ client.on("close", drop);
1996
+ client.on("error", drop);
1997
+ return client;
1998
+ }
1999
+ /**
2000
+ * Returns true while the underlying ImapFlow socket is healthy.
2001
+ * ImapFlow sets `usable=false` when the connection is closed, in the
2002
+ * middle of logging out, or otherwise unsafe to issue commands on.
2003
+ */
2004
+ isClientUsable() {
2005
+ return !!this.client && this.client.usable === true;
2006
+ }
2007
+ /**
2008
+ * Guarantees a usable IMAP client and returns it. Reconnects transparently
2009
+ * if the previous client was dropped (idle timeout, server-side disconnect,
2010
+ * network flap). Concurrent callers share a single in-flight reconnect to
2011
+ * avoid duplicate sessions.
2012
+ *
2013
+ * Callers should bind the result to a local `const client` and use that
2014
+ * within the method instead of `this.client`, so a `'close'` event mid-call
2015
+ * does not race the active operation.
2016
+ */
2017
+ async ensureConnected() {
2018
+ if (this.connectingPromise) {
2019
+ await this.connectingPromise;
2020
+ }
2021
+ if (!this.isClientUsable()) {
2022
+ if (!this.passwordCreds) {
2023
+ throw new Error("Not connected");
2024
+ }
2025
+ this.connectingPromise = (async () => {
2026
+ if (this.client) {
2027
+ try {
2028
+ await this.client.logout();
2029
+ } catch {
2030
+ }
2031
+ this.client = null;
2032
+ }
2033
+ const client = this.buildClient();
2034
+ await client.connect();
2035
+ this.client = client;
2036
+ })().finally(() => {
2037
+ this.connectingPromise = null;
2038
+ });
2039
+ await this.connectingPromise;
2040
+ }
2041
+ if (!this.client) {
2042
+ throw new Error("IMAP client unavailable after reconnect attempt");
2043
+ }
2044
+ return this.client;
1850
2045
  }
1851
2046
  async testConnection() {
1852
2047
  try {
@@ -1857,14 +2052,14 @@ var ImapAdapter = class {
1857
2052
  }
1858
2053
  }
1859
2054
  async listFolders() {
1860
- if (!this.client) throw new Error("Not connected");
1861
- const imapFolders = await this.client.list();
2055
+ const client = await this.ensureConnected();
2056
+ const imapFolders = await client.list();
1862
2057
  return imapFolders.map(mapImapFolder);
1863
2058
  }
1864
2059
  async createFolder(name, parentPath) {
1865
- if (!this.client) throw new Error("Not connected");
2060
+ const client = await this.ensureConnected();
1866
2061
  const fullPath = parentPath ? `${parentPath}/${name}` : name;
1867
- const result = await this.client.mailboxCreate(fullPath);
2062
+ const result = await client.mailboxCreate(fullPath);
1868
2063
  return {
1869
2064
  id: result.path,
1870
2065
  name: result.name || name,
@@ -1893,7 +2088,7 @@ var ImapAdapter = class {
1893
2088
  * and matching by name or special-use type.
1894
2089
  */
1895
2090
  async resolveFolder(folder) {
1896
- if (!this.client) throw new Error("Not connected");
2091
+ const client = await this.ensureConnected();
1897
2092
  const FOLDER_ALIASES = {
1898
2093
  junk: ["Junk", "Spam", "Bulk Mail", "Junk E-mail", "Junk Email"],
1899
2094
  spam: ["Junk", "Spam", "Bulk Mail", "Junk E-mail", "Junk Email"],
@@ -1902,7 +2097,7 @@ var ImapAdapter = class {
1902
2097
  drafts: ["Drafts", "Draft"],
1903
2098
  archive: ["Archive", "All Mail"]
1904
2099
  };
1905
- const folders = await this.client.list();
2100
+ const folders = await client.list();
1906
2101
  const lowerFolder = folder.toLowerCase();
1907
2102
  const exactMatch = folders.find(
1908
2103
  (f) => f.path === folder || f.path.toLowerCase() === lowerFolder
@@ -1940,7 +2135,7 @@ var ImapAdapter = class {
1940
2135
  return folder;
1941
2136
  }
1942
2137
  async search(query) {
1943
- if (!this.client) throw new Error("Not connected");
2138
+ const client = await this.ensureConnected();
1944
2139
  const requestedFolder = query.folder || "INBOX";
1945
2140
  let folder;
1946
2141
  try {
@@ -1949,17 +2144,25 @@ var ImapAdapter = class {
1949
2144
  folder = requestedFolder;
1950
2145
  }
1951
2146
  try {
1952
- await this.client.noop();
2147
+ await client.noop();
1953
2148
  } catch {
1954
2149
  }
1955
2150
  let lock;
1956
2151
  try {
1957
- lock = await this.client.getMailboxLock(folder);
2152
+ lock = await client.getMailboxLock(folder);
1958
2153
  } catch (error) {
1959
2154
  throw formatImapError(error, `Failed to open folder "${folder}"`);
1960
2155
  }
1961
2156
  try {
1962
- const mailboxExists = this.client.mailbox?.exists ?? 0;
2157
+ let mailboxExists = client.mailbox?.exists ?? 0;
2158
+ if (mailboxExists === 0) {
2159
+ try {
2160
+ const status = await client.status(folder, { messages: true });
2161
+ mailboxExists = status?.messages ?? 0;
2162
+ } catch {
2163
+ mailboxExists = 0;
2164
+ }
2165
+ }
1963
2166
  if (mailboxExists === 0) {
1964
2167
  return [];
1965
2168
  }
@@ -1967,14 +2170,14 @@ var ImapAdapter = class {
1967
2170
  const hasCriteria = Object.keys(criteria).length > 0;
1968
2171
  let allUids = [];
1969
2172
  try {
1970
- const searchResult = await this.client.search(
2173
+ const searchResult = await client.search(
1971
2174
  hasCriteria ? criteria : { all: true },
1972
2175
  { uid: true }
1973
2176
  );
1974
2177
  allUids = Array.isArray(searchResult) ? searchResult : [];
1975
2178
  } catch {
1976
2179
  try {
1977
- allUids = await this.collectUidsViaFetch(query);
2180
+ allUids = await this.collectUidsViaFetch(client, query);
1978
2181
  } catch {
1979
2182
  return [];
1980
2183
  }
@@ -1982,7 +2185,7 @@ var ImapAdapter = class {
1982
2185
  const offset = query.offset || 0;
1983
2186
  const slicedUids = query.limit ? allUids.slice(offset, offset + query.limit) : allUids.slice(offset);
1984
2187
  if (slicedUids.length === 0) return [];
1985
- return await this.fetchEmails(slicedUids, folder, query.returnBody);
2188
+ return await this.fetchEmails(client, slicedUids, folder, query.returnBody);
1986
2189
  } catch (error) {
1987
2190
  throw formatImapError(error, `Search failed in folder "${folder}"`);
1988
2191
  } finally {
@@ -1996,13 +2199,12 @@ var ImapAdapter = class {
1996
2199
  * 2. FETCH 1:N using mailbox.exists as explicit range
1997
2200
  * 3. Individual sequence number fetches (slowest but most resilient)
1998
2201
  */
1999
- async collectUidsViaFetch(query) {
2000
- if (!this.client) return [];
2202
+ async collectUidsViaFetch(client, query) {
2001
2203
  const uids = [];
2002
- const exists = this.client.mailbox?.exists ?? 0;
2204
+ const exists = client.mailbox?.exists ?? 0;
2003
2205
  const safeFetch = async (range) => {
2004
2206
  try {
2005
- const messages = await this.client.fetchAll(range, { uid: true, flags: true });
2207
+ const messages = await client.fetchAll(range, { uid: true, flags: true });
2006
2208
  for (const msg of messages) {
2007
2209
  if (query.unreadOnly && msg.flags?.has("\\Seen")) continue;
2008
2210
  if (query.starredOnly && !msg.flags?.has("\\Flagged")) continue;
@@ -2022,7 +2224,7 @@ var ImapAdapter = class {
2022
2224
  const count = exists > 0 ? exists : 50;
2023
2225
  for (let seq = 1; seq <= count; seq++) {
2024
2226
  try {
2025
- const msgs = await this.client.fetchAll(String(seq), { uid: true, flags: true });
2227
+ const msgs = await client.fetchAll(String(seq), { uid: true, flags: true });
2026
2228
  for (const msg of msgs) {
2027
2229
  if (query.unreadOnly && msg.flags?.has("\\Seen")) continue;
2028
2230
  if (query.starredOnly && !msg.flags?.has("\\Flagged")) continue;
@@ -2036,19 +2238,18 @@ var ImapAdapter = class {
2036
2238
  /**
2037
2239
  * Fetches email data for a set of UIDs, either with full body or lightweight headers.
2038
2240
  */
2039
- async fetchEmails(uids, folder, returnBody) {
2040
- if (!this.client) return [];
2241
+ async fetchEmails(client, uids, folder, returnBody) {
2041
2242
  const emails = [];
2042
2243
  const fetchOpts = returnBody ? { source: true, uid: true, flags: true } : { envelope: true, uid: true, flags: true, bodyStructure: true };
2043
2244
  const uidRange = uids.join(",");
2044
2245
  let messages;
2045
2246
  try {
2046
- messages = await this.client.fetchAll(uidRange, fetchOpts, { uid: true });
2247
+ messages = await client.fetchAll(uidRange, fetchOpts, { uid: true });
2047
2248
  } catch {
2048
2249
  messages = [];
2049
2250
  for (const uid of uids) {
2050
2251
  try {
2051
- const batch = await this.client.fetchAll(String(uid), fetchOpts, { uid: true });
2252
+ const batch = await client.fetchAll(String(uid), fetchOpts, { uid: true });
2052
2253
  messages.push(...batch);
2053
2254
  } catch {
2054
2255
  }
@@ -2087,17 +2288,17 @@ var ImapAdapter = class {
2087
2288
  return emails;
2088
2289
  }
2089
2290
  async getEmail(id, folder) {
2090
- if (!this.client) throw new Error("Not connected");
2291
+ const client = await this.ensureConnected();
2091
2292
  const targetFolder = folder || "INBOX";
2092
2293
  let lock;
2093
2294
  try {
2094
- lock = await this.client.getMailboxLock(targetFolder);
2295
+ lock = await client.getMailboxLock(targetFolder);
2095
2296
  } catch (error) {
2096
2297
  throw formatImapError(error, `Failed to open folder "${targetFolder}"`);
2097
2298
  }
2098
2299
  try {
2099
2300
  const uid = parseInt(id, 10);
2100
- const msg = await this.client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
2301
+ const msg = await client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
2101
2302
  if (!msg) throw new Error(`Email ${id} not found`);
2102
2303
  const parsed = await simpleParser(msg.source);
2103
2304
  parsed.flags = msg.flags;
@@ -2107,22 +2308,22 @@ var ImapAdapter = class {
2107
2308
  }
2108
2309
  }
2109
2310
  async getThread(threadId) {
2110
- if (!this.client) throw new Error("Not connected");
2311
+ const client = await this.ensureConnected();
2111
2312
  let lock;
2112
2313
  try {
2113
- lock = await this.client.getMailboxLock("INBOX");
2314
+ lock = await client.getMailboxLock("INBOX");
2114
2315
  } catch (error) {
2115
2316
  throw formatImapError(error, 'Failed to open folder "INBOX"');
2116
2317
  }
2117
2318
  try {
2118
- const searchResult = await this.client.search(
2319
+ const searchResult = await client.search(
2119
2320
  { or: [{ header: { "message-id": threadId } }, { header: { references: threadId } }, { header: { "in-reply-to": threadId } }] },
2120
2321
  { uid: true }
2121
2322
  );
2122
2323
  const uids = Array.isArray(searchResult) ? searchResult : [];
2123
2324
  if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
2124
2325
  const messages = [];
2125
- for await (const msg of this.client.fetch(uids, { source: true, uid: true, flags: true })) {
2326
+ for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
2126
2327
  const parsed = await simpleParser(msg.source);
2127
2328
  parsed.flags = msg.flags;
2128
2329
  messages.push(mapParsedEmail(parsed, "INBOX", this.accountId, msg.uid));
@@ -2148,15 +2349,15 @@ var ImapAdapter = class {
2148
2349
  }
2149
2350
  }
2150
2351
  async getAttachment(emailId, attachmentId) {
2151
- if (!this.client) throw new Error("Not connected");
2352
+ const client = await this.ensureConnected();
2152
2353
  let lock;
2153
2354
  try {
2154
- lock = await this.client.getMailboxLock("INBOX");
2355
+ lock = await client.getMailboxLock("INBOX");
2155
2356
  } catch (error) {
2156
2357
  throw formatImapError(error, 'Failed to open folder "INBOX"');
2157
2358
  }
2158
2359
  try {
2159
- const msg = await this.client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
2360
+ const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
2160
2361
  if (!msg) throw new Error(`Email ${emailId} not found`);
2161
2362
  const parsed = await simpleParser(msg.source);
2162
2363
  const attachment = (parsed.attachments || []).find(
@@ -2182,8 +2383,7 @@ var ImapAdapter = class {
2182
2383
  const messageId = await sendViaSmtp(transport, this.email, params);
2183
2384
  return { id: messageId };
2184
2385
  }
2185
- async createDraft(params) {
2186
- if (!this.client) throw new Error("Not connected");
2386
+ buildDraftRfc2822(params) {
2187
2387
  const lines = [];
2188
2388
  lines.push(`From: ${this.email}`);
2189
2389
  lines.push(`To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`);
@@ -2198,8 +2398,39 @@ var ImapAdapter = class {
2198
2398
  lines.push("Content-Type: text/plain; charset=utf-8");
2199
2399
  lines.push("");
2200
2400
  lines.push(params.body.text || "");
2201
- const rawMessage = lines.join("\r\n");
2202
- const result = await this.client.append("Drafts", rawMessage, ["\\Draft", "\\Seen"]);
2401
+ return lines.join("\r\n");
2402
+ }
2403
+ async createDraft(params) {
2404
+ const client = await this.ensureConnected();
2405
+ const rawMessage = this.buildDraftRfc2822(params);
2406
+ const draftsFolder = await this.resolveFolder("Drafts");
2407
+ const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
2408
+ if (!result) throw new Error("Failed to append draft");
2409
+ return { id: String(result.uid || result) };
2410
+ }
2411
+ /**
2412
+ * IMAP has no in-place message update — a draft is just a message in the
2413
+ * Drafts folder, and IMAP messages are immutable once appended. This
2414
+ * deletes the old revision and appends a new one, so the returned `id`
2415
+ * is a NEW uid; the old id no longer resolves after this call.
2416
+ */
2417
+ async updateDraft(draftId, params, sourceFolder) {
2418
+ const client = await this.ensureConnected();
2419
+ const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
2420
+ let lock;
2421
+ try {
2422
+ lock = await client.getMailboxLock(draftsFolder);
2423
+ } catch (error) {
2424
+ throw formatImapError(error, `Failed to open folder "${draftsFolder}"`);
2425
+ }
2426
+ try {
2427
+ await client.messageDelete(draftId, { uid: true });
2428
+ } finally {
2429
+ lock.release();
2430
+ }
2431
+ const rawMessage = this.buildDraftRfc2822(params);
2432
+ const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
2433
+ if (!result) throw new Error("Failed to append updated draft");
2203
2434
  return { id: String(result.uid || result) };
2204
2435
  }
2205
2436
  async listDrafts(limit, offset) {
@@ -2210,16 +2441,16 @@ var ImapAdapter = class {
2210
2441
  });
2211
2442
  }
2212
2443
  async moveEmail(emailId, targetFolder, sourceFolder) {
2213
- if (!this.client) throw new Error("Not connected");
2444
+ const client = await this.ensureConnected();
2214
2445
  const folder = sourceFolder || "INBOX";
2215
2446
  let lock;
2216
2447
  try {
2217
- lock = await this.client.getMailboxLock(folder);
2448
+ lock = await client.getMailboxLock(folder);
2218
2449
  } catch (error) {
2219
2450
  throw formatImapError(error, `Failed to open folder "${folder}"`);
2220
2451
  }
2221
2452
  try {
2222
- await this.client.messageMove(emailId, targetFolder, { uid: true });
2453
+ await client.messageMove(emailId, targetFolder, { uid: true });
2223
2454
  } finally {
2224
2455
  lock.release();
2225
2456
  }
@@ -2249,44 +2480,44 @@ var ImapAdapter = class {
2249
2480
  }
2250
2481
  }
2251
2482
  async deleteEmail(emailId, permanent, sourceFolder) {
2252
- if (!this.client) throw new Error("Not connected");
2483
+ const client = await this.ensureConnected();
2253
2484
  const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
2254
2485
  const trashFolder = await this.resolveFolder("Trash");
2255
2486
  let lock;
2256
2487
  try {
2257
- lock = await this.client.getMailboxLock(folder);
2488
+ lock = await client.getMailboxLock(folder);
2258
2489
  } catch (error) {
2259
2490
  throw formatImapError(error, `Failed to open folder "${folder}"`);
2260
2491
  }
2261
2492
  try {
2262
2493
  if (permanent || folder === trashFolder) {
2263
- await this.client.messageDelete(emailId, { uid: true });
2494
+ await client.messageDelete(emailId, { uid: true });
2264
2495
  } else {
2265
- await this.client.messageMove(emailId, trashFolder, { uid: true });
2496
+ await client.messageMove(emailId, trashFolder, { uid: true });
2266
2497
  }
2267
2498
  } finally {
2268
2499
  lock.release();
2269
2500
  }
2270
2501
  }
2271
2502
  async markEmail(emailId, flags, sourceFolder) {
2272
- if (!this.client) throw new Error("Not connected");
2503
+ const client = await this.ensureConnected();
2273
2504
  const folder = sourceFolder || "INBOX";
2274
2505
  let lock;
2275
2506
  try {
2276
- lock = await this.client.getMailboxLock(folder);
2507
+ lock = await client.getMailboxLock(folder);
2277
2508
  } catch (error) {
2278
2509
  throw formatImapError(error, `Failed to open folder "${folder}"`);
2279
2510
  }
2280
2511
  try {
2281
2512
  if (flags.read === true) {
2282
- await this.client.messageFlagsAdd(emailId, ["\\Seen"], { uid: true });
2513
+ await client.messageFlagsAdd(emailId, ["\\Seen"], { uid: true });
2283
2514
  } else if (flags.read === false) {
2284
- await this.client.messageFlagsRemove(emailId, ["\\Seen"], { uid: true });
2515
+ await client.messageFlagsRemove(emailId, ["\\Seen"], { uid: true });
2285
2516
  }
2286
2517
  if (flags.starred === true || flags.flagged === true) {
2287
- await this.client.messageFlagsAdd(emailId, ["\\Flagged"], { uid: true });
2518
+ await client.messageFlagsAdd(emailId, ["\\Flagged"], { uid: true });
2288
2519
  } else if (flags.starred === false || flags.flagged === false) {
2289
- await this.client.messageFlagsRemove(emailId, ["\\Flagged"], { uid: true });
2520
+ await client.messageFlagsRemove(emailId, ["\\Flagged"], { uid: true });
2290
2521
  }
2291
2522
  } finally {
2292
2523
  lock.release();
@@ -2319,32 +2550,32 @@ var ImapAdapter = class {
2319
2550
  return { id: String(result?.uid ?? "") };
2320
2551
  }
2321
2552
  async batchDelete(emailIds, permanent, sourceFolder) {
2322
- if (!this.client) throw new Error("Not connected");
2553
+ const client = await this.ensureConnected();
2323
2554
  const result = { succeeded: [], failed: [] };
2324
2555
  const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
2325
2556
  const trashFolder = await this.resolveFolder("Trash");
2326
2557
  const shouldPermanentDelete = permanent || folder === trashFolder;
2327
2558
  let lock;
2328
2559
  try {
2329
- lock = await this.client.getMailboxLock(folder);
2560
+ lock = await client.getMailboxLock(folder);
2330
2561
  } catch (error) {
2331
2562
  throw formatImapError(error, `Failed to open folder "${folder}"`);
2332
2563
  }
2333
2564
  try {
2334
2565
  const uidRange = emailIds.join(",");
2335
2566
  if (shouldPermanentDelete) {
2336
- await this.client.messageDelete(uidRange, { uid: true });
2567
+ await client.messageDelete(uidRange, { uid: true });
2337
2568
  } else {
2338
- await this.client.messageMove(uidRange, trashFolder, { uid: true });
2569
+ await client.messageMove(uidRange, trashFolder, { uid: true });
2339
2570
  }
2340
2571
  result.succeeded = [...emailIds];
2341
2572
  } catch (error) {
2342
2573
  for (const id of emailIds) {
2343
2574
  try {
2344
2575
  if (shouldPermanentDelete) {
2345
- await this.client.messageDelete(id, { uid: true });
2576
+ await client.messageDelete(id, { uid: true });
2346
2577
  } else {
2347
- await this.client.messageMove(id, trashFolder, { uid: true });
2578
+ await client.messageMove(id, trashFolder, { uid: true });
2348
2579
  }
2349
2580
  result.succeeded.push(id);
2350
2581
  } catch (e) {
@@ -2357,23 +2588,23 @@ var ImapAdapter = class {
2357
2588
  return result;
2358
2589
  }
2359
2590
  async batchMove(emailIds, targetFolder, sourceFolder) {
2360
- if (!this.client) throw new Error("Not connected");
2591
+ const client = await this.ensureConnected();
2361
2592
  const result = { succeeded: [], failed: [] };
2362
2593
  const folder = sourceFolder || "INBOX";
2363
2594
  let lock;
2364
2595
  try {
2365
- lock = await this.client.getMailboxLock(folder);
2596
+ lock = await client.getMailboxLock(folder);
2366
2597
  } catch (error) {
2367
2598
  throw formatImapError(error, `Failed to open folder "${folder}"`);
2368
2599
  }
2369
2600
  try {
2370
2601
  const uidRange = emailIds.join(",");
2371
- await this.client.messageMove(uidRange, targetFolder, { uid: true });
2602
+ await client.messageMove(uidRange, targetFolder, { uid: true });
2372
2603
  result.succeeded = [...emailIds];
2373
2604
  } catch (error) {
2374
2605
  for (const id of emailIds) {
2375
2606
  try {
2376
- await this.client.messageMove(id, targetFolder, { uid: true });
2607
+ await client.messageMove(id, targetFolder, { uid: true });
2377
2608
  result.succeeded.push(id);
2378
2609
  } catch (e) {
2379
2610
  result.failed.push({ id, error: e.message });
@@ -2385,26 +2616,26 @@ var ImapAdapter = class {
2385
2616
  return result;
2386
2617
  }
2387
2618
  async batchMark(emailIds, flags, sourceFolder) {
2388
- if (!this.client) throw new Error("Not connected");
2619
+ const client = await this.ensureConnected();
2389
2620
  const result = { succeeded: [], failed: [] };
2390
2621
  const folder = sourceFolder || "INBOX";
2391
2622
  let lock;
2392
2623
  try {
2393
- lock = await this.client.getMailboxLock(folder);
2624
+ lock = await client.getMailboxLock(folder);
2394
2625
  } catch (error) {
2395
2626
  throw formatImapError(error, `Failed to open folder "${folder}"`);
2396
2627
  }
2397
2628
  try {
2398
2629
  const uidRange = emailIds.join(",");
2399
2630
  if (flags.read === true) {
2400
- await this.client.messageFlagsAdd(uidRange, ["\\Seen"], { uid: true });
2631
+ await client.messageFlagsAdd(uidRange, ["\\Seen"], { uid: true });
2401
2632
  } else if (flags.read === false) {
2402
- await this.client.messageFlagsRemove(uidRange, ["\\Seen"], { uid: true });
2633
+ await client.messageFlagsRemove(uidRange, ["\\Seen"], { uid: true });
2403
2634
  }
2404
2635
  if (flags.starred === true || flags.flagged === true) {
2405
- await this.client.messageFlagsAdd(uidRange, ["\\Flagged"], { uid: true });
2636
+ await client.messageFlagsAdd(uidRange, ["\\Flagged"], { uid: true });
2406
2637
  } else if (flags.starred === false || flags.flagged === false) {
2407
- await this.client.messageFlagsRemove(uidRange, ["\\Flagged"], { uid: true });
2638
+ await client.messageFlagsRemove(uidRange, ["\\Flagged"], { uid: true });
2408
2639
  }
2409
2640
  result.succeeded = [...emailIds];
2410
2641
  } catch (error) {
@@ -2443,9 +2674,9 @@ var ICloudAdapter = class extends ImapAdapter {
2443
2674
  };
2444
2675
 
2445
2676
  // src/oauth-config.ts
2446
- var GMAIL_CLIENT_ID = "1062266096576-lu2ukplhc3lcm0bvheml5i18urptdp4t.apps.googleusercontent.com";
2447
- var GMAIL_CLIENT_SECRET = "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";
2448
- var OUTLOOK_CLIENT_ID = "76d32225-ef10-4b49-951c-c80ae906595d";
2677
+ var GMAIL_CLIENT_ID = process.env.EMAIL_MCP_GMAIL_CLIENT_ID || "1062266096576-lu2ukplhc3lcm0bvheml5i18urptdp4t.apps.googleusercontent.com";
2678
+ var GMAIL_CLIENT_SECRET = process.env.EMAIL_MCP_GMAIL_CLIENT_SECRET || "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";
2679
+ var OUTLOOK_CLIENT_ID = process.env.EMAIL_MCP_OUTLOOK_CLIENT_ID || "76d32225-ef10-4b49-951c-c80ae906595d";
2449
2680
 
2450
2681
  // src/account-manager.ts
2451
2682
  function createProvider(provider) {
@@ -2633,14 +2864,30 @@ async function testAndSave(creds) {
2633
2864
  async function promptAccountName(defaultName) {
2634
2865
  return askText("Account name:", defaultName);
2635
2866
  }
2636
- async function setupGmail() {
2867
+ async function setupGmail(scopeModeFlag) {
2637
2868
  console.log("\n--- Gmail Setup ---");
2869
+ if (process.env.EMAIL_MCP_GMAIL_CLIENT_ID) {
2870
+ console.log("Using your own Google OAuth Client ID (EMAIL_MCP_GMAIL_CLIENT_ID).");
2871
+ }
2638
2872
  console.log("A browser window will open for Google authorization.\n");
2873
+ let scopeMode = scopeModeFlag;
2874
+ if (!scopeMode) {
2875
+ scopeMode = await askChoice("Gmail permission scope:", [
2876
+ {
2877
+ name: "Full (default) \u2014 includes permanent delete, bypassing Trash",
2878
+ value: "full"
2879
+ },
2880
+ {
2881
+ name: "Restricted \u2014 read/send/label/archive/trash only, no permanent delete",
2882
+ value: "restricted"
2883
+ }
2884
+ ]);
2885
+ }
2639
2886
  const server = new OAuthCallbackServer();
2640
2887
  const port = await server.start();
2641
2888
  const redirectUri = `http://localhost:${port}/callback`;
2642
2889
  const gmailAuth = new GmailAuth(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, redirectUri);
2643
- const { url, codeVerifier } = gmailAuth.getAuthUrl(redirectUri);
2890
+ const { url, codeVerifier } = gmailAuth.getAuthUrl(redirectUri, scopeMode);
2644
2891
  console.log("\nOpening browser for Google authorization...");
2645
2892
  console.log(`If the browser does not open, visit:
2646
2893
  ${url}
@@ -2839,6 +3086,17 @@ async function main() {
2839
3086
  await removeAccount(id);
2840
3087
  return;
2841
3088
  }
3089
+ const scopeIdx = args.indexOf("--scope");
3090
+ let scopeModeFlag;
3091
+ if (scopeIdx !== -1) {
3092
+ const value = args[scopeIdx + 1];
3093
+ if (value === "full" || value === "restricted") {
3094
+ scopeModeFlag = value;
3095
+ } else {
3096
+ console.error("Usage: --scope <full|restricted>");
3097
+ process.exit(1);
3098
+ }
3099
+ }
2842
3100
  console.log("@marlinjai/email-mcp Account Setup\n");
2843
3101
  let addMore = true;
2844
3102
  while (addMore) {
@@ -2850,7 +3108,7 @@ async function main() {
2850
3108
  ]);
2851
3109
  switch (provider) {
2852
3110
  case ProviderType.Gmail:
2853
- await setupGmail();
3111
+ await setupGmail(scopeModeFlag);
2854
3112
  break;
2855
3113
  case ProviderType.Outlook:
2856
3114
  await setupOutlook();