@marlinjai/email-mcp 1.7.2 → 1.8.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 +16 -1
- package/dist/index.js +283 -73
- package/dist/index.js.map +2 -2
- package/dist/setup/wizard.js +271 -64
- package/dist/setup/wizard.js.map +3 -3
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
|
|
|
123
123
|
|------|-------------|
|
|
124
124
|
| `email_list_accounts` | List all configured accounts with connection status |
|
|
125
125
|
| `email_add_account` | Add a new IMAP or iCloud account (Gmail/Outlook require setup wizard) |
|
|
126
|
-
| `email_remove_account` | Remove an account and its stored credentials |
|
|
126
|
+
| `email_remove_account` | Remove an account and its stored credentials; revokes the Google grant for Gmail, removes Outlook tokens from the local token cache, and reports the outcome |
|
|
127
127
|
| `email_test_account` | Test connection to an account |
|
|
128
128
|
|
|
129
129
|
### Reading & Searching (6)
|
|
@@ -248,6 +248,21 @@ export EMAIL_MCP_KEY="your-strong-passphrase"
|
|
|
248
248
|
When `EMAIL_MCP_KEY` is set, existing credential files are transparently
|
|
249
249
|
re-encrypted with the passphrase the next time they are read.
|
|
250
250
|
|
|
251
|
+
The Outlook refresh token lives in the token cache of Microsoft's authentication
|
|
252
|
+
library (MSAL), `~/.email-mcp/msal-cache.enc`, encrypted with the same scheme and key
|
|
253
|
+
derivation as `credentials.enc`, so `EMAIL_MCP_KEY` protects both files. Versions before 1.8.0
|
|
254
|
+
kept this cache as plain JSON in `~/.email-mcp/msal-cache.json`; 1.8.0 encrypts it and
|
|
255
|
+
deletes the plain file the first time it reads it, without signing you out. Going back
|
|
256
|
+
to an older version afterwards means signing in to Outlook again.
|
|
257
|
+
|
|
258
|
+
On macOS and Linux, attachments saved with `email_save_attachment` are written
|
|
259
|
+
owner-only (`0600`), and folders email-mcp creates for them are `0700`. They are not
|
|
260
|
+
encrypted.
|
|
261
|
+
|
|
262
|
+
The OAuth sign-in callback started by `email-mcp-setup` listens on the loopback
|
|
263
|
+
addresses only (`127.0.0.1`, and `::1` when available), so nothing else on your network
|
|
264
|
+
can reach it.
|
|
265
|
+
|
|
251
266
|
## Support
|
|
252
267
|
|
|
253
268
|
If this project is useful to you, consider supporting its development:
|
package/dist/index.js
CHANGED
|
@@ -123,6 +123,14 @@ function decrypt(ciphertext, seeds) {
|
|
|
123
123
|
}
|
|
124
124
|
throw lastError ?? new Error("Unable to decrypt credentials");
|
|
125
125
|
}
|
|
126
|
+
function encryptWithMachineKey(plaintext) {
|
|
127
|
+
return encrypt(plaintext, getSeedCandidates()[0]);
|
|
128
|
+
}
|
|
129
|
+
function decryptWithMachineKey(ciphertext) {
|
|
130
|
+
const seeds = getSeedCandidates();
|
|
131
|
+
const { plaintext, seedUsed } = decrypt(ciphertext, seeds);
|
|
132
|
+
return { plaintext, usedLegacyKey: seedUsed !== seeds[0] };
|
|
133
|
+
}
|
|
126
134
|
var ALGORITHM, KEY_LENGTH, IV_LENGTH, SALT_LENGTH, PBKDF2_ITERATIONS, cachedMachineId, CredentialStore;
|
|
127
135
|
var init_credential_store = __esm({
|
|
128
136
|
"src/auth/credential-store.ts"() {
|
|
@@ -146,10 +154,9 @@ var init_credential_store = __esm({
|
|
|
146
154
|
return { accounts: {} };
|
|
147
155
|
}
|
|
148
156
|
const raw = fs.readFileSync(this.filePath, "utf-8");
|
|
149
|
-
const
|
|
150
|
-
const { plaintext, seedUsed } = decrypt(raw, seeds);
|
|
157
|
+
const { plaintext, usedLegacyKey } = decryptWithMachineKey(raw);
|
|
151
158
|
const data = JSON.parse(plaintext);
|
|
152
|
-
if (
|
|
159
|
+
if (usedLegacyKey) {
|
|
153
160
|
try {
|
|
154
161
|
this.write(data);
|
|
155
162
|
} catch {
|
|
@@ -159,7 +166,7 @@ var init_credential_store = __esm({
|
|
|
159
166
|
}
|
|
160
167
|
write(data) {
|
|
161
168
|
const json2 = JSON.stringify(data);
|
|
162
|
-
const encrypted =
|
|
169
|
+
const encrypted = encryptWithMachineKey(json2);
|
|
163
170
|
fs.writeFileSync(this.filePath, encrypted, { mode: 384 });
|
|
164
171
|
}
|
|
165
172
|
async save(creds) {
|
|
@@ -7683,7 +7690,41 @@ function resolveScopeMode(explicit) {
|
|
|
7683
7690
|
if (explicit) return explicit;
|
|
7684
7691
|
return process.env.EMAIL_MCP_GMAIL_SCOPE?.toLowerCase() === "restricted" ? "restricted" : "full";
|
|
7685
7692
|
}
|
|
7686
|
-
|
|
7693
|
+
async function revokeGoogleGrant(token) {
|
|
7694
|
+
if (!token) {
|
|
7695
|
+
return {
|
|
7696
|
+
ok: false,
|
|
7697
|
+
detail: `No Google token was stored for this account, so nothing could be revoked. Remove email-mcp manually at ${GOOGLE_PERMISSIONS_URL}.`
|
|
7698
|
+
};
|
|
7699
|
+
}
|
|
7700
|
+
try {
|
|
7701
|
+
const res = await fetch(GOOGLE_REVOKE_ENDPOINT, {
|
|
7702
|
+
method: "POST",
|
|
7703
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
7704
|
+
body: new URLSearchParams({ token }).toString(),
|
|
7705
|
+
signal: AbortSignal.timeout(REVOKE_TIMEOUT_MS)
|
|
7706
|
+
});
|
|
7707
|
+
if (res.ok) {
|
|
7708
|
+
return { ok: true, detail: "Google revoked the grant: email-mcp no longer has access to this Gmail account." };
|
|
7709
|
+
}
|
|
7710
|
+
let reason = "";
|
|
7711
|
+
try {
|
|
7712
|
+
const body = await res.json();
|
|
7713
|
+
reason = [body.error, body.error_description].filter(Boolean).join(": ");
|
|
7714
|
+
} catch {
|
|
7715
|
+
}
|
|
7716
|
+
return {
|
|
7717
|
+
ok: false,
|
|
7718
|
+
detail: `Google refused the revocation (HTTP ${res.status}${reason ? `, ${reason}` : ""}). The grant may already be revoked or expired. Check and remove email-mcp manually at ${GOOGLE_PERMISSIONS_URL}.`
|
|
7719
|
+
};
|
|
7720
|
+
} catch (err) {
|
|
7721
|
+
return {
|
|
7722
|
+
ok: false,
|
|
7723
|
+
detail: `Could not reach Google's revocation endpoint (${err?.message ?? String(err)}). Remove email-mcp manually at ${GOOGLE_PERMISSIONS_URL}.`
|
|
7724
|
+
};
|
|
7725
|
+
}
|
|
7726
|
+
}
|
|
7727
|
+
var GMAIL_SCOPES, GOOGLE_REVOKE_ENDPOINT, GOOGLE_PERMISSIONS_URL, REVOKE_TIMEOUT_MS, GmailAuth;
|
|
7687
7728
|
var init_auth = __esm({
|
|
7688
7729
|
"src/providers/gmail/auth.ts"() {
|
|
7689
7730
|
"use strict";
|
|
@@ -7710,6 +7751,9 @@ var init_auth = __esm({
|
|
|
7710
7751
|
"https://www.googleapis.com/auth/gmail.settings.basic"
|
|
7711
7752
|
]
|
|
7712
7753
|
};
|
|
7754
|
+
GOOGLE_REVOKE_ENDPOINT = "https://oauth2.googleapis.com/revoke";
|
|
7755
|
+
GOOGLE_PERMISSIONS_URL = "https://myaccount.google.com/permissions";
|
|
7756
|
+
REVOKE_TIMEOUT_MS = 1e4;
|
|
7713
7757
|
GmailAuth = class {
|
|
7714
7758
|
clientId;
|
|
7715
7759
|
clientSecret;
|
|
@@ -8391,31 +8435,82 @@ import {
|
|
|
8391
8435
|
PublicClientApplication,
|
|
8392
8436
|
CryptoProvider
|
|
8393
8437
|
} from "@azure/msal-node";
|
|
8394
|
-
function
|
|
8438
|
+
function writeEncryptedCache(cacheFilePath, serialized) {
|
|
8439
|
+
const dir = path2.dirname(cacheFilePath);
|
|
8440
|
+
if (!fs2.existsSync(dir)) {
|
|
8441
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
8442
|
+
}
|
|
8443
|
+
const tmpPath = `${cacheFilePath}.${process.pid}.tmp`;
|
|
8444
|
+
fs2.writeFileSync(tmpPath, encryptWithMachineKey(serialized), { mode: 384 });
|
|
8445
|
+
fs2.chmodSync(tmpPath, 384);
|
|
8446
|
+
fs2.renameSync(tmpPath, cacheFilePath);
|
|
8447
|
+
}
|
|
8448
|
+
function loadSerializedCache(cacheFilePath, legacyPlaintextPath) {
|
|
8449
|
+
const hasEncrypted = fs2.existsSync(cacheFilePath);
|
|
8450
|
+
if (fs2.existsSync(legacyPlaintextPath)) {
|
|
8451
|
+
const plaintextIsNewer = !hasEncrypted || fs2.statSync(legacyPlaintextPath).mtimeMs > fs2.statSync(cacheFilePath).mtimeMs;
|
|
8452
|
+
if (plaintextIsNewer) {
|
|
8453
|
+
const serialized = fs2.readFileSync(legacyPlaintextPath, "utf-8");
|
|
8454
|
+
writeEncryptedCache(cacheFilePath, serialized);
|
|
8455
|
+
fs2.unlinkSync(legacyPlaintextPath);
|
|
8456
|
+
return serialized;
|
|
8457
|
+
}
|
|
8458
|
+
fs2.unlinkSync(legacyPlaintextPath);
|
|
8459
|
+
}
|
|
8460
|
+
if (!hasEncrypted) return null;
|
|
8461
|
+
const raw = fs2.readFileSync(cacheFilePath, "utf-8");
|
|
8462
|
+
let plaintext;
|
|
8463
|
+
let usedLegacyKey;
|
|
8464
|
+
try {
|
|
8465
|
+
({ plaintext, usedLegacyKey } = decryptWithMachineKey(raw));
|
|
8466
|
+
} catch {
|
|
8467
|
+
throw new MsalCacheDecryptError(cacheFilePath);
|
|
8468
|
+
}
|
|
8469
|
+
if (usedLegacyKey) {
|
|
8470
|
+
try {
|
|
8471
|
+
writeEncryptedCache(cacheFilePath, plaintext);
|
|
8472
|
+
} catch {
|
|
8473
|
+
}
|
|
8474
|
+
}
|
|
8475
|
+
return plaintext;
|
|
8476
|
+
}
|
|
8477
|
+
function createEncryptedFileCachePlugin(cacheFilePath, legacyPlaintextPath = path2.join(path2.dirname(cacheFilePath), LEGACY_PLAINTEXT_CACHE_NAME)) {
|
|
8478
|
+
let loadFailed = false;
|
|
8395
8479
|
return {
|
|
8396
8480
|
async beforeCacheAccess(context) {
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8481
|
+
loadFailed = false;
|
|
8482
|
+
try {
|
|
8483
|
+
const serialized = loadSerializedCache(cacheFilePath, legacyPlaintextPath);
|
|
8484
|
+
if (serialized !== null) {
|
|
8485
|
+
context.tokenCache.deserialize(serialized);
|
|
8486
|
+
}
|
|
8487
|
+
} catch (err) {
|
|
8488
|
+
loadFailed = true;
|
|
8489
|
+
throw err;
|
|
8400
8490
|
}
|
|
8401
8491
|
},
|
|
8402
8492
|
async afterCacheAccess(context) {
|
|
8403
|
-
if (context.cacheHasChanged)
|
|
8404
|
-
|
|
8405
|
-
if (!fs2.existsSync(dir)) {
|
|
8406
|
-
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
8407
|
-
}
|
|
8408
|
-
fs2.writeFileSync(cacheFilePath, context.tokenCache.serialize(), { mode: 384 });
|
|
8409
|
-
}
|
|
8493
|
+
if (loadFailed || !context.cacheHasChanged) return;
|
|
8494
|
+
writeEncryptedCache(cacheFilePath, context.tokenCache.serialize());
|
|
8410
8495
|
}
|
|
8411
8496
|
};
|
|
8412
8497
|
}
|
|
8413
|
-
var DEFAULT_CACHE_DIR, DEFAULT_CACHE_FILE, OutlookAuth;
|
|
8498
|
+
var DEFAULT_CACHE_DIR, DEFAULT_CACHE_FILE, LEGACY_PLAINTEXT_CACHE_NAME, MsalCacheDecryptError, OutlookAuth;
|
|
8414
8499
|
var init_auth2 = __esm({
|
|
8415
8500
|
"src/providers/outlook/auth.ts"() {
|
|
8416
8501
|
"use strict";
|
|
8502
|
+
init_credential_store();
|
|
8417
8503
|
DEFAULT_CACHE_DIR = path2.join(os2.homedir(), ".email-mcp");
|
|
8418
|
-
DEFAULT_CACHE_FILE = path2.join(DEFAULT_CACHE_DIR, "msal-cache.
|
|
8504
|
+
DEFAULT_CACHE_FILE = path2.join(DEFAULT_CACHE_DIR, "msal-cache.enc");
|
|
8505
|
+
LEGACY_PLAINTEXT_CACHE_NAME = "msal-cache.json";
|
|
8506
|
+
MsalCacheDecryptError = class extends Error {
|
|
8507
|
+
constructor(cacheFilePath) {
|
|
8508
|
+
super(
|
|
8509
|
+
`The Outlook token cache ${cacheFilePath} could not be decrypted: it was modified, is corrupt, or was written with a different key (EMAIL_MCP_KEY or another machine). Delete that file and re-run email-mcp-setup to sign in to Outlook again.`
|
|
8510
|
+
);
|
|
8511
|
+
this.name = "MsalCacheDecryptError";
|
|
8512
|
+
}
|
|
8513
|
+
};
|
|
8419
8514
|
OutlookAuth = class _OutlookAuth {
|
|
8420
8515
|
static AUTHORITY = "https://login.microsoftonline.com/consumers";
|
|
8421
8516
|
// MailboxSettings.ReadWrite is required for the messageRules API (block
|
|
@@ -8431,7 +8526,7 @@ var init_auth2 = __esm({
|
|
|
8431
8526
|
authority: _OutlookAuth.AUTHORITY
|
|
8432
8527
|
},
|
|
8433
8528
|
cache: {
|
|
8434
|
-
cachePlugin:
|
|
8529
|
+
cachePlugin: createEncryptedFileCachePlugin(cacheFilePath ?? DEFAULT_CACHE_FILE)
|
|
8435
8530
|
}
|
|
8436
8531
|
});
|
|
8437
8532
|
this.cryptoProvider = new CryptoProvider();
|
|
@@ -8485,6 +8580,27 @@ var init_auth2 = __esm({
|
|
|
8485
8580
|
homeAccountId: result.account?.homeAccountId ?? homeAccountId
|
|
8486
8581
|
};
|
|
8487
8582
|
}
|
|
8583
|
+
/**
|
|
8584
|
+
* Removes one account (its refresh, access and ID tokens) from the MSAL
|
|
8585
|
+
* cache. Matches by home account id, or by username for credentials saved
|
|
8586
|
+
* before the home account id was recorded. Returns whether an entry was
|
|
8587
|
+
* found and removed.
|
|
8588
|
+
*/
|
|
8589
|
+
async removeCachedAccount(match) {
|
|
8590
|
+
const cache = this.pca.getTokenCache();
|
|
8591
|
+
let account = null;
|
|
8592
|
+
if (match.homeAccountId) {
|
|
8593
|
+
account = await cache.getAccountByHomeId(match.homeAccountId);
|
|
8594
|
+
}
|
|
8595
|
+
if (!account && match.username) {
|
|
8596
|
+
const wanted = match.username.trim().toLowerCase();
|
|
8597
|
+
const all = await cache.getAllAccounts();
|
|
8598
|
+
account = all.find((a) => (a.username ?? "").trim().toLowerCase() === wanted) ?? null;
|
|
8599
|
+
}
|
|
8600
|
+
if (!account) return false;
|
|
8601
|
+
await cache.removeAccount(account);
|
|
8602
|
+
return true;
|
|
8603
|
+
}
|
|
8488
8604
|
/**
|
|
8489
8605
|
* @deprecated Use refreshTokenSilent() instead.
|
|
8490
8606
|
* Kept for backward compatibility with credentials that have a stored refresh token.
|
|
@@ -8754,7 +8870,7 @@ var init_adapter3 = __esm({
|
|
|
8754
8870
|
const result = await client.mailboxCreate(fullPath);
|
|
8755
8871
|
return {
|
|
8756
8872
|
id: result.path,
|
|
8757
|
-
name
|
|
8873
|
+
name,
|
|
8758
8874
|
path: result.path,
|
|
8759
8875
|
type: FolderType.Other
|
|
8760
8876
|
};
|
|
@@ -8992,6 +9108,7 @@ var init_adapter3 = __esm({
|
|
|
8992
9108
|
const uid = parseInt(id, 10);
|
|
8993
9109
|
const msg = await client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
|
|
8994
9110
|
if (!msg) throw new Error(`Email ${id} not found`);
|
|
9111
|
+
if (!msg.source) throw new Error(`Email ${id} has no source`);
|
|
8995
9112
|
const parsed = await simpleParser(msg.source);
|
|
8996
9113
|
parsed.flags = msg.flags;
|
|
8997
9114
|
return mapParsedEmail(parsed, targetFolder, this.accountId, msg.uid);
|
|
@@ -9016,6 +9133,7 @@ var init_adapter3 = __esm({
|
|
|
9016
9133
|
if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
|
|
9017
9134
|
const messages = [];
|
|
9018
9135
|
for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
|
|
9136
|
+
if (!msg.source) continue;
|
|
9019
9137
|
const parsed = await simpleParser(msg.source);
|
|
9020
9138
|
parsed.flags = msg.flags;
|
|
9021
9139
|
messages.push(mapParsedEmail(parsed, "INBOX", this.accountId, msg.uid));
|
|
@@ -9051,6 +9169,7 @@ var init_adapter3 = __esm({
|
|
|
9051
9169
|
try {
|
|
9052
9170
|
const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
|
|
9053
9171
|
if (!msg) throw new Error(`Email ${emailId} not found`);
|
|
9172
|
+
if (!msg.source) throw new Error(`Email ${emailId} has no source`);
|
|
9054
9173
|
const parsed = await simpleParser(msg.source);
|
|
9055
9174
|
const attachment = (parsed.attachments || []).find(
|
|
9056
9175
|
(att) => att.contentId === attachmentId || att.filename === attachmentId
|
|
@@ -9358,16 +9477,16 @@ var init_adapter4 = __esm({
|
|
|
9358
9477
|
ICloudAdapter = class extends ImapAdapter {
|
|
9359
9478
|
providerType = ProviderType.ICloud;
|
|
9360
9479
|
async connect(credentials) {
|
|
9480
|
+
const passwordDefaults = {
|
|
9481
|
+
host: "imap.mail.me.com",
|
|
9482
|
+
port: 993,
|
|
9483
|
+
tls: true,
|
|
9484
|
+
smtpHost: "smtp.mail.me.com",
|
|
9485
|
+
smtpPort: 587
|
|
9486
|
+
};
|
|
9361
9487
|
const withDefaults = {
|
|
9362
9488
|
...credentials,
|
|
9363
|
-
password: {
|
|
9364
|
-
host: "imap.mail.me.com",
|
|
9365
|
-
port: 993,
|
|
9366
|
-
tls: true,
|
|
9367
|
-
smtpHost: "smtp.mail.me.com",
|
|
9368
|
-
smtpPort: 587,
|
|
9369
|
-
...credentials.password
|
|
9370
|
-
}
|
|
9489
|
+
password: { ...passwordDefaults, ...credentials.password }
|
|
9371
9490
|
};
|
|
9372
9491
|
return super.connect(withDefaults);
|
|
9373
9492
|
}
|
|
@@ -9401,7 +9520,7 @@ function createProvider(provider) {
|
|
|
9401
9520
|
throw new Error(`Unknown provider: ${provider}`);
|
|
9402
9521
|
}
|
|
9403
9522
|
}
|
|
9404
|
-
var AccountManager;
|
|
9523
|
+
var MICROSOFT_CONSENT_URL, AccountManager;
|
|
9405
9524
|
var init_account_manager = __esm({
|
|
9406
9525
|
"src/account-manager.ts"() {
|
|
9407
9526
|
"use strict";
|
|
@@ -9414,6 +9533,7 @@ var init_account_manager = __esm({
|
|
|
9414
9533
|
init_adapter3();
|
|
9415
9534
|
init_types();
|
|
9416
9535
|
init_oauth_config();
|
|
9536
|
+
MICROSOFT_CONSENT_URL = "https://account.live.com/consent/Manage";
|
|
9417
9537
|
AccountManager = class {
|
|
9418
9538
|
store;
|
|
9419
9539
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -9468,9 +9588,55 @@ var init_account_manager = __esm({
|
|
|
9468
9588
|
await this.store.save(creds);
|
|
9469
9589
|
await this.connectAccount(creds.id);
|
|
9470
9590
|
}
|
|
9591
|
+
/**
|
|
9592
|
+
* Removes an account locally, then cleans up at the provider as far as the
|
|
9593
|
+
* provider allows. Local removal always happens first and never depends on
|
|
9594
|
+
* the network; a failed provider step is reported in the result, not thrown.
|
|
9595
|
+
*/
|
|
9471
9596
|
async removeAccount(accountId) {
|
|
9597
|
+
const creds = await this.store.get(accountId);
|
|
9598
|
+
if (!creds) throw new Error(`Account ${accountId} not found`);
|
|
9472
9599
|
await this.disconnectAccount(accountId);
|
|
9473
9600
|
await this.store.remove(accountId);
|
|
9601
|
+
const result = {
|
|
9602
|
+
success: true,
|
|
9603
|
+
accountId,
|
|
9604
|
+
provider: creds.provider,
|
|
9605
|
+
email: creds.email,
|
|
9606
|
+
revocation: { status: "not_applicable", detail: "" }
|
|
9607
|
+
};
|
|
9608
|
+
if (creds.provider === ProviderType.Gmail) {
|
|
9609
|
+
const token = creds.oauth?.refresh_token || creds.oauth?.access_token || "";
|
|
9610
|
+
const { ok, detail } = await revokeGoogleGrant(token);
|
|
9611
|
+
result.revocation = { status: ok ? "revoked" : "failed", detail };
|
|
9612
|
+
} else if (creds.provider === ProviderType.Outlook) {
|
|
9613
|
+
result.tokenCache = await this.removeFromMsalCache(creds);
|
|
9614
|
+
result.revocation = {
|
|
9615
|
+
status: "local_only",
|
|
9616
|
+
detail: `Microsoft offers no endpoint to revoke a refresh token for a personal Microsoft account, so the grant still exists at Microsoft until it expires. To cancel it now, remove email-mcp at ${MICROSOFT_CONSENT_URL}.`
|
|
9617
|
+
};
|
|
9618
|
+
} else {
|
|
9619
|
+
result.revocation = {
|
|
9620
|
+
status: "not_applicable",
|
|
9621
|
+
detail: "This account used a password, not an OAuth grant, so there is nothing to revoke. The password itself stays valid at your provider; if it was an app-specific password (for example from Apple), delete it there if you no longer need it."
|
|
9622
|
+
};
|
|
9623
|
+
}
|
|
9624
|
+
return result;
|
|
9625
|
+
}
|
|
9626
|
+
async removeFromMsalCache(creds) {
|
|
9627
|
+
try {
|
|
9628
|
+
const outlookAuth = new OutlookAuth(OUTLOOK_CLIENT_ID);
|
|
9629
|
+
const removed = await outlookAuth.removeCachedAccount({
|
|
9630
|
+
homeAccountId: creds.oauth?.msal_home_account_id,
|
|
9631
|
+
username: creds.email
|
|
9632
|
+
});
|
|
9633
|
+
return removed ? { status: "removed", detail: "The account's refresh and access tokens were removed from the local Outlook token cache." } : { status: "not_found", detail: "The local Outlook token cache held no tokens for this account." };
|
|
9634
|
+
} catch (err) {
|
|
9635
|
+
return {
|
|
9636
|
+
status: "failed",
|
|
9637
|
+
detail: `The account's tokens could not be removed from the local Outlook token cache: ${err?.message ?? String(err)}`
|
|
9638
|
+
};
|
|
9639
|
+
}
|
|
9474
9640
|
}
|
|
9475
9641
|
async disconnectAccount(accountId) {
|
|
9476
9642
|
const provider = this.providers.get(accountId);
|
|
@@ -9671,53 +9837,95 @@ var init_oauth_server = __esm({
|
|
|
9671
9837
|
<p>An error occurred during authorization. Please try again.</p>
|
|
9672
9838
|
</body>
|
|
9673
9839
|
</html>`;
|
|
9674
|
-
OAuthCallbackServer = class {
|
|
9675
|
-
|
|
9840
|
+
OAuthCallbackServer = class _OAuthCallbackServer {
|
|
9841
|
+
servers = [];
|
|
9676
9842
|
resolveCode = null;
|
|
9677
9843
|
rejectCode = null;
|
|
9678
9844
|
timeoutMs;
|
|
9679
9845
|
timeoutHandle = null;
|
|
9680
9846
|
expectedState = null;
|
|
9847
|
+
static IPV4_LOOPBACK = "127.0.0.1";
|
|
9848
|
+
static IPV6_LOOPBACK = "::1";
|
|
9849
|
+
/** Attempts at finding a port that is free on both loopback addresses. */
|
|
9850
|
+
static MAX_PORT_ATTEMPTS = 5;
|
|
9681
9851
|
constructor(timeoutMs = 12e4) {
|
|
9682
9852
|
this.timeoutMs = timeoutMs;
|
|
9683
9853
|
}
|
|
9684
|
-
|
|
9854
|
+
handleRequest = (req, res) => {
|
|
9855
|
+
const url2 = new URL(req.url || "/", `http://localhost`);
|
|
9856
|
+
if (url2.pathname === "/callback" || url2.pathname === "/") {
|
|
9857
|
+
const code = url2.searchParams.get("code");
|
|
9858
|
+
const error48 = url2.searchParams.get("error");
|
|
9859
|
+
const state = url2.searchParams.get("state");
|
|
9860
|
+
if (code && this.expectedState !== null && state !== this.expectedState) {
|
|
9861
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
9862
|
+
res.end(ERROR_HTML);
|
|
9863
|
+
this.rejectCode?.(new Error("OAuth error: state mismatch, callback did not come from the authorization request we started"));
|
|
9864
|
+
} else if (code) {
|
|
9865
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
9866
|
+
res.end(SUCCESS_HTML);
|
|
9867
|
+
this.resolveCode?.(code);
|
|
9868
|
+
} else {
|
|
9869
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
9870
|
+
res.end(ERROR_HTML);
|
|
9871
|
+
this.rejectCode?.(new Error(`OAuth error: ${error48 || "no code received"}`));
|
|
9872
|
+
}
|
|
9873
|
+
} else {
|
|
9874
|
+
res.writeHead(404);
|
|
9875
|
+
res.end("Not Found");
|
|
9876
|
+
}
|
|
9877
|
+
};
|
|
9878
|
+
listenOn(host, port) {
|
|
9685
9879
|
return new Promise((resolve, reject) => {
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
} else if (code) {
|
|
9697
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
9698
|
-
res.end(SUCCESS_HTML);
|
|
9699
|
-
this.resolveCode?.(code);
|
|
9700
|
-
} else {
|
|
9701
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
9702
|
-
res.end(ERROR_HTML);
|
|
9703
|
-
this.rejectCode?.(new Error(`OAuth error: ${error48 || "no code received"}`));
|
|
9704
|
-
}
|
|
9705
|
-
} else {
|
|
9706
|
-
res.writeHead(404);
|
|
9707
|
-
res.end("Not Found");
|
|
9708
|
-
}
|
|
9709
|
-
});
|
|
9710
|
-
this.server.listen(0, () => {
|
|
9711
|
-
const addr = this.server.address();
|
|
9712
|
-
if (addr && typeof addr === "object") {
|
|
9713
|
-
resolve(addr.port);
|
|
9714
|
-
} else {
|
|
9715
|
-
reject(new Error("Failed to get server port"));
|
|
9716
|
-
}
|
|
9880
|
+
const server = http.createServer(this.handleRequest);
|
|
9881
|
+
const onError = (err) => {
|
|
9882
|
+
server.close();
|
|
9883
|
+
reject(err);
|
|
9884
|
+
};
|
|
9885
|
+
server.once("error", onError);
|
|
9886
|
+
server.listen(port, host, () => {
|
|
9887
|
+
server.off("error", onError);
|
|
9888
|
+
server.on("error", (err) => this.rejectCode?.(err));
|
|
9889
|
+
resolve(server);
|
|
9717
9890
|
});
|
|
9718
|
-
this.server.on("error", reject);
|
|
9719
9891
|
});
|
|
9720
9892
|
}
|
|
9893
|
+
/**
|
|
9894
|
+
* Starts listening on an OS-assigned port on 127.0.0.1 and, when available,
|
|
9895
|
+
* the same port on ::1. Resolves with the port.
|
|
9896
|
+
*/
|
|
9897
|
+
async start() {
|
|
9898
|
+
let lastError;
|
|
9899
|
+
for (let attempt = 0; attempt < _OAuthCallbackServer.MAX_PORT_ATTEMPTS; attempt++) {
|
|
9900
|
+
const v4 = await this.listenOn(_OAuthCallbackServer.IPV4_LOOPBACK, 0);
|
|
9901
|
+
const addr = v4.address();
|
|
9902
|
+
if (!addr || typeof addr !== "object") {
|
|
9903
|
+
v4.close();
|
|
9904
|
+
throw new Error("Failed to get server port");
|
|
9905
|
+
}
|
|
9906
|
+
const port = addr.port;
|
|
9907
|
+
try {
|
|
9908
|
+
const v6 = await this.listenOn(_OAuthCallbackServer.IPV6_LOOPBACK, port);
|
|
9909
|
+
this.servers = [v4, v6];
|
|
9910
|
+
return port;
|
|
9911
|
+
} catch (err) {
|
|
9912
|
+
if (err?.code === "EADDRINUSE") {
|
|
9913
|
+
v4.close();
|
|
9914
|
+
lastError = err;
|
|
9915
|
+
continue;
|
|
9916
|
+
}
|
|
9917
|
+
this.servers = [v4];
|
|
9918
|
+
return port;
|
|
9919
|
+
}
|
|
9920
|
+
}
|
|
9921
|
+
throw new Error(
|
|
9922
|
+
`Could not find a port free on both loopback addresses: ${lastError?.message ?? "unknown error"}`
|
|
9923
|
+
);
|
|
9924
|
+
}
|
|
9925
|
+
/** The addresses the callback server is listening on (for diagnostics and tests). */
|
|
9926
|
+
boundAddresses() {
|
|
9927
|
+
return this.servers.map((s) => s.address()).filter((a) => !!a && typeof a === "object").map((a) => a.address);
|
|
9928
|
+
}
|
|
9721
9929
|
/**
|
|
9722
9930
|
* Resolve with the authorization code once the browser is redirected back.
|
|
9723
9931
|
* Pass the `state` that was put into the authorization URL: callbacks whose
|
|
@@ -9745,10 +9953,10 @@ var init_oauth_server = __esm({
|
|
|
9745
9953
|
clearTimeout(this.timeoutHandle);
|
|
9746
9954
|
this.timeoutHandle = null;
|
|
9747
9955
|
}
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
this.server = null;
|
|
9956
|
+
for (const server of this.servers) {
|
|
9957
|
+
server.close();
|
|
9751
9958
|
}
|
|
9959
|
+
this.servers = [];
|
|
9752
9960
|
}
|
|
9753
9961
|
};
|
|
9754
9962
|
}
|
|
@@ -33327,14 +33535,14 @@ function registerAccountTools(server, accountManager) {
|
|
|
33327
33535
|
);
|
|
33328
33536
|
server.tool(
|
|
33329
33537
|
"email_remove_account",
|
|
33330
|
-
"Remove an email account",
|
|
33538
|
+
"Remove an email account: deletes its stored credentials, revokes the Google grant for Gmail accounts, and removes Outlook tokens from the local token cache. The result reports whether provider-side revocation succeeded.",
|
|
33331
33539
|
{
|
|
33332
33540
|
accountId: external_exports3.string().describe("ID of the account to remove")
|
|
33333
33541
|
},
|
|
33334
33542
|
async (args) => {
|
|
33335
33543
|
try {
|
|
33336
|
-
await accountManager.removeAccount(args.accountId);
|
|
33337
|
-
return jsonResult(
|
|
33544
|
+
const result = await accountManager.removeAccount(args.accountId);
|
|
33545
|
+
return jsonResult(result);
|
|
33338
33546
|
} catch (error48) {
|
|
33339
33547
|
return jsonResult({ error: error48.message });
|
|
33340
33548
|
}
|
|
@@ -33518,8 +33726,9 @@ function registerReadingTools(server, accountManager) {
|
|
|
33518
33726
|
const absPath = resolveDownloadPath(args.outputPath);
|
|
33519
33727
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33520
33728
|
const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
|
|
33521
|
-
fs3.mkdirSync(path3.dirname(absPath), { recursive: true });
|
|
33522
|
-
fs3.writeFileSync(absPath, Buffer.from(data));
|
|
33729
|
+
fs3.mkdirSync(path3.dirname(absPath), { recursive: true, mode: 448 });
|
|
33730
|
+
fs3.writeFileSync(absPath, Buffer.from(data), { mode: 384 });
|
|
33731
|
+
fs3.chmodSync(absPath, 384);
|
|
33523
33732
|
return jsonResult2({
|
|
33524
33733
|
path: absPath,
|
|
33525
33734
|
bytes: data.length,
|
|
@@ -34242,11 +34451,12 @@ function registerModerationTools(server, accountManager) {
|
|
|
34242
34451
|
}
|
|
34243
34452
|
|
|
34244
34453
|
// src/server.ts
|
|
34454
|
+
var SERVER_VERSION = "1.8.0";
|
|
34245
34455
|
async function createServer(accountManager) {
|
|
34246
34456
|
const mgr = accountManager ?? new AccountManager();
|
|
34247
34457
|
const server = new McpServer({
|
|
34248
34458
|
name: "email-mcp",
|
|
34249
|
-
version:
|
|
34459
|
+
version: SERVER_VERSION
|
|
34250
34460
|
});
|
|
34251
34461
|
registerAccountTools(server, mgr);
|
|
34252
34462
|
registerReadingTools(server, mgr);
|