@marlinjai/email-mcp 1.5.0 → 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 +13 -4
- package/dist/index.js +207 -21
- package/dist/index.js.map +2 -2
- package/dist/setup/wizard.js +142 -21
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +1 -1
package/dist/setup/wizard.js
CHANGED
|
@@ -342,14 +342,33 @@ var OAuthCallbackServer = class {
|
|
|
342
342
|
// src/providers/gmail/auth.ts
|
|
343
343
|
import { OAuth2Client } from "google-auth-library";
|
|
344
344
|
import crypto2 from "node:crypto";
|
|
345
|
-
var GMAIL_SCOPES =
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
+
}
|
|
353
372
|
var GmailAuth = class {
|
|
354
373
|
clientId;
|
|
355
374
|
clientSecret;
|
|
@@ -359,13 +378,13 @@ var GmailAuth = class {
|
|
|
359
378
|
this.clientSecret = clientSecret;
|
|
360
379
|
this.oauth2Client = new OAuth2Client(clientId, clientSecret, redirectUri);
|
|
361
380
|
}
|
|
362
|
-
getAuthUrl(redirectUri) {
|
|
381
|
+
getAuthUrl(redirectUri, scopeMode) {
|
|
363
382
|
const codeVerifier = crypto2.randomBytes(32).toString("base64url");
|
|
364
383
|
const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
365
384
|
this.oauth2Client = new OAuth2Client(this.clientId, this.clientSecret, redirectUri);
|
|
366
385
|
const url = this.oauth2Client.generateAuthUrl({
|
|
367
386
|
access_type: "offline",
|
|
368
|
-
scope: GMAIL_SCOPES,
|
|
387
|
+
scope: GMAIL_SCOPES[resolveScopeMode(scopeMode)],
|
|
369
388
|
prompt: "consent",
|
|
370
389
|
code_challenge: codeChallenge,
|
|
371
390
|
code_challenge_method: "S256"
|
|
@@ -728,11 +747,12 @@ var GmailAdapter = class {
|
|
|
728
747
|
const labels = res.data.labels || [];
|
|
729
748
|
return labels.map(mapGmailLabel);
|
|
730
749
|
}
|
|
731
|
-
async createFolder(name) {
|
|
750
|
+
async createFolder(name, parentPath) {
|
|
732
751
|
const gmail = this.ensureConnected();
|
|
752
|
+
const fullName = parentPath ? `${parentPath}/${name}` : name;
|
|
733
753
|
const res = await gmail.users.labels.create({
|
|
734
754
|
userId: "me",
|
|
735
|
-
requestBody: { name }
|
|
755
|
+
requestBody: { name: fullName }
|
|
736
756
|
});
|
|
737
757
|
return mapGmailLabel(res.data);
|
|
738
758
|
}
|
|
@@ -849,6 +869,18 @@ var GmailAdapter = class {
|
|
|
849
869
|
});
|
|
850
870
|
return { id: res.data.id || "" };
|
|
851
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
|
+
}
|
|
852
884
|
async listDrafts(limit, _offset) {
|
|
853
885
|
const gmail = this.ensureConnected();
|
|
854
886
|
const res = await gmail.users.drafts.list({
|
|
@@ -1012,6 +1044,34 @@ var GmailAdapter = class {
|
|
|
1012
1044
|
}
|
|
1013
1045
|
return result;
|
|
1014
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
|
+
}
|
|
1015
1075
|
async getRawMessage(emailId, _sourceFolder) {
|
|
1016
1076
|
const gmail = this.ensureConnected();
|
|
1017
1077
|
const res = await gmail.users.messages.get({
|
|
@@ -1475,6 +1535,12 @@ var OutlookAdapter = class {
|
|
|
1475
1535
|
const result = await client.api("/me/messages").post(message);
|
|
1476
1536
|
return { id: result.id };
|
|
1477
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
|
+
}
|
|
1478
1544
|
async listDrafts(limit, offset) {
|
|
1479
1545
|
const client = this.ensureClient();
|
|
1480
1546
|
let request = client.api("/me/mailFolders/drafts/messages");
|
|
@@ -2317,8 +2383,7 @@ var ImapAdapter = class {
|
|
|
2317
2383
|
const messageId = await sendViaSmtp(transport, this.email, params);
|
|
2318
2384
|
return { id: messageId };
|
|
2319
2385
|
}
|
|
2320
|
-
|
|
2321
|
-
const client = await this.ensureConnected();
|
|
2386
|
+
buildDraftRfc2822(params) {
|
|
2322
2387
|
const lines = [];
|
|
2323
2388
|
lines.push(`From: ${this.email}`);
|
|
2324
2389
|
lines.push(`To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`);
|
|
@@ -2333,12 +2398,41 @@ var ImapAdapter = class {
|
|
|
2333
2398
|
lines.push("Content-Type: text/plain; charset=utf-8");
|
|
2334
2399
|
lines.push("");
|
|
2335
2400
|
lines.push(params.body.text || "");
|
|
2336
|
-
|
|
2401
|
+
return lines.join("\r\n");
|
|
2402
|
+
}
|
|
2403
|
+
async createDraft(params) {
|
|
2404
|
+
const client = await this.ensureConnected();
|
|
2405
|
+
const rawMessage = this.buildDraftRfc2822(params);
|
|
2337
2406
|
const draftsFolder = await this.resolveFolder("Drafts");
|
|
2338
2407
|
const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
|
|
2339
2408
|
if (!result) throw new Error("Failed to append draft");
|
|
2340
2409
|
return { id: String(result.uid || result) };
|
|
2341
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");
|
|
2434
|
+
return { id: String(result.uid || result) };
|
|
2435
|
+
}
|
|
2342
2436
|
async listDrafts(limit, offset) {
|
|
2343
2437
|
return this.search({
|
|
2344
2438
|
folder: "Drafts",
|
|
@@ -2580,9 +2674,9 @@ var ICloudAdapter = class extends ImapAdapter {
|
|
|
2580
2674
|
};
|
|
2581
2675
|
|
|
2582
2676
|
// src/oauth-config.ts
|
|
2583
|
-
var GMAIL_CLIENT_ID = "1062266096576-lu2ukplhc3lcm0bvheml5i18urptdp4t.apps.googleusercontent.com";
|
|
2584
|
-
var GMAIL_CLIENT_SECRET = "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";
|
|
2585
|
-
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";
|
|
2586
2680
|
|
|
2587
2681
|
// src/account-manager.ts
|
|
2588
2682
|
function createProvider(provider) {
|
|
@@ -2770,14 +2864,30 @@ async function testAndSave(creds) {
|
|
|
2770
2864
|
async function promptAccountName(defaultName) {
|
|
2771
2865
|
return askText("Account name:", defaultName);
|
|
2772
2866
|
}
|
|
2773
|
-
async function setupGmail() {
|
|
2867
|
+
async function setupGmail(scopeModeFlag) {
|
|
2774
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
|
+
}
|
|
2775
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
|
+
}
|
|
2776
2886
|
const server = new OAuthCallbackServer();
|
|
2777
2887
|
const port = await server.start();
|
|
2778
2888
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
2779
2889
|
const gmailAuth = new GmailAuth(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, redirectUri);
|
|
2780
|
-
const { url, codeVerifier } = gmailAuth.getAuthUrl(redirectUri);
|
|
2890
|
+
const { url, codeVerifier } = gmailAuth.getAuthUrl(redirectUri, scopeMode);
|
|
2781
2891
|
console.log("\nOpening browser for Google authorization...");
|
|
2782
2892
|
console.log(`If the browser does not open, visit:
|
|
2783
2893
|
${url}
|
|
@@ -2976,6 +3086,17 @@ async function main() {
|
|
|
2976
3086
|
await removeAccount(id);
|
|
2977
3087
|
return;
|
|
2978
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
|
+
}
|
|
2979
3100
|
console.log("@marlinjai/email-mcp Account Setup\n");
|
|
2980
3101
|
let addMore = true;
|
|
2981
3102
|
while (addMore) {
|
|
@@ -2987,7 +3108,7 @@ async function main() {
|
|
|
2987
3108
|
]);
|
|
2988
3109
|
switch (provider) {
|
|
2989
3110
|
case ProviderType.Gmail:
|
|
2990
|
-
await setupGmail();
|
|
3111
|
+
await setupGmail(scopeModeFlag);
|
|
2991
3112
|
break;
|
|
2992
3113
|
case ProviderType.Outlook:
|
|
2993
3114
|
await setupOutlook();
|