@marlinjai/email-mcp 1.7.2 → 1.8.1
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 +365 -123
- package/dist/index.js.map +3 -3
- package/dist/setup/wizard.js +345 -114
- package/dist/setup/wizard.js.map +4 -4
- package/package.json +5 -1
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) {
|
|
@@ -7144,6 +7151,58 @@ var init_mapper = __esm({
|
|
|
7144
7151
|
}
|
|
7145
7152
|
});
|
|
7146
7153
|
|
|
7154
|
+
// src/providers/mime.ts
|
|
7155
|
+
import MailComposer from "nodemailer/lib/mail-composer/index.js";
|
|
7156
|
+
import { convert } from "html-to-text";
|
|
7157
|
+
function htmlToText(html) {
|
|
7158
|
+
return convert(html, {
|
|
7159
|
+
wordwrap: false,
|
|
7160
|
+
selectors: [
|
|
7161
|
+
{ selector: "img", format: "skip" },
|
|
7162
|
+
{ selector: "a", options: { hideLinkHrefIfSameAsText: true } }
|
|
7163
|
+
]
|
|
7164
|
+
}).trim();
|
|
7165
|
+
}
|
|
7166
|
+
function normalizeBody(body) {
|
|
7167
|
+
const html = body.html ? body.html : void 0;
|
|
7168
|
+
if (body.text) return { text: body.text, html };
|
|
7169
|
+
if (html) return { text: htmlToText(html), html };
|
|
7170
|
+
return { text: "" };
|
|
7171
|
+
}
|
|
7172
|
+
function toAddress(contact) {
|
|
7173
|
+
return contact.name ? { name: contact.name, address: contact.email } : contact.email;
|
|
7174
|
+
}
|
|
7175
|
+
function toAddressList(contacts) {
|
|
7176
|
+
return contacts?.length ? contacts.map(toAddress) : void 0;
|
|
7177
|
+
}
|
|
7178
|
+
async function buildMimeMessage(from, params) {
|
|
7179
|
+
const { text, html } = normalizeBody(params.body);
|
|
7180
|
+
const mail = {
|
|
7181
|
+
from,
|
|
7182
|
+
to: toAddressList(params.to),
|
|
7183
|
+
cc: toAddressList(params.cc),
|
|
7184
|
+
bcc: toAddressList(params.bcc),
|
|
7185
|
+
subject: params.subject,
|
|
7186
|
+
text,
|
|
7187
|
+
html,
|
|
7188
|
+
inReplyTo: params.inReplyTo,
|
|
7189
|
+
references: params.references?.length ? params.references : void 0,
|
|
7190
|
+
attachments: params.attachments?.map((a) => ({
|
|
7191
|
+
filename: a.filename,
|
|
7192
|
+
content: a.content,
|
|
7193
|
+
contentType: a.contentType
|
|
7194
|
+
}))
|
|
7195
|
+
};
|
|
7196
|
+
const node = new MailComposer(mail).compile();
|
|
7197
|
+
node.keepBcc = true;
|
|
7198
|
+
return node.build();
|
|
7199
|
+
}
|
|
7200
|
+
var init_mime = __esm({
|
|
7201
|
+
"src/providers/mime.ts"() {
|
|
7202
|
+
"use strict";
|
|
7203
|
+
}
|
|
7204
|
+
});
|
|
7205
|
+
|
|
7147
7206
|
// src/providers/gmail/adapter.ts
|
|
7148
7207
|
import { Readable } from "node:stream";
|
|
7149
7208
|
import { google } from "googleapis";
|
|
@@ -7154,6 +7213,7 @@ var init_adapter = __esm({
|
|
|
7154
7213
|
"use strict";
|
|
7155
7214
|
init_types();
|
|
7156
7215
|
init_mapper();
|
|
7216
|
+
init_mime();
|
|
7157
7217
|
GmailAdapter = class {
|
|
7158
7218
|
providerType = ProviderType.Gmail;
|
|
7159
7219
|
gmail = null;
|
|
@@ -7295,7 +7355,7 @@ var init_adapter = __esm({
|
|
|
7295
7355
|
}
|
|
7296
7356
|
async sendEmail(params) {
|
|
7297
7357
|
const gmail = this.ensureConnected();
|
|
7298
|
-
const raw = this.
|
|
7358
|
+
const raw = await this.buildRaw(params);
|
|
7299
7359
|
const res = await gmail.users.messages.send({
|
|
7300
7360
|
userId: "me",
|
|
7301
7361
|
requestBody: { raw }
|
|
@@ -7307,7 +7367,7 @@ var init_adapter = __esm({
|
|
|
7307
7367
|
}
|
|
7308
7368
|
async createDraft(params) {
|
|
7309
7369
|
const gmail = this.ensureConnected();
|
|
7310
|
-
const raw = this.
|
|
7370
|
+
const raw = await this.buildRaw(params);
|
|
7311
7371
|
const res = await gmail.users.drafts.create({
|
|
7312
7372
|
userId: "me",
|
|
7313
7373
|
requestBody: {
|
|
@@ -7318,7 +7378,7 @@ var init_adapter = __esm({
|
|
|
7318
7378
|
}
|
|
7319
7379
|
async updateDraft(draftId, params) {
|
|
7320
7380
|
const gmail = this.ensureConnected();
|
|
7321
|
-
const raw = this.
|
|
7381
|
+
const raw = await this.buildRaw(params);
|
|
7322
7382
|
const res = await gmail.users.drafts.update({
|
|
7323
7383
|
userId: "me",
|
|
7324
7384
|
id: draftId,
|
|
@@ -7647,30 +7707,15 @@ var init_adapter = __esm({
|
|
|
7647
7707
|
const gmail = this.ensureConnected();
|
|
7648
7708
|
await gmail.users.settings.filters.delete({ userId: "me", id: ruleId });
|
|
7649
7709
|
}
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
if (params.bcc?.length) {
|
|
7660
|
-
lines.push(`Bcc: ${params.bcc.map((c) => c.email).join(", ")}`);
|
|
7661
|
-
}
|
|
7662
|
-
lines.push(`Subject: ${params.subject}`);
|
|
7663
|
-
if (params.inReplyTo) {
|
|
7664
|
-
lines.push(`In-Reply-To: ${params.inReplyTo}`);
|
|
7665
|
-
}
|
|
7666
|
-
if (params.references?.length) {
|
|
7667
|
-
lines.push(`References: ${params.references.join(" ")}`);
|
|
7668
|
-
}
|
|
7669
|
-
lines.push("MIME-Version: 1.0");
|
|
7670
|
-
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
7671
|
-
lines.push("");
|
|
7672
|
-
lines.push(params.body.text || params.body.html || "");
|
|
7673
|
-
return Buffer.from(lines.join("\r\n")).toString("base64url");
|
|
7710
|
+
/**
|
|
7711
|
+
* Builds the base64url `raw` payload Gmail expects for send, draft create and
|
|
7712
|
+
* draft update: a full MIME message with a text part, an HTML part when one
|
|
7713
|
+
* was given (or a text part derived from HTML-only input), encoded headers
|
|
7714
|
+
* and any attachments. See `buildMimeMessage`.
|
|
7715
|
+
*/
|
|
7716
|
+
async buildRaw(params) {
|
|
7717
|
+
const message = await buildMimeMessage(this.email, params);
|
|
7718
|
+
return message.toString("base64url");
|
|
7674
7719
|
}
|
|
7675
7720
|
};
|
|
7676
7721
|
}
|
|
@@ -7683,7 +7728,41 @@ function resolveScopeMode(explicit) {
|
|
|
7683
7728
|
if (explicit) return explicit;
|
|
7684
7729
|
return process.env.EMAIL_MCP_GMAIL_SCOPE?.toLowerCase() === "restricted" ? "restricted" : "full";
|
|
7685
7730
|
}
|
|
7686
|
-
|
|
7731
|
+
async function revokeGoogleGrant(token) {
|
|
7732
|
+
if (!token) {
|
|
7733
|
+
return {
|
|
7734
|
+
ok: false,
|
|
7735
|
+
detail: `No Google token was stored for this account, so nothing could be revoked. Remove email-mcp manually at ${GOOGLE_PERMISSIONS_URL}.`
|
|
7736
|
+
};
|
|
7737
|
+
}
|
|
7738
|
+
try {
|
|
7739
|
+
const res = await fetch(GOOGLE_REVOKE_ENDPOINT, {
|
|
7740
|
+
method: "POST",
|
|
7741
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
7742
|
+
body: new URLSearchParams({ token }).toString(),
|
|
7743
|
+
signal: AbortSignal.timeout(REVOKE_TIMEOUT_MS)
|
|
7744
|
+
});
|
|
7745
|
+
if (res.ok) {
|
|
7746
|
+
return { ok: true, detail: "Google revoked the grant: email-mcp no longer has access to this Gmail account." };
|
|
7747
|
+
}
|
|
7748
|
+
let reason = "";
|
|
7749
|
+
try {
|
|
7750
|
+
const body = await res.json();
|
|
7751
|
+
reason = [body.error, body.error_description].filter(Boolean).join(": ");
|
|
7752
|
+
} catch {
|
|
7753
|
+
}
|
|
7754
|
+
return {
|
|
7755
|
+
ok: false,
|
|
7756
|
+
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}.`
|
|
7757
|
+
};
|
|
7758
|
+
} catch (err) {
|
|
7759
|
+
return {
|
|
7760
|
+
ok: false,
|
|
7761
|
+
detail: `Could not reach Google's revocation endpoint (${err?.message ?? String(err)}). Remove email-mcp manually at ${GOOGLE_PERMISSIONS_URL}.`
|
|
7762
|
+
};
|
|
7763
|
+
}
|
|
7764
|
+
}
|
|
7765
|
+
var GMAIL_SCOPES, GOOGLE_REVOKE_ENDPOINT, GOOGLE_PERMISSIONS_URL, REVOKE_TIMEOUT_MS, GmailAuth;
|
|
7687
7766
|
var init_auth = __esm({
|
|
7688
7767
|
"src/providers/gmail/auth.ts"() {
|
|
7689
7768
|
"use strict";
|
|
@@ -7710,6 +7789,9 @@ var init_auth = __esm({
|
|
|
7710
7789
|
"https://www.googleapis.com/auth/gmail.settings.basic"
|
|
7711
7790
|
]
|
|
7712
7791
|
};
|
|
7792
|
+
GOOGLE_REVOKE_ENDPOINT = "https://oauth2.googleapis.com/revoke";
|
|
7793
|
+
GOOGLE_PERMISSIONS_URL = "https://myaccount.google.com/permissions";
|
|
7794
|
+
REVOKE_TIMEOUT_MS = 1e4;
|
|
7713
7795
|
GmailAuth = class {
|
|
7714
7796
|
clientId;
|
|
7715
7797
|
clientSecret;
|
|
@@ -8391,31 +8473,82 @@ import {
|
|
|
8391
8473
|
PublicClientApplication,
|
|
8392
8474
|
CryptoProvider
|
|
8393
8475
|
} from "@azure/msal-node";
|
|
8394
|
-
function
|
|
8476
|
+
function writeEncryptedCache(cacheFilePath, serialized) {
|
|
8477
|
+
const dir = path2.dirname(cacheFilePath);
|
|
8478
|
+
if (!fs2.existsSync(dir)) {
|
|
8479
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
8480
|
+
}
|
|
8481
|
+
const tmpPath = `${cacheFilePath}.${process.pid}.tmp`;
|
|
8482
|
+
fs2.writeFileSync(tmpPath, encryptWithMachineKey(serialized), { mode: 384 });
|
|
8483
|
+
fs2.chmodSync(tmpPath, 384);
|
|
8484
|
+
fs2.renameSync(tmpPath, cacheFilePath);
|
|
8485
|
+
}
|
|
8486
|
+
function loadSerializedCache(cacheFilePath, legacyPlaintextPath) {
|
|
8487
|
+
const hasEncrypted = fs2.existsSync(cacheFilePath);
|
|
8488
|
+
if (fs2.existsSync(legacyPlaintextPath)) {
|
|
8489
|
+
const plaintextIsNewer = !hasEncrypted || fs2.statSync(legacyPlaintextPath).mtimeMs > fs2.statSync(cacheFilePath).mtimeMs;
|
|
8490
|
+
if (plaintextIsNewer) {
|
|
8491
|
+
const serialized = fs2.readFileSync(legacyPlaintextPath, "utf-8");
|
|
8492
|
+
writeEncryptedCache(cacheFilePath, serialized);
|
|
8493
|
+
fs2.unlinkSync(legacyPlaintextPath);
|
|
8494
|
+
return serialized;
|
|
8495
|
+
}
|
|
8496
|
+
fs2.unlinkSync(legacyPlaintextPath);
|
|
8497
|
+
}
|
|
8498
|
+
if (!hasEncrypted) return null;
|
|
8499
|
+
const raw = fs2.readFileSync(cacheFilePath, "utf-8");
|
|
8500
|
+
let plaintext;
|
|
8501
|
+
let usedLegacyKey;
|
|
8502
|
+
try {
|
|
8503
|
+
({ plaintext, usedLegacyKey } = decryptWithMachineKey(raw));
|
|
8504
|
+
} catch {
|
|
8505
|
+
throw new MsalCacheDecryptError(cacheFilePath);
|
|
8506
|
+
}
|
|
8507
|
+
if (usedLegacyKey) {
|
|
8508
|
+
try {
|
|
8509
|
+
writeEncryptedCache(cacheFilePath, plaintext);
|
|
8510
|
+
} catch {
|
|
8511
|
+
}
|
|
8512
|
+
}
|
|
8513
|
+
return plaintext;
|
|
8514
|
+
}
|
|
8515
|
+
function createEncryptedFileCachePlugin(cacheFilePath, legacyPlaintextPath = path2.join(path2.dirname(cacheFilePath), LEGACY_PLAINTEXT_CACHE_NAME)) {
|
|
8516
|
+
let loadFailed = false;
|
|
8395
8517
|
return {
|
|
8396
8518
|
async beforeCacheAccess(context) {
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8519
|
+
loadFailed = false;
|
|
8520
|
+
try {
|
|
8521
|
+
const serialized = loadSerializedCache(cacheFilePath, legacyPlaintextPath);
|
|
8522
|
+
if (serialized !== null) {
|
|
8523
|
+
context.tokenCache.deserialize(serialized);
|
|
8524
|
+
}
|
|
8525
|
+
} catch (err) {
|
|
8526
|
+
loadFailed = true;
|
|
8527
|
+
throw err;
|
|
8400
8528
|
}
|
|
8401
8529
|
},
|
|
8402
8530
|
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
|
-
}
|
|
8531
|
+
if (loadFailed || !context.cacheHasChanged) return;
|
|
8532
|
+
writeEncryptedCache(cacheFilePath, context.tokenCache.serialize());
|
|
8410
8533
|
}
|
|
8411
8534
|
};
|
|
8412
8535
|
}
|
|
8413
|
-
var DEFAULT_CACHE_DIR, DEFAULT_CACHE_FILE, OutlookAuth;
|
|
8536
|
+
var DEFAULT_CACHE_DIR, DEFAULT_CACHE_FILE, LEGACY_PLAINTEXT_CACHE_NAME, MsalCacheDecryptError, OutlookAuth;
|
|
8414
8537
|
var init_auth2 = __esm({
|
|
8415
8538
|
"src/providers/outlook/auth.ts"() {
|
|
8416
8539
|
"use strict";
|
|
8540
|
+
init_credential_store();
|
|
8417
8541
|
DEFAULT_CACHE_DIR = path2.join(os2.homedir(), ".email-mcp");
|
|
8418
|
-
DEFAULT_CACHE_FILE = path2.join(DEFAULT_CACHE_DIR, "msal-cache.
|
|
8542
|
+
DEFAULT_CACHE_FILE = path2.join(DEFAULT_CACHE_DIR, "msal-cache.enc");
|
|
8543
|
+
LEGACY_PLAINTEXT_CACHE_NAME = "msal-cache.json";
|
|
8544
|
+
MsalCacheDecryptError = class extends Error {
|
|
8545
|
+
constructor(cacheFilePath) {
|
|
8546
|
+
super(
|
|
8547
|
+
`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.`
|
|
8548
|
+
);
|
|
8549
|
+
this.name = "MsalCacheDecryptError";
|
|
8550
|
+
}
|
|
8551
|
+
};
|
|
8419
8552
|
OutlookAuth = class _OutlookAuth {
|
|
8420
8553
|
static AUTHORITY = "https://login.microsoftonline.com/consumers";
|
|
8421
8554
|
// MailboxSettings.ReadWrite is required for the messageRules API (block
|
|
@@ -8431,7 +8564,7 @@ var init_auth2 = __esm({
|
|
|
8431
8564
|
authority: _OutlookAuth.AUTHORITY
|
|
8432
8565
|
},
|
|
8433
8566
|
cache: {
|
|
8434
|
-
cachePlugin:
|
|
8567
|
+
cachePlugin: createEncryptedFileCachePlugin(cacheFilePath ?? DEFAULT_CACHE_FILE)
|
|
8435
8568
|
}
|
|
8436
8569
|
});
|
|
8437
8570
|
this.cryptoProvider = new CryptoProvider();
|
|
@@ -8485,6 +8618,27 @@ var init_auth2 = __esm({
|
|
|
8485
8618
|
homeAccountId: result.account?.homeAccountId ?? homeAccountId
|
|
8486
8619
|
};
|
|
8487
8620
|
}
|
|
8621
|
+
/**
|
|
8622
|
+
* Removes one account (its refresh, access and ID tokens) from the MSAL
|
|
8623
|
+
* cache. Matches by home account id, or by username for credentials saved
|
|
8624
|
+
* before the home account id was recorded. Returns whether an entry was
|
|
8625
|
+
* found and removed.
|
|
8626
|
+
*/
|
|
8627
|
+
async removeCachedAccount(match) {
|
|
8628
|
+
const cache = this.pca.getTokenCache();
|
|
8629
|
+
let account = null;
|
|
8630
|
+
if (match.homeAccountId) {
|
|
8631
|
+
account = await cache.getAccountByHomeId(match.homeAccountId);
|
|
8632
|
+
}
|
|
8633
|
+
if (!account && match.username) {
|
|
8634
|
+
const wanted = match.username.trim().toLowerCase();
|
|
8635
|
+
const all = await cache.getAllAccounts();
|
|
8636
|
+
account = all.find((a) => (a.username ?? "").trim().toLowerCase() === wanted) ?? null;
|
|
8637
|
+
}
|
|
8638
|
+
if (!account) return false;
|
|
8639
|
+
await cache.removeAccount(account);
|
|
8640
|
+
return true;
|
|
8641
|
+
}
|
|
8488
8642
|
/**
|
|
8489
8643
|
* @deprecated Use refreshTokenSilent() instead.
|
|
8490
8644
|
* Kept for backward compatibility with credentials that have a stored refresh token.
|
|
@@ -8586,14 +8740,15 @@ function createSmtpTransport(email3, creds) {
|
|
|
8586
8740
|
});
|
|
8587
8741
|
}
|
|
8588
8742
|
async function sendViaSmtp(transport, from, params) {
|
|
8743
|
+
const { text, html } = normalizeBody(params.body);
|
|
8589
8744
|
const result = await transport.sendMail({
|
|
8590
8745
|
from,
|
|
8591
|
-
to: params.to.map(
|
|
8592
|
-
cc: params.cc?.
|
|
8593
|
-
bcc: params.bcc?.
|
|
8746
|
+
to: params.to.map(toAddress),
|
|
8747
|
+
cc: params.cc?.length ? params.cc.map(toAddress) : void 0,
|
|
8748
|
+
bcc: params.bcc?.length ? params.bcc.map(toAddress) : void 0,
|
|
8594
8749
|
subject: params.subject,
|
|
8595
|
-
text
|
|
8596
|
-
html
|
|
8750
|
+
text,
|
|
8751
|
+
html,
|
|
8597
8752
|
inReplyTo: params.inReplyTo,
|
|
8598
8753
|
references: params.references?.join(" "),
|
|
8599
8754
|
attachments: params.attachments?.map((a) => ({
|
|
@@ -8607,6 +8762,7 @@ async function sendViaSmtp(transport, from, params) {
|
|
|
8607
8762
|
var init_smtp = __esm({
|
|
8608
8763
|
"src/providers/imap/smtp.ts"() {
|
|
8609
8764
|
"use strict";
|
|
8765
|
+
init_mime();
|
|
8610
8766
|
}
|
|
8611
8767
|
});
|
|
8612
8768
|
|
|
@@ -8639,6 +8795,7 @@ var init_adapter3 = __esm({
|
|
|
8639
8795
|
init_types();
|
|
8640
8796
|
init_mapper3();
|
|
8641
8797
|
init_smtp();
|
|
8798
|
+
init_mime();
|
|
8642
8799
|
ImapAdapter = class {
|
|
8643
8800
|
providerType = ProviderType.IMAP;
|
|
8644
8801
|
client = null;
|
|
@@ -8754,7 +8911,7 @@ var init_adapter3 = __esm({
|
|
|
8754
8911
|
const result = await client.mailboxCreate(fullPath);
|
|
8755
8912
|
return {
|
|
8756
8913
|
id: result.path,
|
|
8757
|
-
name
|
|
8914
|
+
name,
|
|
8758
8915
|
path: result.path,
|
|
8759
8916
|
type: FolderType.Other
|
|
8760
8917
|
};
|
|
@@ -8992,6 +9149,7 @@ var init_adapter3 = __esm({
|
|
|
8992
9149
|
const uid = parseInt(id, 10);
|
|
8993
9150
|
const msg = await client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
|
|
8994
9151
|
if (!msg) throw new Error(`Email ${id} not found`);
|
|
9152
|
+
if (!msg.source) throw new Error(`Email ${id} has no source`);
|
|
8995
9153
|
const parsed = await simpleParser(msg.source);
|
|
8996
9154
|
parsed.flags = msg.flags;
|
|
8997
9155
|
return mapParsedEmail(parsed, targetFolder, this.accountId, msg.uid);
|
|
@@ -9016,6 +9174,7 @@ var init_adapter3 = __esm({
|
|
|
9016
9174
|
if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
|
|
9017
9175
|
const messages = [];
|
|
9018
9176
|
for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
|
|
9177
|
+
if (!msg.source) continue;
|
|
9019
9178
|
const parsed = await simpleParser(msg.source);
|
|
9020
9179
|
parsed.flags = msg.flags;
|
|
9021
9180
|
messages.push(mapParsedEmail(parsed, "INBOX", this.accountId, msg.uid));
|
|
@@ -9051,6 +9210,7 @@ var init_adapter3 = __esm({
|
|
|
9051
9210
|
try {
|
|
9052
9211
|
const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
|
|
9053
9212
|
if (!msg) throw new Error(`Email ${emailId} not found`);
|
|
9213
|
+
if (!msg.source) throw new Error(`Email ${emailId} has no source`);
|
|
9054
9214
|
const parsed = await simpleParser(msg.source);
|
|
9055
9215
|
const attachment = (parsed.attachments || []).find(
|
|
9056
9216
|
(att) => att.contentId === attachmentId || att.filename === attachmentId
|
|
@@ -9075,26 +9235,17 @@ var init_adapter3 = __esm({
|
|
|
9075
9235
|
const messageId = await sendViaSmtp(transport, this.email, params);
|
|
9076
9236
|
return { id: messageId };
|
|
9077
9237
|
}
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
lines.push(`Subject: ${params.subject}`);
|
|
9086
|
-
lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
9087
|
-
if (params.inReplyTo) lines.push(`In-Reply-To: ${params.inReplyTo}`);
|
|
9088
|
-
if (params.references?.length) lines.push(`References: ${params.references.join(" ")}`);
|
|
9089
|
-
lines.push("MIME-Version: 1.0");
|
|
9090
|
-
lines.push("Content-Type: text/plain; charset=utf-8");
|
|
9091
|
-
lines.push("");
|
|
9092
|
-
lines.push(params.body.text || "");
|
|
9093
|
-
return lines.join("\r\n");
|
|
9238
|
+
/**
|
|
9239
|
+
* Builds the full MIME message a draft is stored as: text and HTML parts,
|
|
9240
|
+
* Bcc kept so the draft remembers its recipients, encoded headers and any
|
|
9241
|
+
* attachments. See `buildMimeMessage`.
|
|
9242
|
+
*/
|
|
9243
|
+
buildDraftMessage(params) {
|
|
9244
|
+
return buildMimeMessage(this.email, params);
|
|
9094
9245
|
}
|
|
9095
9246
|
async createDraft(params) {
|
|
9096
9247
|
const client = await this.ensureConnected();
|
|
9097
|
-
const rawMessage = this.
|
|
9248
|
+
const rawMessage = await this.buildDraftMessage(params);
|
|
9098
9249
|
const draftsFolder = await this.resolveFolder("Drafts");
|
|
9099
9250
|
const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
|
|
9100
9251
|
if (!result) throw new Error("Failed to append draft");
|
|
@@ -9109,6 +9260,7 @@ var init_adapter3 = __esm({
|
|
|
9109
9260
|
async updateDraft(draftId, params, sourceFolder) {
|
|
9110
9261
|
const client = await this.ensureConnected();
|
|
9111
9262
|
const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
|
|
9263
|
+
const rawMessage = await this.buildDraftMessage(params);
|
|
9112
9264
|
let lock;
|
|
9113
9265
|
try {
|
|
9114
9266
|
lock = await client.getMailboxLock(draftsFolder);
|
|
@@ -9120,7 +9272,6 @@ var init_adapter3 = __esm({
|
|
|
9120
9272
|
} finally {
|
|
9121
9273
|
lock.release();
|
|
9122
9274
|
}
|
|
9123
|
-
const rawMessage = this.buildDraftRfc2822(params);
|
|
9124
9275
|
const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
|
|
9125
9276
|
if (!result) throw new Error("Failed to append updated draft");
|
|
9126
9277
|
return { id: String(result.uid || result) };
|
|
@@ -9358,16 +9509,16 @@ var init_adapter4 = __esm({
|
|
|
9358
9509
|
ICloudAdapter = class extends ImapAdapter {
|
|
9359
9510
|
providerType = ProviderType.ICloud;
|
|
9360
9511
|
async connect(credentials) {
|
|
9512
|
+
const passwordDefaults = {
|
|
9513
|
+
host: "imap.mail.me.com",
|
|
9514
|
+
port: 993,
|
|
9515
|
+
tls: true,
|
|
9516
|
+
smtpHost: "smtp.mail.me.com",
|
|
9517
|
+
smtpPort: 587
|
|
9518
|
+
};
|
|
9361
9519
|
const withDefaults = {
|
|
9362
9520
|
...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
|
-
}
|
|
9521
|
+
password: { ...passwordDefaults, ...credentials.password }
|
|
9371
9522
|
};
|
|
9372
9523
|
return super.connect(withDefaults);
|
|
9373
9524
|
}
|
|
@@ -9401,7 +9552,7 @@ function createProvider(provider) {
|
|
|
9401
9552
|
throw new Error(`Unknown provider: ${provider}`);
|
|
9402
9553
|
}
|
|
9403
9554
|
}
|
|
9404
|
-
var AccountManager;
|
|
9555
|
+
var MICROSOFT_CONSENT_URL, AccountManager;
|
|
9405
9556
|
var init_account_manager = __esm({
|
|
9406
9557
|
"src/account-manager.ts"() {
|
|
9407
9558
|
"use strict";
|
|
@@ -9414,6 +9565,7 @@ var init_account_manager = __esm({
|
|
|
9414
9565
|
init_adapter3();
|
|
9415
9566
|
init_types();
|
|
9416
9567
|
init_oauth_config();
|
|
9568
|
+
MICROSOFT_CONSENT_URL = "https://account.live.com/consent/Manage";
|
|
9417
9569
|
AccountManager = class {
|
|
9418
9570
|
store;
|
|
9419
9571
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -9468,9 +9620,55 @@ var init_account_manager = __esm({
|
|
|
9468
9620
|
await this.store.save(creds);
|
|
9469
9621
|
await this.connectAccount(creds.id);
|
|
9470
9622
|
}
|
|
9623
|
+
/**
|
|
9624
|
+
* Removes an account locally, then cleans up at the provider as far as the
|
|
9625
|
+
* provider allows. Local removal always happens first and never depends on
|
|
9626
|
+
* the network; a failed provider step is reported in the result, not thrown.
|
|
9627
|
+
*/
|
|
9471
9628
|
async removeAccount(accountId) {
|
|
9629
|
+
const creds = await this.store.get(accountId);
|
|
9630
|
+
if (!creds) throw new Error(`Account ${accountId} not found`);
|
|
9472
9631
|
await this.disconnectAccount(accountId);
|
|
9473
9632
|
await this.store.remove(accountId);
|
|
9633
|
+
const result = {
|
|
9634
|
+
success: true,
|
|
9635
|
+
accountId,
|
|
9636
|
+
provider: creds.provider,
|
|
9637
|
+
email: creds.email,
|
|
9638
|
+
revocation: { status: "not_applicable", detail: "" }
|
|
9639
|
+
};
|
|
9640
|
+
if (creds.provider === ProviderType.Gmail) {
|
|
9641
|
+
const token = creds.oauth?.refresh_token || creds.oauth?.access_token || "";
|
|
9642
|
+
const { ok, detail } = await revokeGoogleGrant(token);
|
|
9643
|
+
result.revocation = { status: ok ? "revoked" : "failed", detail };
|
|
9644
|
+
} else if (creds.provider === ProviderType.Outlook) {
|
|
9645
|
+
result.tokenCache = await this.removeFromMsalCache(creds);
|
|
9646
|
+
result.revocation = {
|
|
9647
|
+
status: "local_only",
|
|
9648
|
+
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}.`
|
|
9649
|
+
};
|
|
9650
|
+
} else {
|
|
9651
|
+
result.revocation = {
|
|
9652
|
+
status: "not_applicable",
|
|
9653
|
+
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."
|
|
9654
|
+
};
|
|
9655
|
+
}
|
|
9656
|
+
return result;
|
|
9657
|
+
}
|
|
9658
|
+
async removeFromMsalCache(creds) {
|
|
9659
|
+
try {
|
|
9660
|
+
const outlookAuth = new OutlookAuth(OUTLOOK_CLIENT_ID);
|
|
9661
|
+
const removed = await outlookAuth.removeCachedAccount({
|
|
9662
|
+
homeAccountId: creds.oauth?.msal_home_account_id,
|
|
9663
|
+
username: creds.email
|
|
9664
|
+
});
|
|
9665
|
+
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." };
|
|
9666
|
+
} catch (err) {
|
|
9667
|
+
return {
|
|
9668
|
+
status: "failed",
|
|
9669
|
+
detail: `The account's tokens could not be removed from the local Outlook token cache: ${err?.message ?? String(err)}`
|
|
9670
|
+
};
|
|
9671
|
+
}
|
|
9474
9672
|
}
|
|
9475
9673
|
async disconnectAccount(accountId) {
|
|
9476
9674
|
const provider = this.providers.get(accountId);
|
|
@@ -9671,53 +9869,95 @@ var init_oauth_server = __esm({
|
|
|
9671
9869
|
<p>An error occurred during authorization. Please try again.</p>
|
|
9672
9870
|
</body>
|
|
9673
9871
|
</html>`;
|
|
9674
|
-
OAuthCallbackServer = class {
|
|
9675
|
-
|
|
9872
|
+
OAuthCallbackServer = class _OAuthCallbackServer {
|
|
9873
|
+
servers = [];
|
|
9676
9874
|
resolveCode = null;
|
|
9677
9875
|
rejectCode = null;
|
|
9678
9876
|
timeoutMs;
|
|
9679
9877
|
timeoutHandle = null;
|
|
9680
9878
|
expectedState = null;
|
|
9879
|
+
static IPV4_LOOPBACK = "127.0.0.1";
|
|
9880
|
+
static IPV6_LOOPBACK = "::1";
|
|
9881
|
+
/** Attempts at finding a port that is free on both loopback addresses. */
|
|
9882
|
+
static MAX_PORT_ATTEMPTS = 5;
|
|
9681
9883
|
constructor(timeoutMs = 12e4) {
|
|
9682
9884
|
this.timeoutMs = timeoutMs;
|
|
9683
9885
|
}
|
|
9684
|
-
|
|
9886
|
+
handleRequest = (req, res) => {
|
|
9887
|
+
const url2 = new URL(req.url || "/", `http://localhost`);
|
|
9888
|
+
if (url2.pathname === "/callback" || url2.pathname === "/") {
|
|
9889
|
+
const code = url2.searchParams.get("code");
|
|
9890
|
+
const error48 = url2.searchParams.get("error");
|
|
9891
|
+
const state = url2.searchParams.get("state");
|
|
9892
|
+
if (code && this.expectedState !== null && state !== this.expectedState) {
|
|
9893
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
9894
|
+
res.end(ERROR_HTML);
|
|
9895
|
+
this.rejectCode?.(new Error("OAuth error: state mismatch, callback did not come from the authorization request we started"));
|
|
9896
|
+
} else if (code) {
|
|
9897
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
9898
|
+
res.end(SUCCESS_HTML);
|
|
9899
|
+
this.resolveCode?.(code);
|
|
9900
|
+
} else {
|
|
9901
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
9902
|
+
res.end(ERROR_HTML);
|
|
9903
|
+
this.rejectCode?.(new Error(`OAuth error: ${error48 || "no code received"}`));
|
|
9904
|
+
}
|
|
9905
|
+
} else {
|
|
9906
|
+
res.writeHead(404);
|
|
9907
|
+
res.end("Not Found");
|
|
9908
|
+
}
|
|
9909
|
+
};
|
|
9910
|
+
listenOn(host, port) {
|
|
9685
9911
|
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
|
-
}
|
|
9912
|
+
const server = http.createServer(this.handleRequest);
|
|
9913
|
+
const onError = (err) => {
|
|
9914
|
+
server.close();
|
|
9915
|
+
reject(err);
|
|
9916
|
+
};
|
|
9917
|
+
server.once("error", onError);
|
|
9918
|
+
server.listen(port, host, () => {
|
|
9919
|
+
server.off("error", onError);
|
|
9920
|
+
server.on("error", (err) => this.rejectCode?.(err));
|
|
9921
|
+
resolve(server);
|
|
9717
9922
|
});
|
|
9718
|
-
this.server.on("error", reject);
|
|
9719
9923
|
});
|
|
9720
9924
|
}
|
|
9925
|
+
/**
|
|
9926
|
+
* Starts listening on an OS-assigned port on 127.0.0.1 and, when available,
|
|
9927
|
+
* the same port on ::1. Resolves with the port.
|
|
9928
|
+
*/
|
|
9929
|
+
async start() {
|
|
9930
|
+
let lastError;
|
|
9931
|
+
for (let attempt = 0; attempt < _OAuthCallbackServer.MAX_PORT_ATTEMPTS; attempt++) {
|
|
9932
|
+
const v4 = await this.listenOn(_OAuthCallbackServer.IPV4_LOOPBACK, 0);
|
|
9933
|
+
const addr = v4.address();
|
|
9934
|
+
if (!addr || typeof addr !== "object") {
|
|
9935
|
+
v4.close();
|
|
9936
|
+
throw new Error("Failed to get server port");
|
|
9937
|
+
}
|
|
9938
|
+
const port = addr.port;
|
|
9939
|
+
try {
|
|
9940
|
+
const v6 = await this.listenOn(_OAuthCallbackServer.IPV6_LOOPBACK, port);
|
|
9941
|
+
this.servers = [v4, v6];
|
|
9942
|
+
return port;
|
|
9943
|
+
} catch (err) {
|
|
9944
|
+
if (err?.code === "EADDRINUSE") {
|
|
9945
|
+
v4.close();
|
|
9946
|
+
lastError = err;
|
|
9947
|
+
continue;
|
|
9948
|
+
}
|
|
9949
|
+
this.servers = [v4];
|
|
9950
|
+
return port;
|
|
9951
|
+
}
|
|
9952
|
+
}
|
|
9953
|
+
throw new Error(
|
|
9954
|
+
`Could not find a port free on both loopback addresses: ${lastError?.message ?? "unknown error"}`
|
|
9955
|
+
);
|
|
9956
|
+
}
|
|
9957
|
+
/** The addresses the callback server is listening on (for diagnostics and tests). */
|
|
9958
|
+
boundAddresses() {
|
|
9959
|
+
return this.servers.map((s) => s.address()).filter((a) => !!a && typeof a === "object").map((a) => a.address);
|
|
9960
|
+
}
|
|
9721
9961
|
/**
|
|
9722
9962
|
* Resolve with the authorization code once the browser is redirected back.
|
|
9723
9963
|
* Pass the `state` that was put into the authorization URL: callbacks whose
|
|
@@ -9745,10 +9985,10 @@ var init_oauth_server = __esm({
|
|
|
9745
9985
|
clearTimeout(this.timeoutHandle);
|
|
9746
9986
|
this.timeoutHandle = null;
|
|
9747
9987
|
}
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
this.server = null;
|
|
9988
|
+
for (const server of this.servers) {
|
|
9989
|
+
server.close();
|
|
9751
9990
|
}
|
|
9991
|
+
this.servers = [];
|
|
9752
9992
|
}
|
|
9753
9993
|
};
|
|
9754
9994
|
}
|
|
@@ -33327,14 +33567,14 @@ function registerAccountTools(server, accountManager) {
|
|
|
33327
33567
|
);
|
|
33328
33568
|
server.tool(
|
|
33329
33569
|
"email_remove_account",
|
|
33330
|
-
"Remove an email account",
|
|
33570
|
+
"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
33571
|
{
|
|
33332
33572
|
accountId: external_exports3.string().describe("ID of the account to remove")
|
|
33333
33573
|
},
|
|
33334
33574
|
async (args) => {
|
|
33335
33575
|
try {
|
|
33336
|
-
await accountManager.removeAccount(args.accountId);
|
|
33337
|
-
return jsonResult(
|
|
33576
|
+
const result = await accountManager.removeAccount(args.accountId);
|
|
33577
|
+
return jsonResult(result);
|
|
33338
33578
|
} catch (error48) {
|
|
33339
33579
|
return jsonResult({ error: error48.message });
|
|
33340
33580
|
}
|
|
@@ -33518,8 +33758,9 @@ function registerReadingTools(server, accountManager) {
|
|
|
33518
33758
|
const absPath = resolveDownloadPath(args.outputPath);
|
|
33519
33759
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33520
33760
|
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));
|
|
33761
|
+
fs3.mkdirSync(path3.dirname(absPath), { recursive: true, mode: 448 });
|
|
33762
|
+
fs3.writeFileSync(absPath, Buffer.from(data), { mode: 384 });
|
|
33763
|
+
fs3.chmodSync(absPath, 384);
|
|
33523
33764
|
return jsonResult2({
|
|
33524
33765
|
path: absPath,
|
|
33525
33766
|
bytes: data.length,
|
|
@@ -34242,11 +34483,12 @@ function registerModerationTools(server, accountManager) {
|
|
|
34242
34483
|
}
|
|
34243
34484
|
|
|
34244
34485
|
// src/server.ts
|
|
34486
|
+
var SERVER_VERSION = "1.8.1";
|
|
34245
34487
|
async function createServer(accountManager) {
|
|
34246
34488
|
const mgr = accountManager ?? new AccountManager();
|
|
34247
34489
|
const server = new McpServer({
|
|
34248
34490
|
name: "email-mcp",
|
|
34249
|
-
version:
|
|
34491
|
+
version: SERVER_VERSION
|
|
34250
34492
|
});
|
|
34251
34493
|
registerAccountTools(server, mgr);
|
|
34252
34494
|
registerReadingTools(server, mgr);
|