@marlinjai/email-mcp 1.4.4 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -5
- package/dist/index.js +506 -138
- package/dist/index.js.map +3 -3
- package/dist/setup/wizard.js +369 -111
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +1 -1
package/dist/setup/wizard.js
CHANGED
|
@@ -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
|
-
|
|
120
|
-
|
|
120
|
+
var cachedMachineId;
|
|
121
|
+
function getStableMachineId() {
|
|
122
|
+
if (cachedMachineId !== void 0) return cachedMachineId;
|
|
123
|
+
cachedMachineId = computeStableMachineId();
|
|
124
|
+
return cachedMachineId;
|
|
121
125
|
}
|
|
122
|
-
function
|
|
123
|
-
|
|
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
|
|
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
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
let
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
164
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1861
|
-
const imapFolders = await
|
|
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
|
-
|
|
2060
|
+
const client = await this.ensureConnected();
|
|
1866
2061
|
const fullPath = parentPath ? `${parentPath}/${name}` : name;
|
|
1867
|
-
const result = await
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2147
|
+
await client.noop();
|
|
1953
2148
|
} catch {
|
|
1954
2149
|
}
|
|
1955
2150
|
let lock;
|
|
1956
2151
|
try {
|
|
1957
|
-
lock = await
|
|
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
|
-
|
|
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
|
|
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 =
|
|
2204
|
+
const exists = client.mailbox?.exists ?? 0;
|
|
2003
2205
|
const safeFetch = async (range) => {
|
|
2004
2206
|
try {
|
|
2005
|
-
const messages = await
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2291
|
+
const client = await this.ensureConnected();
|
|
2091
2292
|
const targetFolder = folder || "INBOX";
|
|
2092
2293
|
let lock;
|
|
2093
2294
|
try {
|
|
2094
|
-
lock = await
|
|
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
|
|
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
|
-
|
|
2311
|
+
const client = await this.ensureConnected();
|
|
2111
2312
|
let lock;
|
|
2112
2313
|
try {
|
|
2113
|
-
lock = await
|
|
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
|
|
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
|
|
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
|
-
|
|
2352
|
+
const client = await this.ensureConnected();
|
|
2152
2353
|
let lock;
|
|
2153
2354
|
try {
|
|
2154
|
-
lock = await
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2202
|
-
|
|
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
|
-
|
|
2444
|
+
const client = await this.ensureConnected();
|
|
2214
2445
|
const folder = sourceFolder || "INBOX";
|
|
2215
2446
|
let lock;
|
|
2216
2447
|
try {
|
|
2217
|
-
lock = await
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
2494
|
+
await client.messageDelete(emailId, { uid: true });
|
|
2264
2495
|
} else {
|
|
2265
|
-
await
|
|
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
|
-
|
|
2503
|
+
const client = await this.ensureConnected();
|
|
2273
2504
|
const folder = sourceFolder || "INBOX";
|
|
2274
2505
|
let lock;
|
|
2275
2506
|
try {
|
|
2276
|
-
lock = await
|
|
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
|
|
2513
|
+
await client.messageFlagsAdd(emailId, ["\\Seen"], { uid: true });
|
|
2283
2514
|
} else if (flags.read === false) {
|
|
2284
|
-
await
|
|
2515
|
+
await client.messageFlagsRemove(emailId, ["\\Seen"], { uid: true });
|
|
2285
2516
|
}
|
|
2286
2517
|
if (flags.starred === true || flags.flagged === true) {
|
|
2287
|
-
await
|
|
2518
|
+
await client.messageFlagsAdd(emailId, ["\\Flagged"], { uid: true });
|
|
2288
2519
|
} else if (flags.starred === false || flags.flagged === false) {
|
|
2289
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
|
2567
|
+
await client.messageDelete(uidRange, { uid: true });
|
|
2337
2568
|
} else {
|
|
2338
|
-
await
|
|
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
|
|
2576
|
+
await client.messageDelete(id, { uid: true });
|
|
2346
2577
|
} else {
|
|
2347
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
2631
|
+
await client.messageFlagsAdd(uidRange, ["\\Seen"], { uid: true });
|
|
2401
2632
|
} else if (flags.read === false) {
|
|
2402
|
-
await
|
|
2633
|
+
await client.messageFlagsRemove(uidRange, ["\\Seen"], { uid: true });
|
|
2403
2634
|
}
|
|
2404
2635
|
if (flags.starred === true || flags.flagged === true) {
|
|
2405
|
-
await
|
|
2636
|
+
await client.messageFlagsAdd(uidRange, ["\\Flagged"], { uid: true });
|
|
2406
2637
|
} else if (flags.starred === false || flags.flagged === false) {
|
|
2407
|
-
await
|
|
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();
|