@marlinjai/email-mcp 1.9.0 → 1.10.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 +31 -9
- package/dist/index.js +589 -83
- package/dist/index.js.map +4 -4
- package/dist/setup/wizard.js +294 -28
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3396,8 +3396,8 @@ var require_utils = __commonJS({
|
|
|
3396
3396
|
}
|
|
3397
3397
|
return ind;
|
|
3398
3398
|
}
|
|
3399
|
-
function removeDotSegments(
|
|
3400
|
-
let input2 =
|
|
3399
|
+
function removeDotSegments(path5) {
|
|
3400
|
+
let input2 = path5;
|
|
3401
3401
|
const output2 = [];
|
|
3402
3402
|
let nextSlash = -1;
|
|
3403
3403
|
let len = 0;
|
|
@@ -3596,8 +3596,8 @@ var require_schemes = __commonJS({
|
|
|
3596
3596
|
wsComponent.secure = void 0;
|
|
3597
3597
|
}
|
|
3598
3598
|
if (wsComponent.resourceName) {
|
|
3599
|
-
const [
|
|
3600
|
-
wsComponent.path =
|
|
3599
|
+
const [path5, query] = wsComponent.resourceName.split("?");
|
|
3600
|
+
wsComponent.path = path5 && path5 !== "/" ? path5 : void 0;
|
|
3601
3601
|
wsComponent.query = query;
|
|
3602
3602
|
wsComponent.resourceName = void 0;
|
|
3603
3603
|
}
|
|
@@ -7068,6 +7068,14 @@ function extractAttachments(payload) {
|
|
|
7068
7068
|
}
|
|
7069
7069
|
return attachments;
|
|
7070
7070
|
}
|
|
7071
|
+
function threadingHeaders(headers) {
|
|
7072
|
+
const picked = {};
|
|
7073
|
+
for (const name of ["Message-ID", "In-Reply-To", "References"]) {
|
|
7074
|
+
const value = getHeader(headers, name);
|
|
7075
|
+
if (value) picked[name.toLowerCase()] = value;
|
|
7076
|
+
}
|
|
7077
|
+
return Object.keys(picked).length > 0 ? picked : void 0;
|
|
7078
|
+
}
|
|
7071
7079
|
function mapGmailMessage(message, accountId) {
|
|
7072
7080
|
const headers = message.payload?.headers || [];
|
|
7073
7081
|
const labelIds = message.labelIds || [];
|
|
@@ -7102,6 +7110,7 @@ function mapGmailMessage(message, accountId) {
|
|
|
7102
7110
|
snippet: message.snippet,
|
|
7103
7111
|
attachments,
|
|
7104
7112
|
labels: labelIds,
|
|
7113
|
+
headers: threadingHeaders(headers),
|
|
7105
7114
|
flags: {
|
|
7106
7115
|
read: !labelIds.includes("UNREAD"),
|
|
7107
7116
|
starred: labelIds.includes("STARRED"),
|
|
@@ -7358,7 +7367,7 @@ var init_adapter = __esm({
|
|
|
7358
7367
|
const raw = await this.buildRaw(params);
|
|
7359
7368
|
const res = await gmail.users.messages.send({
|
|
7360
7369
|
userId: "me",
|
|
7361
|
-
requestBody: { raw }
|
|
7370
|
+
requestBody: { raw, ...this.threadFor(params) }
|
|
7362
7371
|
});
|
|
7363
7372
|
return {
|
|
7364
7373
|
id: res.data.id || "",
|
|
@@ -7371,19 +7380,53 @@ var init_adapter = __esm({
|
|
|
7371
7380
|
const res = await gmail.users.drafts.create({
|
|
7372
7381
|
userId: "me",
|
|
7373
7382
|
requestBody: {
|
|
7374
|
-
message: { raw }
|
|
7383
|
+
message: { raw, ...this.threadFor(params) }
|
|
7375
7384
|
}
|
|
7376
7385
|
});
|
|
7377
7386
|
return { id: res.data.id || "" };
|
|
7378
7387
|
}
|
|
7379
|
-
|
|
7388
|
+
/**
|
|
7389
|
+
* drafts.update replaces the whole message, so what the caller does not
|
|
7390
|
+
* name would be lost: a reply draft would fall out of its thread and a
|
|
7391
|
+
* draft's files would be dropped. So when the caller names no thread, the
|
|
7392
|
+
* thread and the reply headers of the stored draft are carried over, and
|
|
7393
|
+
* when the caller names no attachments, the stored draft's files are. A
|
|
7394
|
+
* given list (an empty one included) replaces the files.
|
|
7395
|
+
*/
|
|
7396
|
+
async updateDraft(draftId, update) {
|
|
7380
7397
|
const gmail = this.ensureConnected();
|
|
7398
|
+
let params = update;
|
|
7399
|
+
const keepThread = update.threadId === void 0;
|
|
7400
|
+
const keepFiles = update.attachments === void 0;
|
|
7401
|
+
if (keepThread || keepFiles) {
|
|
7402
|
+
const existing = await gmail.users.drafts.get({
|
|
7403
|
+
userId: "me",
|
|
7404
|
+
id: draftId,
|
|
7405
|
+
// 'metadata' is ids, labels and headers, no body (drafts.get has no
|
|
7406
|
+
// way to ask for single headers, so all of them come back). The parts
|
|
7407
|
+
// that list the files only come with 'full'.
|
|
7408
|
+
format: keepFiles ? "full" : "metadata"
|
|
7409
|
+
});
|
|
7410
|
+
const message = existing.data.message;
|
|
7411
|
+
if (keepThread) {
|
|
7412
|
+
const header = (name) => (message?.payload?.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || void 0;
|
|
7413
|
+
const inReplyTo = update.inReplyTo ?? header("in-reply-to");
|
|
7414
|
+
const references = update.references ?? header("references")?.split(/\s+/).filter(Boolean);
|
|
7415
|
+
if (message?.threadId && inReplyTo) {
|
|
7416
|
+
params = { ...params, threadId: message.threadId, inReplyTo, references: references?.length ? references : [inReplyTo] };
|
|
7417
|
+
}
|
|
7418
|
+
}
|
|
7419
|
+
if (keepFiles && message?.id) {
|
|
7420
|
+
const stored = await this.storedAttachments(message.id, message.payload);
|
|
7421
|
+
if (stored.length) params = { ...params, attachments: stored };
|
|
7422
|
+
}
|
|
7423
|
+
}
|
|
7381
7424
|
const raw = await this.buildRaw(params);
|
|
7382
7425
|
const res = await gmail.users.drafts.update({
|
|
7383
7426
|
userId: "me",
|
|
7384
7427
|
id: draftId,
|
|
7385
7428
|
requestBody: {
|
|
7386
|
-
message: { raw }
|
|
7429
|
+
message: { raw, ...this.threadFor(params) }
|
|
7387
7430
|
}
|
|
7388
7431
|
});
|
|
7389
7432
|
return { id: res.data.id || draftId };
|
|
@@ -7762,6 +7805,51 @@ var init_adapter = __esm({
|
|
|
7762
7805
|
}
|
|
7763
7806
|
await this.deleteBlockRule(ruleId);
|
|
7764
7807
|
}
|
|
7808
|
+
/**
|
|
7809
|
+
* Gmail files a message into an existing thread only when the threadId
|
|
7810
|
+
* comes with In-Reply-To and References headers and a subject (which has to
|
|
7811
|
+
* match the thread's; the reply tool builds "Re: <original subject>"). A
|
|
7812
|
+
* threadId without them is left out, and the message starts its own thread.
|
|
7813
|
+
*/
|
|
7814
|
+
threadFor(params) {
|
|
7815
|
+
const belongs = params.threadId && params.inReplyTo && params.references?.length && params.subject.trim();
|
|
7816
|
+
return belongs ? { threadId: params.threadId } : {};
|
|
7817
|
+
}
|
|
7818
|
+
/**
|
|
7819
|
+
* The files of a stored message, with their bytes: every part that has a
|
|
7820
|
+
* file name, at any depth. Large files are referenced by an attachment id
|
|
7821
|
+
* and fetched; small ones can come inline in the part. A fetch that fails
|
|
7822
|
+
* is not swallowed: a draft update built on it would drop that file.
|
|
7823
|
+
*/
|
|
7824
|
+
async storedAttachments(messageId, payload) {
|
|
7825
|
+
const gmail = this.ensureConnected();
|
|
7826
|
+
const files = [];
|
|
7827
|
+
const html = (extractBody(payload).html || "").toLowerCase();
|
|
7828
|
+
const visit = async (part) => {
|
|
7829
|
+
if (!part) return;
|
|
7830
|
+
const partHeader = (name) => (part.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || "";
|
|
7831
|
+
const contentId = partHeader("content-id").trim().replace(/^<|>$/g, "").toLowerCase();
|
|
7832
|
+
const embedded = /^\s*inline/i.test(partHeader("content-disposition")) && contentId !== "" && html.includes(`cid:${contentId}`);
|
|
7833
|
+
if (part.filename && !embedded) {
|
|
7834
|
+
let data = part.body?.data;
|
|
7835
|
+
if (!data && part.body?.attachmentId) {
|
|
7836
|
+
const res = await gmail.users.messages.attachments.get({ userId: "me", messageId, id: part.body.attachmentId });
|
|
7837
|
+
data = res.data.data;
|
|
7838
|
+
}
|
|
7839
|
+
if (data === void 0 || data === null) {
|
|
7840
|
+
throw new Error(`Could not read the draft's attachment ${part.filename}; the draft was not changed.`);
|
|
7841
|
+
}
|
|
7842
|
+
files.push({
|
|
7843
|
+
filename: part.filename,
|
|
7844
|
+
content: Buffer.from(data, "base64url"),
|
|
7845
|
+
contentType: part.mimeType || "application/octet-stream"
|
|
7846
|
+
});
|
|
7847
|
+
}
|
|
7848
|
+
for (const child of part.parts || []) await visit(child);
|
|
7849
|
+
};
|
|
7850
|
+
await visit(payload);
|
|
7851
|
+
return files;
|
|
7852
|
+
}
|
|
7765
7853
|
/**
|
|
7766
7854
|
* Builds the base64url `raw` payload Gmail expects for send, draft create and
|
|
7767
7855
|
* draft update: a full MIME message with a text part, an HTML part when one
|
|
@@ -8055,12 +8143,41 @@ var init_mapper2 = __esm({
|
|
|
8055
8143
|
|
|
8056
8144
|
// src/providers/outlook/adapter.ts
|
|
8057
8145
|
import { Client } from "@microsoft/microsoft-graph-client";
|
|
8058
|
-
|
|
8146
|
+
function megabytes(bytes) {
|
|
8147
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
8148
|
+
}
|
|
8149
|
+
function assertOutlookAttachmentSizes(attachments) {
|
|
8150
|
+
if (!attachments?.length) return;
|
|
8151
|
+
const limit = megabytes(OUTLOOK_MAX_ATTACHMENT_BYTES);
|
|
8152
|
+
for (const att of attachments) {
|
|
8153
|
+
if (att.content.length > OUTLOOK_MAX_ATTACHMENT_BYTES) {
|
|
8154
|
+
throw new Error(`Outlook accepts attachments up to ${limit} through email-mcp; ${att.filename} is ${megabytes(att.content.length)}. ${UPLOAD_SESSION_HINT}`);
|
|
8155
|
+
}
|
|
8156
|
+
}
|
|
8157
|
+
}
|
|
8158
|
+
function assertOutlookRequestSize(body) {
|
|
8159
|
+
const bytes = Buffer.byteLength(JSON.stringify(body));
|
|
8160
|
+
if (bytes > OUTLOOK_MAX_REQUEST_BYTES) {
|
|
8161
|
+
throw new Error(
|
|
8162
|
+
`Outlook accepts requests up to ${megabytes(OUTLOOK_MAX_REQUEST_BYTES)} through email-mcp; this message is ${megabytes(bytes)} once its attachments are base64-encoded. ${UPLOAD_SESSION_HINT}`
|
|
8163
|
+
);
|
|
8164
|
+
}
|
|
8165
|
+
}
|
|
8166
|
+
var toGraphAttachment, OUTLOOK_MAX_ATTACHMENT_BYTES, OUTLOOK_MAX_REQUEST_BYTES, UPLOAD_SESSION_HINT, OutlookAdapter;
|
|
8059
8167
|
var init_adapter2 = __esm({
|
|
8060
8168
|
"src/providers/outlook/adapter.ts"() {
|
|
8061
8169
|
"use strict";
|
|
8062
8170
|
init_types();
|
|
8063
8171
|
init_mapper2();
|
|
8172
|
+
toGraphAttachment = (att) => ({
|
|
8173
|
+
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
8174
|
+
name: att.filename,
|
|
8175
|
+
contentType: att.contentType,
|
|
8176
|
+
contentBytes: att.content.toString("base64")
|
|
8177
|
+
});
|
|
8178
|
+
OUTLOOK_MAX_ATTACHMENT_BYTES = 3 * 1024 * 1024;
|
|
8179
|
+
OUTLOOK_MAX_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
8180
|
+
UPLOAD_SESSION_HINT = "Larger files need an upload session, which email-mcp does not implement for Outlook yet.";
|
|
8064
8181
|
OutlookAdapter = class _OutlookAdapter {
|
|
8065
8182
|
providerType = ProviderType.Outlook;
|
|
8066
8183
|
client = null;
|
|
@@ -8079,8 +8196,8 @@ var init_adapter2 = __esm({
|
|
|
8079
8196
|
}
|
|
8080
8197
|
});
|
|
8081
8198
|
const originalApi = client.api.bind(client);
|
|
8082
|
-
client.api = (
|
|
8083
|
-
return originalApi(
|
|
8199
|
+
client.api = (path5) => {
|
|
8200
|
+
return originalApi(path5).header("Prefer", 'IdType="ImmutableId"');
|
|
8084
8201
|
};
|
|
8085
8202
|
this.client = client;
|
|
8086
8203
|
}
|
|
@@ -8200,31 +8317,125 @@ var init_adapter2 = __esm({
|
|
|
8200
8317
|
meta: mapGraphAttachment(attachment)
|
|
8201
8318
|
};
|
|
8202
8319
|
}
|
|
8320
|
+
/**
|
|
8321
|
+
* Graph only threads a reply (conversationId, subject, quoted history)
|
|
8322
|
+
* through the createReply flow: POST /me/messages and /me/sendMail always
|
|
8323
|
+
* start a new conversation whatever In-Reply-To says. Build the reply draft,
|
|
8324
|
+
* overlay our subject, body, recipients and attachments, and return its id.
|
|
8325
|
+
* Note the body replaces Graph's pre-filled quoted history.
|
|
8326
|
+
*
|
|
8327
|
+
* Each attachment goes up in a request of its own, so the sizes are checked
|
|
8328
|
+
* per file, and before createReply so that a file that is too big leaves no
|
|
8329
|
+
* draft behind. A draft that cannot be filled is deleted again. (A draft
|
|
8330
|
+
* whose send failed is kept, see `sendEmail`.)
|
|
8331
|
+
*/
|
|
8332
|
+
async createReplyDraft(params, replyToId) {
|
|
8333
|
+
const client = this.ensureClient();
|
|
8334
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
8335
|
+
const draft = await client.api(`/me/messages/${encodeURIComponent(replyToId)}/createReply`).post({});
|
|
8336
|
+
try {
|
|
8337
|
+
await this.fillReplyDraft(draft.id, params);
|
|
8338
|
+
} catch (error48) {
|
|
8339
|
+
throw await this.discardDraft(draft.id, error48);
|
|
8340
|
+
}
|
|
8341
|
+
return draft.id;
|
|
8342
|
+
}
|
|
8343
|
+
async fillReplyDraft(draftId, params) {
|
|
8344
|
+
const client = this.ensureClient();
|
|
8345
|
+
const toGraphRecipient = (c) => ({
|
|
8346
|
+
emailAddress: { name: c.name, address: c.email }
|
|
8347
|
+
});
|
|
8348
|
+
const patch = {
|
|
8349
|
+
// Always the caller's subject: Graph pre-fills its own "RE: ..." one.
|
|
8350
|
+
subject: params.subject,
|
|
8351
|
+
body: { contentType: params.body.html ? "html" : "text", content: params.body.html || params.body.text || "" }
|
|
8352
|
+
};
|
|
8353
|
+
if (params.to?.length) patch.toRecipients = params.to.map(toGraphRecipient);
|
|
8354
|
+
if (params.cc !== void 0) patch.ccRecipients = params.cc.map(toGraphRecipient);
|
|
8355
|
+
if (params.bcc !== void 0) patch.bccRecipients = params.bcc.map(toGraphRecipient);
|
|
8356
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(patch);
|
|
8357
|
+
for (const att of params.attachments ?? []) {
|
|
8358
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post(toGraphAttachment(att));
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
8361
|
+
/**
|
|
8362
|
+
* Deletes a reply draft that could not be filled, so a failed reply leaves
|
|
8363
|
+
* nothing half-built in Drafts. Returns the error to throw: the original
|
|
8364
|
+
* one, or, when the delete fails as well, the original message with a note
|
|
8365
|
+
* that the draft is still there.
|
|
8366
|
+
*/
|
|
8367
|
+
async discardDraft(draftId, cause) {
|
|
8368
|
+
try {
|
|
8369
|
+
await this.ensureClient().api(`/me/messages/${encodeURIComponent(draftId)}`).delete();
|
|
8370
|
+
return cause;
|
|
8371
|
+
} catch (deleteError) {
|
|
8372
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
8373
|
+
return new Error(
|
|
8374
|
+
`${message} (the reply draft ${draftId} could not be removed and was left in Drafts: ${deleteError?.message ?? deleteError})`,
|
|
8375
|
+
{ cause }
|
|
8376
|
+
);
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8203
8379
|
async sendEmail(params) {
|
|
8204
8380
|
const client = this.ensureClient();
|
|
8381
|
+
if (params.replyToGraphId) {
|
|
8382
|
+
const draftId = await this.createReplyDraft(params, params.replyToGraphId);
|
|
8383
|
+
try {
|
|
8384
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}/send`).post({});
|
|
8385
|
+
} catch (error48) {
|
|
8386
|
+
throw new Error(
|
|
8387
|
+
`${error48?.message ?? error48} (the reply was saved as draft ${draftId} in Drafts and was not confirmed as sent; check Sent Items before sending it again)`,
|
|
8388
|
+
{ cause: error48 }
|
|
8389
|
+
);
|
|
8390
|
+
}
|
|
8391
|
+
return { id: draftId };
|
|
8392
|
+
}
|
|
8393
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
8205
8394
|
const message = this.buildGraphMessage(params);
|
|
8206
8395
|
const payload = { message };
|
|
8207
8396
|
if (params.attachments?.length) {
|
|
8208
|
-
payload.message.attachments = params.attachments.map(
|
|
8209
|
-
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
8210
|
-
name: att.filename,
|
|
8211
|
-
contentType: att.contentType,
|
|
8212
|
-
contentBytes: att.content.toString("base64")
|
|
8213
|
-
}));
|
|
8397
|
+
payload.message.attachments = params.attachments.map(toGraphAttachment);
|
|
8214
8398
|
}
|
|
8399
|
+
assertOutlookRequestSize(payload);
|
|
8215
8400
|
await client.api("/me/sendMail").post(payload);
|
|
8216
8401
|
return { id: `sent-${Date.now()}` };
|
|
8217
8402
|
}
|
|
8218
8403
|
async createDraft(params) {
|
|
8219
8404
|
const client = this.ensureClient();
|
|
8405
|
+
if (params.replyToGraphId) {
|
|
8406
|
+
return { id: await this.createReplyDraft(params, params.replyToGraphId) };
|
|
8407
|
+
}
|
|
8408
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
8220
8409
|
const message = this.buildGraphMessage(params);
|
|
8410
|
+
if (params.attachments?.length) {
|
|
8411
|
+
message.attachments = params.attachments.map(toGraphAttachment);
|
|
8412
|
+
}
|
|
8413
|
+
assertOutlookRequestSize(message);
|
|
8221
8414
|
const result = await client.api("/me/messages").post(message);
|
|
8222
8415
|
return { id: result.id };
|
|
8223
8416
|
}
|
|
8224
8417
|
async updateDraft(draftId, params) {
|
|
8225
8418
|
const client = this.ensureClient();
|
|
8419
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
8226
8420
|
const message = this.buildGraphMessage(params);
|
|
8227
8421
|
await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(message);
|
|
8422
|
+
if (params.attachments) {
|
|
8423
|
+
const attachmentsPath = `/me/messages/${encodeURIComponent(draftId)}/attachments`;
|
|
8424
|
+
const previousIds = [];
|
|
8425
|
+
let page = await client.api(attachmentsPath).select("id").get();
|
|
8426
|
+
for (; ; ) {
|
|
8427
|
+
previousIds.push(...(page.value || []).map((att) => att.id));
|
|
8428
|
+
const next = page["@odata.nextLink"];
|
|
8429
|
+
if (!next) break;
|
|
8430
|
+
page = await client.api(next).get();
|
|
8431
|
+
}
|
|
8432
|
+
for (const att of params.attachments) {
|
|
8433
|
+
await client.api(attachmentsPath).post(toGraphAttachment(att));
|
|
8434
|
+
}
|
|
8435
|
+
for (const id of previousIds) {
|
|
8436
|
+
await client.api(`${attachmentsPath}/${encodeURIComponent(id)}`).delete();
|
|
8437
|
+
}
|
|
8438
|
+
}
|
|
8228
8439
|
return { id: draftId };
|
|
8229
8440
|
}
|
|
8230
8441
|
async listDrafts(limit, offset) {
|
|
@@ -8781,6 +8992,12 @@ function mapImapFolder(imapFolder) {
|
|
|
8781
8992
|
unreadCount: imapFolder.status?.unseen
|
|
8782
8993
|
};
|
|
8783
8994
|
}
|
|
8995
|
+
function imapAttachmentId(att, index) {
|
|
8996
|
+
return att.contentId || `att-${index}`;
|
|
8997
|
+
}
|
|
8998
|
+
function headerValue(line) {
|
|
8999
|
+
return line.slice(line.indexOf(":") + 1).replace(/\r?\n[ \t]+/g, " ").trim();
|
|
9000
|
+
}
|
|
8784
9001
|
function mapParsedEmail(parsed, folder, accountId, uid) {
|
|
8785
9002
|
const mapContact = (addr) => ({
|
|
8786
9003
|
name: addr.name || void 0,
|
|
@@ -8791,7 +9008,7 @@ function mapParsedEmail(parsed, folder, accountId, uid) {
|
|
|
8791
9008
|
return addrs.value.map(mapContact);
|
|
8792
9009
|
};
|
|
8793
9010
|
const attachments = (parsed.attachments || []).map((att, i) => ({
|
|
8794
|
-
id: att
|
|
9011
|
+
id: imapAttachmentId(att, i),
|
|
8795
9012
|
filename: att.filename || `attachment-${i}`,
|
|
8796
9013
|
contentType: att.contentType || "application/octet-stream",
|
|
8797
9014
|
size: att.size || 0
|
|
@@ -8820,7 +9037,7 @@ function mapParsedEmail(parsed, folder, accountId, uid) {
|
|
|
8820
9037
|
flagged: parsed.flags?.has("\\Flagged") || false,
|
|
8821
9038
|
draft: parsed.flags?.has("\\Draft") || false
|
|
8822
9039
|
},
|
|
8823
|
-
headers: parsed.headerLines ? Object.fromEntries(parsed.headerLines.map((h) => [h.key, h.line])) : void 0
|
|
9040
|
+
headers: parsed.headerLines ? Object.fromEntries(parsed.headerLines.map((h) => [h.key, headerValue(h.line)])) : void 0
|
|
8824
9041
|
};
|
|
8825
9042
|
}
|
|
8826
9043
|
var SPECIAL_USE_MAP;
|
|
@@ -9248,7 +9465,7 @@ var init_adapter3 = __esm({
|
|
|
9248
9465
|
}
|
|
9249
9466
|
async getEmail(id, folder) {
|
|
9250
9467
|
const client = await this.ensureConnected();
|
|
9251
|
-
const targetFolder = folder
|
|
9468
|
+
const targetFolder = folder ? await this.resolveFolder(folder) : "INBOX";
|
|
9252
9469
|
let lock;
|
|
9253
9470
|
try {
|
|
9254
9471
|
lock = await client.getMailboxLock(targetFolder);
|
|
@@ -9267,13 +9484,20 @@ var init_adapter3 = __esm({
|
|
|
9267
9484
|
lock.release();
|
|
9268
9485
|
}
|
|
9269
9486
|
}
|
|
9270
|
-
|
|
9487
|
+
/**
|
|
9488
|
+
* IMAP has no thread object: the thread is the set of messages in ONE
|
|
9489
|
+
* folder whose Message-ID, References or In-Reply-To name `threadId`.
|
|
9490
|
+
* Messages of the same conversation in other folders (the own replies in
|
|
9491
|
+
* Sent, for example) are not part of the result.
|
|
9492
|
+
*/
|
|
9493
|
+
async getThread(threadId, sourceFolder) {
|
|
9271
9494
|
const client = await this.ensureConnected();
|
|
9495
|
+
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
9272
9496
|
let lock;
|
|
9273
9497
|
try {
|
|
9274
|
-
lock = await client.getMailboxLock(
|
|
9498
|
+
lock = await client.getMailboxLock(folder);
|
|
9275
9499
|
} catch (error48) {
|
|
9276
|
-
throw formatImapError(error48,
|
|
9500
|
+
throw formatImapError(error48, `Failed to open folder "${folder}"`);
|
|
9277
9501
|
}
|
|
9278
9502
|
try {
|
|
9279
9503
|
const searchResult = await client.search(
|
|
@@ -9281,13 +9505,13 @@ var init_adapter3 = __esm({
|
|
|
9281
9505
|
{ uid: true }
|
|
9282
9506
|
);
|
|
9283
9507
|
const uids = Array.isArray(searchResult) ? searchResult : [];
|
|
9284
|
-
if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
|
|
9508
|
+
if (uids.length === 0) throw new Error(`Thread ${threadId} not found in "${folder}"`);
|
|
9285
9509
|
const messages = [];
|
|
9286
9510
|
for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
|
|
9287
9511
|
if (!msg.source) continue;
|
|
9288
9512
|
const parsed = await simpleParser(msg.source);
|
|
9289
9513
|
parsed.flags = msg.flags;
|
|
9290
|
-
messages.push(mapParsedEmail(parsed,
|
|
9514
|
+
messages.push(mapParsedEmail(parsed, folder, this.accountId, msg.uid));
|
|
9291
9515
|
}
|
|
9292
9516
|
const participantMap = /* @__PURE__ */ new Map();
|
|
9293
9517
|
for (const msg of messages) {
|
|
@@ -9309,27 +9533,29 @@ var init_adapter3 = __esm({
|
|
|
9309
9533
|
lock.release();
|
|
9310
9534
|
}
|
|
9311
9535
|
}
|
|
9312
|
-
async getAttachment(emailId, attachmentId) {
|
|
9536
|
+
async getAttachment(emailId, attachmentId, sourceFolder) {
|
|
9313
9537
|
const client = await this.ensureConnected();
|
|
9538
|
+
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
9314
9539
|
let lock;
|
|
9315
9540
|
try {
|
|
9316
|
-
lock = await client.getMailboxLock(
|
|
9541
|
+
lock = await client.getMailboxLock(folder);
|
|
9317
9542
|
} catch (error48) {
|
|
9318
|
-
throw formatImapError(error48,
|
|
9543
|
+
throw formatImapError(error48, `Failed to open folder "${folder}"`);
|
|
9319
9544
|
}
|
|
9320
9545
|
try {
|
|
9321
9546
|
const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
|
|
9322
|
-
if (!msg) throw new Error(`Email ${emailId} not found`);
|
|
9547
|
+
if (!msg) throw new Error(`Email ${emailId} not found in "${folder}"`);
|
|
9323
9548
|
if (!msg.source) throw new Error(`Email ${emailId} has no source`);
|
|
9324
9549
|
const parsed = await simpleParser(msg.source);
|
|
9325
|
-
const
|
|
9326
|
-
|
|
9327
|
-
);
|
|
9328
|
-
if (
|
|
9550
|
+
const attachments = parsed.attachments || [];
|
|
9551
|
+
let index = attachments.findIndex((att, i) => imapAttachmentId(att, i) === attachmentId);
|
|
9552
|
+
if (index === -1) index = attachments.findIndex((att) => att.filename === attachmentId);
|
|
9553
|
+
if (index === -1) throw new Error(`Attachment ${attachmentId} not found in email ${emailId}`);
|
|
9554
|
+
const attachment = attachments[index];
|
|
9329
9555
|
return {
|
|
9330
9556
|
data: attachment.content,
|
|
9331
9557
|
meta: {
|
|
9332
|
-
id: attachment
|
|
9558
|
+
id: imapAttachmentId(attachment, index),
|
|
9333
9559
|
filename: attachment.filename || "attachment",
|
|
9334
9560
|
contentType: attachment.contentType || "application/octet-stream",
|
|
9335
9561
|
size: attachment.size || 0
|
|
@@ -9366,18 +9592,53 @@ var init_adapter3 = __esm({
|
|
|
9366
9592
|
* Drafts folder, and IMAP messages are immutable once appended. This
|
|
9367
9593
|
* deletes the old revision and appends a new one, so the returned `id`
|
|
9368
9594
|
* is a NEW uid; the old id no longer resolves after this call.
|
|
9595
|
+
*
|
|
9596
|
+
* A reply draft keeps its place in the thread: when the caller passes no
|
|
9597
|
+
* `inReplyTo`, the In-Reply-To and References headers of the old revision
|
|
9598
|
+
* are copied into the new one. The same goes for files: when the caller
|
|
9599
|
+
* passes no `attachments`, the old revision's attachments are copied over
|
|
9600
|
+
* (a given list, an empty one included, replaces them). Images embedded in
|
|
9601
|
+
* the old HTML body are not files and are not copied: the new body replaces
|
|
9602
|
+
* the old one.
|
|
9369
9603
|
*/
|
|
9370
9604
|
async updateDraft(draftId, params, sourceFolder) {
|
|
9371
9605
|
const client = await this.ensureConnected();
|
|
9372
9606
|
const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
|
|
9373
|
-
const rawMessage = await this.buildDraftMessage(params);
|
|
9374
9607
|
let lock;
|
|
9375
9608
|
try {
|
|
9376
9609
|
lock = await client.getMailboxLock(draftsFolder);
|
|
9377
9610
|
} catch (error48) {
|
|
9378
9611
|
throw formatImapError(error48, `Failed to open folder "${draftsFolder}"`);
|
|
9379
9612
|
}
|
|
9613
|
+
let rawMessage;
|
|
9380
9614
|
try {
|
|
9615
|
+
let next = params;
|
|
9616
|
+
const keepThread = params.inReplyTo === void 0;
|
|
9617
|
+
const keepFiles = params.attachments === void 0;
|
|
9618
|
+
if (keepThread || keepFiles) {
|
|
9619
|
+
const old = await client.fetchOne(String(draftId), { source: true, uid: true }, { uid: true });
|
|
9620
|
+
if (old && old.source) {
|
|
9621
|
+
const parsed = await simpleParser(old.source);
|
|
9622
|
+
if (keepThread && parsed.inReplyTo) {
|
|
9623
|
+
const references = [].concat(parsed.references ?? []);
|
|
9624
|
+
next = { ...next, inReplyTo: parsed.inReplyTo, references: params.references ?? references };
|
|
9625
|
+
}
|
|
9626
|
+
if (keepFiles) {
|
|
9627
|
+
const html = (parsed.html || "").toLowerCase();
|
|
9628
|
+
const files = (parsed.attachments || []).filter((att) => {
|
|
9629
|
+
if (!att.related) return true;
|
|
9630
|
+
const cid = String(att.cid || att.contentId || "").replace(/^<|>$/g, "").toLowerCase();
|
|
9631
|
+
return !cid || !html.includes(`cid:${cid}`);
|
|
9632
|
+
}).map((att, i) => ({
|
|
9633
|
+
filename: att.filename || `attachment-${i + 1}`,
|
|
9634
|
+
content: att.content,
|
|
9635
|
+
contentType: att.contentType || "application/octet-stream"
|
|
9636
|
+
}));
|
|
9637
|
+
if (files.length) next = { ...next, attachments: files };
|
|
9638
|
+
}
|
|
9639
|
+
}
|
|
9640
|
+
}
|
|
9641
|
+
rawMessage = await this.buildDraftMessage(next);
|
|
9381
9642
|
await client.messageDelete(draftId, { uid: true });
|
|
9382
9643
|
} finally {
|
|
9383
9644
|
lock.release();
|
|
@@ -9693,6 +9954,11 @@ var init_account_manager = __esm({
|
|
|
9693
9954
|
connected: this.providers.has(c.id)
|
|
9694
9955
|
}));
|
|
9695
9956
|
}
|
|
9957
|
+
/** The account's own address, or undefined for an unknown account id. */
|
|
9958
|
+
async getAccountEmail(accountId) {
|
|
9959
|
+
const creds = this.credentials.get(accountId) ?? await this.store.get(accountId);
|
|
9960
|
+
return creds?.email;
|
|
9961
|
+
}
|
|
9696
9962
|
async getProvider(accountId) {
|
|
9697
9963
|
const existing = this.providers.get(accountId);
|
|
9698
9964
|
if (existing) {
|
|
@@ -10807,8 +11073,8 @@ function getErrorMap() {
|
|
|
10807
11073
|
|
|
10808
11074
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
10809
11075
|
var makeIssue = (params) => {
|
|
10810
|
-
const { data, path:
|
|
10811
|
-
const fullPath = [...
|
|
11076
|
+
const { data, path: path5, errorMaps, issueData } = params;
|
|
11077
|
+
const fullPath = [...path5, ...issueData.path || []];
|
|
10812
11078
|
const fullIssue = {
|
|
10813
11079
|
...issueData,
|
|
10814
11080
|
path: fullPath
|
|
@@ -10923,11 +11189,11 @@ var errorUtil;
|
|
|
10923
11189
|
|
|
10924
11190
|
// node_modules/zod/v3/types.js
|
|
10925
11191
|
var ParseInputLazyPath = class {
|
|
10926
|
-
constructor(parent, value,
|
|
11192
|
+
constructor(parent, value, path5, key) {
|
|
10927
11193
|
this._cachedPath = [];
|
|
10928
11194
|
this.parent = parent;
|
|
10929
11195
|
this.data = value;
|
|
10930
|
-
this._path =
|
|
11196
|
+
this._path = path5;
|
|
10931
11197
|
this._key = key;
|
|
10932
11198
|
}
|
|
10933
11199
|
get path() {
|
|
@@ -14850,10 +15116,10 @@ function mergeDefs(...defs) {
|
|
|
14850
15116
|
function cloneDef(schema) {
|
|
14851
15117
|
return mergeDefs(schema._zod.def);
|
|
14852
15118
|
}
|
|
14853
|
-
function getElementAtPath(obj,
|
|
14854
|
-
if (!
|
|
15119
|
+
function getElementAtPath(obj, path5) {
|
|
15120
|
+
if (!path5)
|
|
14855
15121
|
return obj;
|
|
14856
|
-
return
|
|
15122
|
+
return path5.reduce((acc, key) => acc?.[key], obj);
|
|
14857
15123
|
}
|
|
14858
15124
|
function promiseAllObject(promisesObj) {
|
|
14859
15125
|
const keys = Object.keys(promisesObj);
|
|
@@ -15236,11 +15502,11 @@ function aborted(x, startIndex = 0) {
|
|
|
15236
15502
|
}
|
|
15237
15503
|
return false;
|
|
15238
15504
|
}
|
|
15239
|
-
function prefixIssues(
|
|
15505
|
+
function prefixIssues(path5, issues) {
|
|
15240
15506
|
return issues.map((iss) => {
|
|
15241
15507
|
var _a2;
|
|
15242
15508
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
15243
|
-
iss.path.unshift(
|
|
15509
|
+
iss.path.unshift(path5);
|
|
15244
15510
|
return iss;
|
|
15245
15511
|
});
|
|
15246
15512
|
}
|
|
@@ -15423,7 +15689,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
|
|
|
15423
15689
|
}
|
|
15424
15690
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
15425
15691
|
const result = { errors: [] };
|
|
15426
|
-
const processError = (error49,
|
|
15692
|
+
const processError = (error49, path5 = []) => {
|
|
15427
15693
|
var _a2, _b;
|
|
15428
15694
|
for (const issue2 of error49.issues) {
|
|
15429
15695
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -15433,7 +15699,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
15433
15699
|
} else if (issue2.code === "invalid_element") {
|
|
15434
15700
|
processError({ issues: issue2.issues }, issue2.path);
|
|
15435
15701
|
} else {
|
|
15436
|
-
const fullpath = [...
|
|
15702
|
+
const fullpath = [...path5, ...issue2.path];
|
|
15437
15703
|
if (fullpath.length === 0) {
|
|
15438
15704
|
result.errors.push(mapper(issue2));
|
|
15439
15705
|
continue;
|
|
@@ -15465,8 +15731,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
15465
15731
|
}
|
|
15466
15732
|
function toDotPath(_path) {
|
|
15467
15733
|
const segs = [];
|
|
15468
|
-
const
|
|
15469
|
-
for (const seg of
|
|
15734
|
+
const path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
15735
|
+
for (const seg of path5) {
|
|
15470
15736
|
if (typeof seg === "number")
|
|
15471
15737
|
segs.push(`[${seg}]`);
|
|
15472
15738
|
else if (typeof seg === "symbol")
|
|
@@ -27872,13 +28138,13 @@ function resolveRef(ref, ctx) {
|
|
|
27872
28138
|
if (!ref.startsWith("#")) {
|
|
27873
28139
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
27874
28140
|
}
|
|
27875
|
-
const
|
|
27876
|
-
if (
|
|
28141
|
+
const path5 = ref.slice(1).split("/").filter(Boolean);
|
|
28142
|
+
if (path5.length === 0) {
|
|
27877
28143
|
return ctx.rootSchema;
|
|
27878
28144
|
}
|
|
27879
28145
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
27880
|
-
if (
|
|
27881
|
-
const key =
|
|
28146
|
+
if (path5[0] === defsKey) {
|
|
28147
|
+
const key = path5[1];
|
|
27882
28148
|
if (!key || !ctx.defs[key]) {
|
|
27883
28149
|
throw new Error(`Reference not found: ${ref}`);
|
|
27884
28150
|
}
|
|
@@ -33807,12 +34073,13 @@ function registerReadingTools(server, accountManager) {
|
|
|
33807
34073
|
"Get a single email by ID",
|
|
33808
34074
|
{
|
|
33809
34075
|
accountId: external_exports3.string(),
|
|
33810
|
-
emailId: external_exports3.string()
|
|
34076
|
+
emailId: external_exports3.string(),
|
|
34077
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
|
|
33811
34078
|
},
|
|
33812
34079
|
async (args) => {
|
|
33813
34080
|
try {
|
|
33814
34081
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33815
|
-
const email3 = await provider.getEmail(args.emailId);
|
|
34082
|
+
const email3 = await provider.getEmail(args.emailId, args.sourceFolder);
|
|
33816
34083
|
return jsonResult2(email3);
|
|
33817
34084
|
} catch (error48) {
|
|
33818
34085
|
return jsonResult2({ error: error48.message });
|
|
@@ -33824,12 +34091,13 @@ function registerReadingTools(server, accountManager) {
|
|
|
33824
34091
|
"Get an email thread by thread ID",
|
|
33825
34092
|
{
|
|
33826
34093
|
accountId: external_exports3.string(),
|
|
33827
|
-
threadId: external_exports3.string()
|
|
34094
|
+
threadId: external_exports3.string(),
|
|
34095
|
+
sourceFolder: external_exports3.string().optional().describe("Folder to look for the thread in on IMAP/iCloud, where a thread is searched in one folder (default INBOX). Gmail and Outlook ignore it")
|
|
33828
34096
|
},
|
|
33829
34097
|
async (args) => {
|
|
33830
34098
|
try {
|
|
33831
34099
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33832
|
-
const thread = await provider.getThread(args.threadId);
|
|
34100
|
+
const thread = await provider.getThread(args.threadId, args.sourceFolder);
|
|
33833
34101
|
return jsonResult2(thread);
|
|
33834
34102
|
} catch (error48) {
|
|
33835
34103
|
return jsonResult2({ error: error48.message });
|
|
@@ -33842,12 +34110,13 @@ function registerReadingTools(server, accountManager) {
|
|
|
33842
34110
|
{
|
|
33843
34111
|
accountId: external_exports3.string(),
|
|
33844
34112
|
emailId: external_exports3.string(),
|
|
33845
|
-
attachmentId: external_exports3.string()
|
|
34113
|
+
attachmentId: external_exports3.string(),
|
|
34114
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
|
|
33846
34115
|
},
|
|
33847
34116
|
async (args) => {
|
|
33848
34117
|
try {
|
|
33849
34118
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33850
|
-
const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
|
|
34119
|
+
const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId, args.sourceFolder);
|
|
33851
34120
|
return jsonResult2({
|
|
33852
34121
|
data: Buffer.from(data).toString("base64"),
|
|
33853
34122
|
meta: meta3
|
|
@@ -33864,13 +34133,14 @@ function registerReadingTools(server, accountManager) {
|
|
|
33864
34133
|
accountId: external_exports3.string(),
|
|
33865
34134
|
emailId: external_exports3.string(),
|
|
33866
34135
|
attachmentId: external_exports3.string(),
|
|
33867
|
-
outputPath: external_exports3.string().describe("Relative file path (within the downloads directory) to save the attachment as")
|
|
34136
|
+
outputPath: external_exports3.string().describe("Relative file path (within the downloads directory) to save the attachment as"),
|
|
34137
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
|
|
33868
34138
|
},
|
|
33869
34139
|
async (args) => {
|
|
33870
34140
|
try {
|
|
33871
34141
|
const absPath = resolveDownloadPath(args.outputPath);
|
|
33872
34142
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33873
|
-
const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
|
|
34143
|
+
const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId, args.sourceFolder);
|
|
33874
34144
|
fs3.mkdirSync(path3.dirname(absPath), { recursive: true, mode: 448 });
|
|
33875
34145
|
fs3.writeFileSync(absPath, Buffer.from(data), { mode: 384 });
|
|
33876
34146
|
fs3.chmodSync(absPath, 384);
|
|
@@ -33887,6 +34157,161 @@ function registerReadingTools(server, accountManager) {
|
|
|
33887
34157
|
);
|
|
33888
34158
|
}
|
|
33889
34159
|
|
|
34160
|
+
// src/tools/attachments.ts
|
|
34161
|
+
import { closeSync, constants as fsConstants, fstatSync, lstatSync, openSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
34162
|
+
import os4 from "node:os";
|
|
34163
|
+
import path4 from "node:path";
|
|
34164
|
+
var MAX_TOTAL_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
34165
|
+
var ATTACHMENTS_DIR_ENV = "EMAIL_MCP_ATTACHMENTS_DIR";
|
|
34166
|
+
var MIME_BY_EXT = {
|
|
34167
|
+
".pdf": "application/pdf",
|
|
34168
|
+
".png": "image/png",
|
|
34169
|
+
".jpg": "image/jpeg",
|
|
34170
|
+
".jpeg": "image/jpeg",
|
|
34171
|
+
".gif": "image/gif",
|
|
34172
|
+
".webp": "image/webp",
|
|
34173
|
+
".txt": "text/plain",
|
|
34174
|
+
".csv": "text/csv",
|
|
34175
|
+
".html": "text/html",
|
|
34176
|
+
".json": "application/json",
|
|
34177
|
+
".zip": "application/zip",
|
|
34178
|
+
".doc": "application/msword",
|
|
34179
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
34180
|
+
".xls": "application/vnd.ms-excel",
|
|
34181
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
34182
|
+
".ics": "text/calendar"
|
|
34183
|
+
};
|
|
34184
|
+
var AttachmentInputSchema = external_exports3.object({
|
|
34185
|
+
path: external_exports3.string().optional().describe(
|
|
34186
|
+
`A file inside the folder the user set in ${ATTACHMENTS_DIR_ENV}: its name, a path relative to that folder, or an absolute path inside it. Files anywhere else are refused, and path attachments are off when that variable is not set.`
|
|
34187
|
+
),
|
|
34188
|
+
content: external_exports3.string().optional().describe("Base64-encoded file content, used instead of path"),
|
|
34189
|
+
filename: external_exports3.string().optional().describe("File name shown to the recipient. Required with content; defaults to the file name of path"),
|
|
34190
|
+
contentType: external_exports3.string().optional().describe("MIME type. Inferred from the file extension when omitted")
|
|
34191
|
+
}).describe("One attachment: give either path or content (base64) plus filename");
|
|
34192
|
+
var AttachmentsSchema = external_exports3.array(AttachmentInputSchema).optional().describe(`Files to attach. Total size is capped at ${MAX_TOTAL_ATTACHMENT_BYTES / (1024 * 1024)} MB`);
|
|
34193
|
+
function inferContentType(filename) {
|
|
34194
|
+
return MIME_BY_EXT[path4.extname(filename).toLowerCase()] ?? "application/octet-stream";
|
|
34195
|
+
}
|
|
34196
|
+
function megabytes2(bytes) {
|
|
34197
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
34198
|
+
}
|
|
34199
|
+
function isInside(dir, target) {
|
|
34200
|
+
const relative = path4.relative(dir, target);
|
|
34201
|
+
return relative !== "" && !relative.startsWith("..") && !path4.isAbsolute(relative);
|
|
34202
|
+
}
|
|
34203
|
+
function attachmentsDir(env = process.env) {
|
|
34204
|
+
const raw = (env[ATTACHMENTS_DIR_ENV] || "").trim();
|
|
34205
|
+
if (!raw) return null;
|
|
34206
|
+
const expanded = raw === "~" || raw.startsWith("~/") ? path4.join(os4.homedir(), raw.slice(1)) : raw;
|
|
34207
|
+
return path4.resolve(expanded);
|
|
34208
|
+
}
|
|
34209
|
+
var HOW_TO_ALLOW = `Set ${ATTACHMENTS_DIR_ENV} to the one folder files may be attached from (in the environment of the email-mcp server, in the MCP client configuration), put the file there and restart the client. Content passed directly as base64 with a filename works without it.`;
|
|
34210
|
+
function planPath(input2, env, dataDir) {
|
|
34211
|
+
const given = input2.path;
|
|
34212
|
+
if (given.trim() === "") throw new Error("Attachment path is empty");
|
|
34213
|
+
const dir = attachmentsDir(env);
|
|
34214
|
+
if (!dir) {
|
|
34215
|
+
throw new Error(`Attaching files by path is switched off: ${ATTACHMENTS_DIR_ENV} is not set. ${HOW_TO_ALLOW}`);
|
|
34216
|
+
}
|
|
34217
|
+
let dirReal;
|
|
34218
|
+
try {
|
|
34219
|
+
dirReal = realpathSync(dir);
|
|
34220
|
+
if (!statSync(dirReal).isDirectory()) throw new Error("not a directory");
|
|
34221
|
+
} catch {
|
|
34222
|
+
throw new Error(`The attachments folder ${dir} (${ATTACHMENTS_DIR_ENV}) does not exist or is not a folder.`);
|
|
34223
|
+
}
|
|
34224
|
+
const candidate = path4.resolve(dir, given);
|
|
34225
|
+
if (!isInside(dir, candidate) && !isInside(dirReal, candidate)) {
|
|
34226
|
+
throw new Error(`Only files inside ${dir} can be attached (${ATTACHMENTS_DIR_ENV}). Put the file there and pass its name.`);
|
|
34227
|
+
}
|
|
34228
|
+
let real;
|
|
34229
|
+
try {
|
|
34230
|
+
real = realpathSync(candidate);
|
|
34231
|
+
} catch {
|
|
34232
|
+
throw new Error(`No such file in the attachments folder: ${path4.relative(dir, candidate)}`);
|
|
34233
|
+
}
|
|
34234
|
+
if (!isInside(dirReal, real)) {
|
|
34235
|
+
throw new Error(`Only files inside ${dir} can be attached (${ATTACHMENTS_DIR_ENV}); ${path4.relative(dir, candidate)} points outside it.`);
|
|
34236
|
+
}
|
|
34237
|
+
let ownDir = null;
|
|
34238
|
+
try {
|
|
34239
|
+
ownDir = realpathSync(dataDir);
|
|
34240
|
+
} catch {
|
|
34241
|
+
ownDir = null;
|
|
34242
|
+
}
|
|
34243
|
+
if (ownDir && path4.dirname(real) === ownDir) {
|
|
34244
|
+
throw new Error("Files in the email-mcp data folder itself cannot be attached.");
|
|
34245
|
+
}
|
|
34246
|
+
const stat = lstatSync(real);
|
|
34247
|
+
if (!stat.isFile()) {
|
|
34248
|
+
throw new Error(`Not a file: ${path4.relative(dir, candidate)}`);
|
|
34249
|
+
}
|
|
34250
|
+
const filename = input2.filename ?? path4.basename(real);
|
|
34251
|
+
return {
|
|
34252
|
+
bytes: stat.size,
|
|
34253
|
+
load: () => ({
|
|
34254
|
+
filename,
|
|
34255
|
+
content: readValidatedFile(real, stat, path4.relative(dir, candidate)),
|
|
34256
|
+
contentType: input2.contentType ?? inferContentType(filename)
|
|
34257
|
+
})
|
|
34258
|
+
};
|
|
34259
|
+
}
|
|
34260
|
+
function readValidatedFile(real, validated, display) {
|
|
34261
|
+
let fd;
|
|
34262
|
+
try {
|
|
34263
|
+
fd = openSync(real, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
34264
|
+
} catch {
|
|
34265
|
+
throw new Error(`${display} changed while it was being attached; try again.`);
|
|
34266
|
+
}
|
|
34267
|
+
try {
|
|
34268
|
+
const opened = fstatSync(fd);
|
|
34269
|
+
if (!opened.isFile() || opened.dev !== validated.dev || opened.ino !== validated.ino) {
|
|
34270
|
+
throw new Error(`${display} changed while it was being attached; try again.`);
|
|
34271
|
+
}
|
|
34272
|
+
return readFileSync(fd);
|
|
34273
|
+
} finally {
|
|
34274
|
+
closeSync(fd);
|
|
34275
|
+
}
|
|
34276
|
+
}
|
|
34277
|
+
function planContent(input2) {
|
|
34278
|
+
if (!input2.filename) throw new Error("Attachment with content requires a filename");
|
|
34279
|
+
const filename = input2.filename;
|
|
34280
|
+
const compact = input2.content.replace(/\s+/g, "");
|
|
34281
|
+
if (compact === "") throw new Error(`Attachment ${filename} has empty content`);
|
|
34282
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(compact) || compact.length % 4 === 1) {
|
|
34283
|
+
throw new Error(`Attachment ${filename}: content is not valid base64`);
|
|
34284
|
+
}
|
|
34285
|
+
const padding = compact.endsWith("==") ? 2 : compact.endsWith("=") ? 1 : 0;
|
|
34286
|
+
return {
|
|
34287
|
+
bytes: Math.floor(compact.length * 3 / 4) - padding,
|
|
34288
|
+
load: () => ({
|
|
34289
|
+
filename,
|
|
34290
|
+
content: Buffer.from(compact, "base64"),
|
|
34291
|
+
contentType: input2.contentType ?? inferContentType(filename)
|
|
34292
|
+
})
|
|
34293
|
+
};
|
|
34294
|
+
}
|
|
34295
|
+
function plan(input2, env, dataDir) {
|
|
34296
|
+
const hasPath = input2.path !== void 0;
|
|
34297
|
+
const hasContent = input2.content !== void 0;
|
|
34298
|
+
if (hasPath && hasContent) throw new Error("Attachment must have either path or content, not both");
|
|
34299
|
+
if (hasPath) return planPath(input2, env, dataDir);
|
|
34300
|
+
if (hasContent) return planContent(input2);
|
|
34301
|
+
throw new Error("Attachment needs a path or content");
|
|
34302
|
+
}
|
|
34303
|
+
function resolveAttachments(inputs, env = process.env, dataDir = path4.join(os4.homedir(), ".email-mcp")) {
|
|
34304
|
+
if (inputs === void 0) return void 0;
|
|
34305
|
+
const planned = inputs.map((input2) => plan(input2, env, dataDir));
|
|
34306
|
+
assertAttachmentTotal(planned.reduce((sum, p) => sum + p.bytes, 0));
|
|
34307
|
+
return planned.map((p) => p.load());
|
|
34308
|
+
}
|
|
34309
|
+
function assertAttachmentTotal(totalBytes) {
|
|
34310
|
+
if (totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
|
|
34311
|
+
throw new Error(`Attachments total ${megabytes2(totalBytes)}, over the ${megabytes2(MAX_TOTAL_ATTACHMENT_BYTES)} limit`);
|
|
34312
|
+
}
|
|
34313
|
+
}
|
|
34314
|
+
|
|
33890
34315
|
// src/tools/sending.ts
|
|
33891
34316
|
var ContactSchema = external_exports3.object({
|
|
33892
34317
|
email: external_exports3.string(),
|
|
@@ -33899,6 +34324,31 @@ var BodySchema = external_exports3.object({
|
|
|
33899
34324
|
function jsonResult3(data) {
|
|
33900
34325
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
33901
34326
|
}
|
|
34327
|
+
function messageIds(headerValue2) {
|
|
34328
|
+
return headerValue2?.match(/<[^<>\s]+>/g) ?? [];
|
|
34329
|
+
}
|
|
34330
|
+
function replyThreading(original) {
|
|
34331
|
+
const [messageId] = messageIds(original.headers?.["message-id"]);
|
|
34332
|
+
if (!messageId) return { threadId: original.threadId, replyToGraphId: original.id };
|
|
34333
|
+
const earlier = messageIds(original.headers?.["references"]).filter((id) => id !== messageId);
|
|
34334
|
+
return {
|
|
34335
|
+
inReplyTo: messageId,
|
|
34336
|
+
references: [...earlier, messageId],
|
|
34337
|
+
threadId: original.threadId,
|
|
34338
|
+
replyToGraphId: original.id
|
|
34339
|
+
};
|
|
34340
|
+
}
|
|
34341
|
+
var sameAddress = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
34342
|
+
function withoutDuplicates(to, cc) {
|
|
34343
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34344
|
+
const firstMention = (c) => {
|
|
34345
|
+
const key = c.email.toLowerCase();
|
|
34346
|
+
if (seen.has(key)) return false;
|
|
34347
|
+
seen.add(key);
|
|
34348
|
+
return true;
|
|
34349
|
+
};
|
|
34350
|
+
return { to: to.filter(firstMention), cc: cc?.filter(firstMention) };
|
|
34351
|
+
}
|
|
33902
34352
|
function registerSendingTools(server, accountManager) {
|
|
33903
34353
|
server.tool(
|
|
33904
34354
|
"email_send",
|
|
@@ -33909,7 +34359,8 @@ function registerSendingTools(server, accountManager) {
|
|
|
33909
34359
|
cc: external_exports3.array(ContactSchema).optional(),
|
|
33910
34360
|
bcc: external_exports3.array(ContactSchema).optional(),
|
|
33911
34361
|
subject: external_exports3.string(),
|
|
33912
|
-
body: BodySchema
|
|
34362
|
+
body: BodySchema,
|
|
34363
|
+
attachments: AttachmentsSchema
|
|
33913
34364
|
},
|
|
33914
34365
|
async (args) => {
|
|
33915
34366
|
try {
|
|
@@ -33919,7 +34370,8 @@ function registerSendingTools(server, accountManager) {
|
|
|
33919
34370
|
cc: args.cc,
|
|
33920
34371
|
bcc: args.bcc,
|
|
33921
34372
|
subject: args.subject,
|
|
33922
|
-
body: args.body
|
|
34373
|
+
body: args.body,
|
|
34374
|
+
attachments: resolveAttachments(args.attachments)
|
|
33923
34375
|
};
|
|
33924
34376
|
const result = await provider.sendEmail(params);
|
|
33925
34377
|
return jsonResult3(result);
|
|
@@ -33930,32 +34382,53 @@ function registerSendingTools(server, accountManager) {
|
|
|
33930
34382
|
);
|
|
33931
34383
|
server.tool(
|
|
33932
34384
|
"email_reply",
|
|
33933
|
-
"Reply to an existing email",
|
|
34385
|
+
"Reply to an existing email, in its thread. With replyAll the reply goes to the original sender and all To and Cc recipients, except the account's own address",
|
|
33934
34386
|
{
|
|
33935
34387
|
accountId: external_exports3.string(),
|
|
33936
34388
|
emailId: external_exports3.string(),
|
|
34389
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)"),
|
|
33937
34390
|
body: BodySchema,
|
|
33938
|
-
replyAll: external_exports3.boolean().optional()
|
|
34391
|
+
replyAll: external_exports3.boolean().optional(),
|
|
34392
|
+
to: external_exports3.array(ContactSchema).optional().describe("Replace the default To (the original sender). Use it when the original was sent by you, so the reply goes to the real correspondents instead of back to yourself"),
|
|
34393
|
+
cc: external_exports3.array(ContactSchema).optional().describe("Cc recipients. Replaces any Cc carried over by replyAll"),
|
|
34394
|
+
bcc: external_exports3.array(ContactSchema).optional(),
|
|
34395
|
+
additionalRecipients: external_exports3.array(ContactSchema).optional().describe("Extra To recipients added to the default or given To (deduplicated by address)"),
|
|
34396
|
+
attachments: AttachmentsSchema
|
|
33939
34397
|
},
|
|
33940
34398
|
async (args) => {
|
|
33941
34399
|
try {
|
|
33942
34400
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33943
|
-
const original = await provider.getEmail(args.emailId);
|
|
33944
|
-
const messageId = original.headers?.["message-id"] ?? original.id;
|
|
34401
|
+
const original = await provider.getEmail(args.emailId, args.sourceFolder);
|
|
33945
34402
|
const subject = original.subject.startsWith("Re: ") ? original.subject : `Re: ${original.subject}`;
|
|
33946
34403
|
let to = [original.from];
|
|
33947
34404
|
let cc;
|
|
33948
34405
|
if (args.replyAll) {
|
|
33949
|
-
|
|
33950
|
-
|
|
34406
|
+
const own = await accountManager.getAccountEmail(args.accountId);
|
|
34407
|
+
const others = (c) => !own || !sameAddress(c.email, own);
|
|
34408
|
+
to = [original.from, ...original.to].filter(others);
|
|
34409
|
+
if (to.length === 0) to = [original.from];
|
|
34410
|
+
cc = original.cc?.filter(others);
|
|
34411
|
+
}
|
|
34412
|
+
if (args.to?.length) to = args.to;
|
|
34413
|
+
if (args.additionalRecipients?.length) {
|
|
34414
|
+
const seen = new Set(to.map((c) => c.email.toLowerCase()));
|
|
34415
|
+
for (const r of args.additionalRecipients) {
|
|
34416
|
+
if (!seen.has(r.email.toLowerCase())) {
|
|
34417
|
+
to = [...to, r];
|
|
34418
|
+
seen.add(r.email.toLowerCase());
|
|
34419
|
+
}
|
|
34420
|
+
}
|
|
33951
34421
|
}
|
|
34422
|
+
if (args.cc !== void 0) cc = args.cc;
|
|
34423
|
+
if (args.replyAll) ({ to, cc } = withoutDuplicates(to, cc));
|
|
33952
34424
|
const params = {
|
|
33953
34425
|
to,
|
|
33954
34426
|
cc,
|
|
34427
|
+
bcc: args.bcc,
|
|
33955
34428
|
subject,
|
|
33956
34429
|
body: args.body,
|
|
33957
|
-
|
|
33958
|
-
|
|
34430
|
+
...replyThreading(original),
|
|
34431
|
+
attachments: resolveAttachments(args.attachments)
|
|
33959
34432
|
};
|
|
33960
34433
|
const result = await provider.sendEmail(params);
|
|
33961
34434
|
return jsonResult3(result);
|
|
@@ -33966,17 +34439,22 @@ function registerSendingTools(server, accountManager) {
|
|
|
33966
34439
|
);
|
|
33967
34440
|
server.tool(
|
|
33968
34441
|
"email_forward",
|
|
33969
|
-
"Forward an email to new recipients",
|
|
34442
|
+
"Forward an email to new recipients. The original message's attachments are left out unless includeOriginalAttachments is true",
|
|
33970
34443
|
{
|
|
33971
34444
|
accountId: external_exports3.string(),
|
|
33972
34445
|
emailId: external_exports3.string(),
|
|
33973
34446
|
to: external_exports3.array(ContactSchema),
|
|
33974
|
-
|
|
34447
|
+
cc: external_exports3.array(ContactSchema).optional(),
|
|
34448
|
+
bcc: external_exports3.array(ContactSchema).optional(),
|
|
34449
|
+
body: BodySchema.optional(),
|
|
34450
|
+
attachments: AttachmentsSchema.describe("Files to attach to the forward. Total size, together with forwarded original attachments, is capped at 25 MB"),
|
|
34451
|
+
includeOriginalAttachments: external_exports3.boolean().optional().describe("Also forward the original message's attachments (default false). Leave it off unless the user asked for the files to go along"),
|
|
34452
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
|
|
33975
34453
|
},
|
|
33976
34454
|
async (args) => {
|
|
33977
34455
|
try {
|
|
33978
34456
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33979
|
-
const original = await provider.getEmail(args.emailId);
|
|
34457
|
+
const original = await provider.getEmail(args.emailId, args.sourceFolder);
|
|
33980
34458
|
const subject = original.subject.startsWith("Fwd: ") ? original.subject : `Fwd: ${original.subject}`;
|
|
33981
34459
|
const forwardHeader = [
|
|
33982
34460
|
"---------- Forwarded message ----------",
|
|
@@ -34010,10 +34488,27 @@ function registerSendingTools(server, accountManager) {
|
|
|
34010
34488
|
originalHtml
|
|
34011
34489
|
].join("");
|
|
34012
34490
|
}
|
|
34491
|
+
const extra = resolveAttachments(args.attachments) ?? [];
|
|
34492
|
+
const forwarded = [];
|
|
34493
|
+
if (args.includeOriginalAttachments && original.attachments?.length) {
|
|
34494
|
+
const extraBytes = extra.reduce((sum, a) => sum + a.content.length, 0);
|
|
34495
|
+
assertAttachmentTotal(extraBytes + original.attachments.reduce((sum, a) => sum + (a.size || 0), 0));
|
|
34496
|
+
let bytes = extraBytes;
|
|
34497
|
+
for (const att of original.attachments) {
|
|
34498
|
+
const fetched = await provider.getAttachment(original.id, att.id, args.sourceFolder);
|
|
34499
|
+
bytes += fetched.data.length;
|
|
34500
|
+
assertAttachmentTotal(bytes);
|
|
34501
|
+
forwarded.push({ filename: att.filename, content: fetched.data, contentType: att.contentType });
|
|
34502
|
+
}
|
|
34503
|
+
}
|
|
34504
|
+
const attachments = [...forwarded, ...extra];
|
|
34013
34505
|
const params = {
|
|
34014
34506
|
to: args.to,
|
|
34507
|
+
cc: args.cc,
|
|
34508
|
+
bcc: args.bcc,
|
|
34015
34509
|
subject,
|
|
34016
|
-
body: { text: forwardedText, html: forwardedHtml }
|
|
34510
|
+
body: { text: forwardedText, html: forwardedHtml },
|
|
34511
|
+
attachments: attachments.length ? attachments : void 0
|
|
34017
34512
|
};
|
|
34018
34513
|
const result = await provider.sendEmail(params);
|
|
34019
34514
|
return jsonResult3(result);
|
|
@@ -34024,12 +34519,15 @@ function registerSendingTools(server, accountManager) {
|
|
|
34024
34519
|
);
|
|
34025
34520
|
server.tool(
|
|
34026
34521
|
"email_draft_create",
|
|
34027
|
-
"Create a new email draft",
|
|
34522
|
+
"Create a new email draft. Pass inReplyToEmailId to save it as a reply in that message's thread",
|
|
34028
34523
|
{
|
|
34029
34524
|
accountId: external_exports3.string(),
|
|
34030
34525
|
to: external_exports3.array(ContactSchema),
|
|
34031
34526
|
subject: external_exports3.string(),
|
|
34032
|
-
body: BodySchema
|
|
34527
|
+
body: BodySchema,
|
|
34528
|
+
attachments: AttachmentsSchema,
|
|
34529
|
+
inReplyToEmailId: external_exports3.string().optional().describe("Id of the email this draft replies to. The draft joins that thread (Gmail threadId, Outlook createReply, In-Reply-To/References headers elsewhere)"),
|
|
34530
|
+
inReplyToSourceFolder: external_exports3.string().optional().describe("Folder of the email named in inReplyToEmailId (required for IMAP/iCloud when that email is not in INBOX). The draft itself is saved to Drafts")
|
|
34033
34531
|
},
|
|
34034
34532
|
async (args) => {
|
|
34035
34533
|
try {
|
|
@@ -34037,8 +34535,12 @@ function registerSendingTools(server, accountManager) {
|
|
|
34037
34535
|
const params = {
|
|
34038
34536
|
to: args.to,
|
|
34039
34537
|
subject: args.subject,
|
|
34040
|
-
body: args.body
|
|
34538
|
+
body: args.body,
|
|
34539
|
+
attachments: resolveAttachments(args.attachments)
|
|
34041
34540
|
};
|
|
34541
|
+
if (args.inReplyToEmailId) {
|
|
34542
|
+
Object.assign(params, replyThreading(await provider.getEmail(args.inReplyToEmailId, args.inReplyToSourceFolder)));
|
|
34543
|
+
}
|
|
34042
34544
|
const result = await provider.createDraft(params);
|
|
34043
34545
|
return jsonResult3(result);
|
|
34044
34546
|
} catch (error48) {
|
|
@@ -34048,13 +34550,16 @@ function registerSendingTools(server, accountManager) {
|
|
|
34048
34550
|
);
|
|
34049
34551
|
server.tool(
|
|
34050
34552
|
"email_draft_update",
|
|
34051
|
-
"Update an existing email draft in place (to/subject/body). On Gmail and Outlook the draft id is unchanged. On iCloud/generic IMAP there is no in-place update
|
|
34553
|
+
"Update an existing email draft in place (to/subject/body). A reply draft stays in its thread. On Gmail and Outlook the draft id is unchanged. On iCloud/generic IMAP there is no in-place update (the old draft is deleted and a new one appended), so the returned id is a NEW id and the old one no longer resolves; always use the id this call returns, not the one you passed in.",
|
|
34052
34554
|
{
|
|
34053
34555
|
accountId: external_exports3.string(),
|
|
34054
34556
|
draftId: external_exports3.string(),
|
|
34055
34557
|
to: external_exports3.array(ContactSchema),
|
|
34056
34558
|
subject: external_exports3.string(),
|
|
34057
34559
|
body: BodySchema,
|
|
34560
|
+
attachments: AttachmentsSchema.describe(
|
|
34561
|
+
"Files to attach. When given, they replace the draft's current attachments; an empty list removes them all. When omitted, the draft keeps the files it has."
|
|
34562
|
+
),
|
|
34058
34563
|
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when the draft is not in the default Drafts folder)")
|
|
34059
34564
|
},
|
|
34060
34565
|
async (args) => {
|
|
@@ -34063,7 +34568,8 @@ function registerSendingTools(server, accountManager) {
|
|
|
34063
34568
|
const params = {
|
|
34064
34569
|
to: args.to,
|
|
34065
34570
|
subject: args.subject,
|
|
34066
|
-
body: args.body
|
|
34571
|
+
body: args.body,
|
|
34572
|
+
attachments: resolveAttachments(args.attachments)
|
|
34067
34573
|
};
|
|
34068
34574
|
const result = await provider.updateDraft(args.draftId, params, args.sourceFolder);
|
|
34069
34575
|
return jsonResult3(result);
|
|
@@ -34605,7 +35111,7 @@ var FORWARD_ALLOWLIST_ENV = "EMAIL_MCP_FORWARD_ALLOWLIST";
|
|
|
34605
35111
|
function forwardAllowlist(env = process.env) {
|
|
34606
35112
|
return (env[FORWARD_ALLOWLIST_ENV] || "").split(",").map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
|
|
34607
35113
|
}
|
|
34608
|
-
var
|
|
35114
|
+
var HOW_TO_ALLOW2 = `Forwarding targets are set by the user, not by the assistant: add the address to ${FORWARD_ALLOWLIST_ENV} (comma-separated) in the environment of the email-mcp server, in the MCP client configuration, then restart the client.`;
|
|
34609
35115
|
function withScopeHint(error48) {
|
|
34610
35116
|
const message = error48?.message || String(error48);
|
|
34611
35117
|
const status = error48?.code ?? error48?.statusCode ?? error48?.status ?? error48?.response?.status;
|
|
@@ -34643,14 +35149,14 @@ function registerForwardingTools(server, accountManager) {
|
|
|
34643
35149
|
if (allowed.length === 0) {
|
|
34644
35150
|
return jsonResult6({
|
|
34645
35151
|
success: false,
|
|
34646
|
-
error: `Forwarding is switched off: ${FORWARD_ALLOWLIST_ENV} is not set. ${
|
|
35152
|
+
error: `Forwarding is switched off: ${FORWARD_ALLOWLIST_ENV} is not set. ${HOW_TO_ALLOW2}`
|
|
34647
35153
|
});
|
|
34648
35154
|
}
|
|
34649
35155
|
const forwardTo = args.forwardTo.trim();
|
|
34650
35156
|
if (!allowed.includes(forwardTo.toLowerCase())) {
|
|
34651
35157
|
return jsonResult6({
|
|
34652
35158
|
success: false,
|
|
34653
|
-
error: `${forwardTo} is not an allowed forwarding target. ${
|
|
35159
|
+
error: `${forwardTo} is not an allowed forwarding target. ${HOW_TO_ALLOW2}`,
|
|
34654
35160
|
allowedTargets: allowed
|
|
34655
35161
|
});
|
|
34656
35162
|
}
|
|
@@ -34716,7 +35222,7 @@ function registerForwardingTools(server, accountManager) {
|
|
|
34716
35222
|
}
|
|
34717
35223
|
|
|
34718
35224
|
// src/server.ts
|
|
34719
|
-
var SERVER_VERSION = "1.
|
|
35225
|
+
var SERVER_VERSION = "1.10.1";
|
|
34720
35226
|
async function createServer(accountManager) {
|
|
34721
35227
|
const mgr = accountManager ?? new AccountManager();
|
|
34722
35228
|
const server = new McpServer({
|