@marlinjai/email-mcp 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -9
- package/dist/index.js +523 -83
- package/dist/index.js.map +4 -4
- package/dist/setup/wizard.js +228 -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,41 @@ 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 a reply draft would fall out
|
|
7390
|
+
* of its thread. When the caller names no thread, the thread and the reply
|
|
7391
|
+
* headers of the stored draft are carried over into the new message.
|
|
7392
|
+
*/
|
|
7393
|
+
async updateDraft(draftId, update) {
|
|
7380
7394
|
const gmail = this.ensureConnected();
|
|
7395
|
+
let params = update;
|
|
7396
|
+
if (update.threadId === void 0) {
|
|
7397
|
+
const existing = await gmail.users.drafts.get({
|
|
7398
|
+
userId: "me",
|
|
7399
|
+
id: draftId,
|
|
7400
|
+
// Ids, labels and headers, no body. drafts.get has no way to ask for
|
|
7401
|
+
// single headers (messages.get does), so all of them come back.
|
|
7402
|
+
format: "metadata"
|
|
7403
|
+
});
|
|
7404
|
+
const message = existing.data.message;
|
|
7405
|
+
const header = (name) => (message?.payload?.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || void 0;
|
|
7406
|
+
const inReplyTo = update.inReplyTo ?? header("in-reply-to");
|
|
7407
|
+
const references = update.references ?? header("references")?.split(/\s+/).filter(Boolean);
|
|
7408
|
+
if (message?.threadId && inReplyTo) {
|
|
7409
|
+
params = { ...update, threadId: message.threadId, inReplyTo, references: references?.length ? references : [inReplyTo] };
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7381
7412
|
const raw = await this.buildRaw(params);
|
|
7382
7413
|
const res = await gmail.users.drafts.update({
|
|
7383
7414
|
userId: "me",
|
|
7384
7415
|
id: draftId,
|
|
7385
7416
|
requestBody: {
|
|
7386
|
-
message: { raw }
|
|
7417
|
+
message: { raw, ...this.threadFor(params) }
|
|
7387
7418
|
}
|
|
7388
7419
|
});
|
|
7389
7420
|
return { id: res.data.id || draftId };
|
|
@@ -7762,6 +7793,16 @@ var init_adapter = __esm({
|
|
|
7762
7793
|
}
|
|
7763
7794
|
await this.deleteBlockRule(ruleId);
|
|
7764
7795
|
}
|
|
7796
|
+
/**
|
|
7797
|
+
* Gmail files a message into an existing thread only when the threadId
|
|
7798
|
+
* comes with In-Reply-To and References headers and a subject (which has to
|
|
7799
|
+
* match the thread's; the reply tool builds "Re: <original subject>"). A
|
|
7800
|
+
* threadId without them is left out, and the message starts its own thread.
|
|
7801
|
+
*/
|
|
7802
|
+
threadFor(params) {
|
|
7803
|
+
const belongs = params.threadId && params.inReplyTo && params.references?.length && params.subject.trim();
|
|
7804
|
+
return belongs ? { threadId: params.threadId } : {};
|
|
7805
|
+
}
|
|
7765
7806
|
/**
|
|
7766
7807
|
* Builds the base64url `raw` payload Gmail expects for send, draft create and
|
|
7767
7808
|
* draft update: a full MIME message with a text part, an HTML part when one
|
|
@@ -8055,12 +8096,41 @@ var init_mapper2 = __esm({
|
|
|
8055
8096
|
|
|
8056
8097
|
// src/providers/outlook/adapter.ts
|
|
8057
8098
|
import { Client } from "@microsoft/microsoft-graph-client";
|
|
8058
|
-
|
|
8099
|
+
function megabytes(bytes) {
|
|
8100
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
8101
|
+
}
|
|
8102
|
+
function assertOutlookAttachmentSizes(attachments) {
|
|
8103
|
+
if (!attachments?.length) return;
|
|
8104
|
+
const limit = megabytes(OUTLOOK_MAX_ATTACHMENT_BYTES);
|
|
8105
|
+
for (const att of attachments) {
|
|
8106
|
+
if (att.content.length > OUTLOOK_MAX_ATTACHMENT_BYTES) {
|
|
8107
|
+
throw new Error(`Outlook accepts attachments up to ${limit} through email-mcp; ${att.filename} is ${megabytes(att.content.length)}. ${UPLOAD_SESSION_HINT}`);
|
|
8108
|
+
}
|
|
8109
|
+
}
|
|
8110
|
+
}
|
|
8111
|
+
function assertOutlookRequestSize(body) {
|
|
8112
|
+
const bytes = Buffer.byteLength(JSON.stringify(body));
|
|
8113
|
+
if (bytes > OUTLOOK_MAX_REQUEST_BYTES) {
|
|
8114
|
+
throw new Error(
|
|
8115
|
+
`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}`
|
|
8116
|
+
);
|
|
8117
|
+
}
|
|
8118
|
+
}
|
|
8119
|
+
var toGraphAttachment, OUTLOOK_MAX_ATTACHMENT_BYTES, OUTLOOK_MAX_REQUEST_BYTES, UPLOAD_SESSION_HINT, OutlookAdapter;
|
|
8059
8120
|
var init_adapter2 = __esm({
|
|
8060
8121
|
"src/providers/outlook/adapter.ts"() {
|
|
8061
8122
|
"use strict";
|
|
8062
8123
|
init_types();
|
|
8063
8124
|
init_mapper2();
|
|
8125
|
+
toGraphAttachment = (att) => ({
|
|
8126
|
+
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
8127
|
+
name: att.filename,
|
|
8128
|
+
contentType: att.contentType,
|
|
8129
|
+
contentBytes: att.content.toString("base64")
|
|
8130
|
+
});
|
|
8131
|
+
OUTLOOK_MAX_ATTACHMENT_BYTES = 3 * 1024 * 1024;
|
|
8132
|
+
OUTLOOK_MAX_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
8133
|
+
UPLOAD_SESSION_HINT = "Larger files need an upload session, which email-mcp does not implement for Outlook yet.";
|
|
8064
8134
|
OutlookAdapter = class _OutlookAdapter {
|
|
8065
8135
|
providerType = ProviderType.Outlook;
|
|
8066
8136
|
client = null;
|
|
@@ -8079,8 +8149,8 @@ var init_adapter2 = __esm({
|
|
|
8079
8149
|
}
|
|
8080
8150
|
});
|
|
8081
8151
|
const originalApi = client.api.bind(client);
|
|
8082
|
-
client.api = (
|
|
8083
|
-
return originalApi(
|
|
8152
|
+
client.api = (path5) => {
|
|
8153
|
+
return originalApi(path5).header("Prefer", 'IdType="ImmutableId"');
|
|
8084
8154
|
};
|
|
8085
8155
|
this.client = client;
|
|
8086
8156
|
}
|
|
@@ -8200,31 +8270,125 @@ var init_adapter2 = __esm({
|
|
|
8200
8270
|
meta: mapGraphAttachment(attachment)
|
|
8201
8271
|
};
|
|
8202
8272
|
}
|
|
8273
|
+
/**
|
|
8274
|
+
* Graph only threads a reply (conversationId, subject, quoted history)
|
|
8275
|
+
* through the createReply flow: POST /me/messages and /me/sendMail always
|
|
8276
|
+
* start a new conversation whatever In-Reply-To says. Build the reply draft,
|
|
8277
|
+
* overlay our subject, body, recipients and attachments, and return its id.
|
|
8278
|
+
* Note the body replaces Graph's pre-filled quoted history.
|
|
8279
|
+
*
|
|
8280
|
+
* Each attachment goes up in a request of its own, so the sizes are checked
|
|
8281
|
+
* per file, and before createReply so that a file that is too big leaves no
|
|
8282
|
+
* draft behind. A draft that cannot be filled is deleted again. (A draft
|
|
8283
|
+
* whose send failed is kept, see `sendEmail`.)
|
|
8284
|
+
*/
|
|
8285
|
+
async createReplyDraft(params, replyToId) {
|
|
8286
|
+
const client = this.ensureClient();
|
|
8287
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
8288
|
+
const draft = await client.api(`/me/messages/${encodeURIComponent(replyToId)}/createReply`).post({});
|
|
8289
|
+
try {
|
|
8290
|
+
await this.fillReplyDraft(draft.id, params);
|
|
8291
|
+
} catch (error48) {
|
|
8292
|
+
throw await this.discardDraft(draft.id, error48);
|
|
8293
|
+
}
|
|
8294
|
+
return draft.id;
|
|
8295
|
+
}
|
|
8296
|
+
async fillReplyDraft(draftId, params) {
|
|
8297
|
+
const client = this.ensureClient();
|
|
8298
|
+
const toGraphRecipient = (c) => ({
|
|
8299
|
+
emailAddress: { name: c.name, address: c.email }
|
|
8300
|
+
});
|
|
8301
|
+
const patch = {
|
|
8302
|
+
// Always the caller's subject: Graph pre-fills its own "RE: ..." one.
|
|
8303
|
+
subject: params.subject,
|
|
8304
|
+
body: { contentType: params.body.html ? "html" : "text", content: params.body.html || params.body.text || "" }
|
|
8305
|
+
};
|
|
8306
|
+
if (params.to?.length) patch.toRecipients = params.to.map(toGraphRecipient);
|
|
8307
|
+
if (params.cc !== void 0) patch.ccRecipients = params.cc.map(toGraphRecipient);
|
|
8308
|
+
if (params.bcc !== void 0) patch.bccRecipients = params.bcc.map(toGraphRecipient);
|
|
8309
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(patch);
|
|
8310
|
+
for (const att of params.attachments ?? []) {
|
|
8311
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post(toGraphAttachment(att));
|
|
8312
|
+
}
|
|
8313
|
+
}
|
|
8314
|
+
/**
|
|
8315
|
+
* Deletes a reply draft that could not be filled, so a failed reply leaves
|
|
8316
|
+
* nothing half-built in Drafts. Returns the error to throw: the original
|
|
8317
|
+
* one, or, when the delete fails as well, the original message with a note
|
|
8318
|
+
* that the draft is still there.
|
|
8319
|
+
*/
|
|
8320
|
+
async discardDraft(draftId, cause) {
|
|
8321
|
+
try {
|
|
8322
|
+
await this.ensureClient().api(`/me/messages/${encodeURIComponent(draftId)}`).delete();
|
|
8323
|
+
return cause;
|
|
8324
|
+
} catch (deleteError) {
|
|
8325
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
8326
|
+
return new Error(
|
|
8327
|
+
`${message} (the reply draft ${draftId} could not be removed and was left in Drafts: ${deleteError?.message ?? deleteError})`,
|
|
8328
|
+
{ cause }
|
|
8329
|
+
);
|
|
8330
|
+
}
|
|
8331
|
+
}
|
|
8203
8332
|
async sendEmail(params) {
|
|
8204
8333
|
const client = this.ensureClient();
|
|
8334
|
+
if (params.replyToGraphId) {
|
|
8335
|
+
const draftId = await this.createReplyDraft(params, params.replyToGraphId);
|
|
8336
|
+
try {
|
|
8337
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}/send`).post({});
|
|
8338
|
+
} catch (error48) {
|
|
8339
|
+
throw new Error(
|
|
8340
|
+
`${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)`,
|
|
8341
|
+
{ cause: error48 }
|
|
8342
|
+
);
|
|
8343
|
+
}
|
|
8344
|
+
return { id: draftId };
|
|
8345
|
+
}
|
|
8346
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
8205
8347
|
const message = this.buildGraphMessage(params);
|
|
8206
8348
|
const payload = { message };
|
|
8207
8349
|
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
|
-
}));
|
|
8350
|
+
payload.message.attachments = params.attachments.map(toGraphAttachment);
|
|
8214
8351
|
}
|
|
8352
|
+
assertOutlookRequestSize(payload);
|
|
8215
8353
|
await client.api("/me/sendMail").post(payload);
|
|
8216
8354
|
return { id: `sent-${Date.now()}` };
|
|
8217
8355
|
}
|
|
8218
8356
|
async createDraft(params) {
|
|
8219
8357
|
const client = this.ensureClient();
|
|
8358
|
+
if (params.replyToGraphId) {
|
|
8359
|
+
return { id: await this.createReplyDraft(params, params.replyToGraphId) };
|
|
8360
|
+
}
|
|
8361
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
8220
8362
|
const message = this.buildGraphMessage(params);
|
|
8363
|
+
if (params.attachments?.length) {
|
|
8364
|
+
message.attachments = params.attachments.map(toGraphAttachment);
|
|
8365
|
+
}
|
|
8366
|
+
assertOutlookRequestSize(message);
|
|
8221
8367
|
const result = await client.api("/me/messages").post(message);
|
|
8222
8368
|
return { id: result.id };
|
|
8223
8369
|
}
|
|
8224
8370
|
async updateDraft(draftId, params) {
|
|
8225
8371
|
const client = this.ensureClient();
|
|
8372
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
8226
8373
|
const message = this.buildGraphMessage(params);
|
|
8227
8374
|
await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(message);
|
|
8375
|
+
if (params.attachments) {
|
|
8376
|
+
const attachmentsPath = `/me/messages/${encodeURIComponent(draftId)}/attachments`;
|
|
8377
|
+
const previousIds = [];
|
|
8378
|
+
let page = await client.api(attachmentsPath).select("id").get();
|
|
8379
|
+
for (; ; ) {
|
|
8380
|
+
previousIds.push(...(page.value || []).map((att) => att.id));
|
|
8381
|
+
const next = page["@odata.nextLink"];
|
|
8382
|
+
if (!next) break;
|
|
8383
|
+
page = await client.api(next).get();
|
|
8384
|
+
}
|
|
8385
|
+
for (const att of params.attachments) {
|
|
8386
|
+
await client.api(attachmentsPath).post(toGraphAttachment(att));
|
|
8387
|
+
}
|
|
8388
|
+
for (const id of previousIds) {
|
|
8389
|
+
await client.api(`${attachmentsPath}/${encodeURIComponent(id)}`).delete();
|
|
8390
|
+
}
|
|
8391
|
+
}
|
|
8228
8392
|
return { id: draftId };
|
|
8229
8393
|
}
|
|
8230
8394
|
async listDrafts(limit, offset) {
|
|
@@ -8781,6 +8945,12 @@ function mapImapFolder(imapFolder) {
|
|
|
8781
8945
|
unreadCount: imapFolder.status?.unseen
|
|
8782
8946
|
};
|
|
8783
8947
|
}
|
|
8948
|
+
function imapAttachmentId(att, index) {
|
|
8949
|
+
return att.contentId || `att-${index}`;
|
|
8950
|
+
}
|
|
8951
|
+
function headerValue(line) {
|
|
8952
|
+
return line.slice(line.indexOf(":") + 1).replace(/\r?\n[ \t]+/g, " ").trim();
|
|
8953
|
+
}
|
|
8784
8954
|
function mapParsedEmail(parsed, folder, accountId, uid) {
|
|
8785
8955
|
const mapContact = (addr) => ({
|
|
8786
8956
|
name: addr.name || void 0,
|
|
@@ -8791,7 +8961,7 @@ function mapParsedEmail(parsed, folder, accountId, uid) {
|
|
|
8791
8961
|
return addrs.value.map(mapContact);
|
|
8792
8962
|
};
|
|
8793
8963
|
const attachments = (parsed.attachments || []).map((att, i) => ({
|
|
8794
|
-
id: att
|
|
8964
|
+
id: imapAttachmentId(att, i),
|
|
8795
8965
|
filename: att.filename || `attachment-${i}`,
|
|
8796
8966
|
contentType: att.contentType || "application/octet-stream",
|
|
8797
8967
|
size: att.size || 0
|
|
@@ -8820,7 +8990,7 @@ function mapParsedEmail(parsed, folder, accountId, uid) {
|
|
|
8820
8990
|
flagged: parsed.flags?.has("\\Flagged") || false,
|
|
8821
8991
|
draft: parsed.flags?.has("\\Draft") || false
|
|
8822
8992
|
},
|
|
8823
|
-
headers: parsed.headerLines ? Object.fromEntries(parsed.headerLines.map((h) => [h.key, h.line])) : void 0
|
|
8993
|
+
headers: parsed.headerLines ? Object.fromEntries(parsed.headerLines.map((h) => [h.key, headerValue(h.line)])) : void 0
|
|
8824
8994
|
};
|
|
8825
8995
|
}
|
|
8826
8996
|
var SPECIAL_USE_MAP;
|
|
@@ -9248,7 +9418,7 @@ var init_adapter3 = __esm({
|
|
|
9248
9418
|
}
|
|
9249
9419
|
async getEmail(id, folder) {
|
|
9250
9420
|
const client = await this.ensureConnected();
|
|
9251
|
-
const targetFolder = folder
|
|
9421
|
+
const targetFolder = folder ? await this.resolveFolder(folder) : "INBOX";
|
|
9252
9422
|
let lock;
|
|
9253
9423
|
try {
|
|
9254
9424
|
lock = await client.getMailboxLock(targetFolder);
|
|
@@ -9267,13 +9437,20 @@ var init_adapter3 = __esm({
|
|
|
9267
9437
|
lock.release();
|
|
9268
9438
|
}
|
|
9269
9439
|
}
|
|
9270
|
-
|
|
9440
|
+
/**
|
|
9441
|
+
* IMAP has no thread object: the thread is the set of messages in ONE
|
|
9442
|
+
* folder whose Message-ID, References or In-Reply-To name `threadId`.
|
|
9443
|
+
* Messages of the same conversation in other folders (the own replies in
|
|
9444
|
+
* Sent, for example) are not part of the result.
|
|
9445
|
+
*/
|
|
9446
|
+
async getThread(threadId, sourceFolder) {
|
|
9271
9447
|
const client = await this.ensureConnected();
|
|
9448
|
+
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
9272
9449
|
let lock;
|
|
9273
9450
|
try {
|
|
9274
|
-
lock = await client.getMailboxLock(
|
|
9451
|
+
lock = await client.getMailboxLock(folder);
|
|
9275
9452
|
} catch (error48) {
|
|
9276
|
-
throw formatImapError(error48,
|
|
9453
|
+
throw formatImapError(error48, `Failed to open folder "${folder}"`);
|
|
9277
9454
|
}
|
|
9278
9455
|
try {
|
|
9279
9456
|
const searchResult = await client.search(
|
|
@@ -9281,13 +9458,13 @@ var init_adapter3 = __esm({
|
|
|
9281
9458
|
{ uid: true }
|
|
9282
9459
|
);
|
|
9283
9460
|
const uids = Array.isArray(searchResult) ? searchResult : [];
|
|
9284
|
-
if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
|
|
9461
|
+
if (uids.length === 0) throw new Error(`Thread ${threadId} not found in "${folder}"`);
|
|
9285
9462
|
const messages = [];
|
|
9286
9463
|
for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
|
|
9287
9464
|
if (!msg.source) continue;
|
|
9288
9465
|
const parsed = await simpleParser(msg.source);
|
|
9289
9466
|
parsed.flags = msg.flags;
|
|
9290
|
-
messages.push(mapParsedEmail(parsed,
|
|
9467
|
+
messages.push(mapParsedEmail(parsed, folder, this.accountId, msg.uid));
|
|
9291
9468
|
}
|
|
9292
9469
|
const participantMap = /* @__PURE__ */ new Map();
|
|
9293
9470
|
for (const msg of messages) {
|
|
@@ -9309,27 +9486,29 @@ var init_adapter3 = __esm({
|
|
|
9309
9486
|
lock.release();
|
|
9310
9487
|
}
|
|
9311
9488
|
}
|
|
9312
|
-
async getAttachment(emailId, attachmentId) {
|
|
9489
|
+
async getAttachment(emailId, attachmentId, sourceFolder) {
|
|
9313
9490
|
const client = await this.ensureConnected();
|
|
9491
|
+
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
9314
9492
|
let lock;
|
|
9315
9493
|
try {
|
|
9316
|
-
lock = await client.getMailboxLock(
|
|
9494
|
+
lock = await client.getMailboxLock(folder);
|
|
9317
9495
|
} catch (error48) {
|
|
9318
|
-
throw formatImapError(error48,
|
|
9496
|
+
throw formatImapError(error48, `Failed to open folder "${folder}"`);
|
|
9319
9497
|
}
|
|
9320
9498
|
try {
|
|
9321
9499
|
const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
|
|
9322
|
-
if (!msg) throw new Error(`Email ${emailId} not found`);
|
|
9500
|
+
if (!msg) throw new Error(`Email ${emailId} not found in "${folder}"`);
|
|
9323
9501
|
if (!msg.source) throw new Error(`Email ${emailId} has no source`);
|
|
9324
9502
|
const parsed = await simpleParser(msg.source);
|
|
9325
|
-
const
|
|
9326
|
-
|
|
9327
|
-
);
|
|
9328
|
-
if (
|
|
9503
|
+
const attachments = parsed.attachments || [];
|
|
9504
|
+
let index = attachments.findIndex((att, i) => imapAttachmentId(att, i) === attachmentId);
|
|
9505
|
+
if (index === -1) index = attachments.findIndex((att) => att.filename === attachmentId);
|
|
9506
|
+
if (index === -1) throw new Error(`Attachment ${attachmentId} not found in email ${emailId}`);
|
|
9507
|
+
const attachment = attachments[index];
|
|
9329
9508
|
return {
|
|
9330
9509
|
data: attachment.content,
|
|
9331
9510
|
meta: {
|
|
9332
|
-
id: attachment
|
|
9511
|
+
id: imapAttachmentId(attachment, index),
|
|
9333
9512
|
filename: attachment.filename || "attachment",
|
|
9334
9513
|
contentType: attachment.contentType || "application/octet-stream",
|
|
9335
9514
|
size: attachment.size || 0
|
|
@@ -9366,18 +9545,34 @@ var init_adapter3 = __esm({
|
|
|
9366
9545
|
* Drafts folder, and IMAP messages are immutable once appended. This
|
|
9367
9546
|
* deletes the old revision and appends a new one, so the returned `id`
|
|
9368
9547
|
* is a NEW uid; the old id no longer resolves after this call.
|
|
9548
|
+
*
|
|
9549
|
+
* A reply draft keeps its place in the thread: when the caller passes no
|
|
9550
|
+
* `inReplyTo`, the In-Reply-To and References headers of the old revision
|
|
9551
|
+
* are copied into the new one.
|
|
9369
9552
|
*/
|
|
9370
9553
|
async updateDraft(draftId, params, sourceFolder) {
|
|
9371
9554
|
const client = await this.ensureConnected();
|
|
9372
9555
|
const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
|
|
9373
|
-
const rawMessage = await this.buildDraftMessage(params);
|
|
9374
9556
|
let lock;
|
|
9375
9557
|
try {
|
|
9376
9558
|
lock = await client.getMailboxLock(draftsFolder);
|
|
9377
9559
|
} catch (error48) {
|
|
9378
9560
|
throw formatImapError(error48, `Failed to open folder "${draftsFolder}"`);
|
|
9379
9561
|
}
|
|
9562
|
+
let rawMessage;
|
|
9380
9563
|
try {
|
|
9564
|
+
let next = params;
|
|
9565
|
+
if (params.inReplyTo === void 0) {
|
|
9566
|
+
const old = await client.fetchOne(String(draftId), { source: true, uid: true }, { uid: true });
|
|
9567
|
+
if (old && old.source) {
|
|
9568
|
+
const parsed = await simpleParser(old.source);
|
|
9569
|
+
const references = [].concat(parsed.references ?? []);
|
|
9570
|
+
if (parsed.inReplyTo) {
|
|
9571
|
+
next = { ...params, inReplyTo: parsed.inReplyTo, references: params.references ?? references };
|
|
9572
|
+
}
|
|
9573
|
+
}
|
|
9574
|
+
}
|
|
9575
|
+
rawMessage = await this.buildDraftMessage(next);
|
|
9381
9576
|
await client.messageDelete(draftId, { uid: true });
|
|
9382
9577
|
} finally {
|
|
9383
9578
|
lock.release();
|
|
@@ -9693,6 +9888,11 @@ var init_account_manager = __esm({
|
|
|
9693
9888
|
connected: this.providers.has(c.id)
|
|
9694
9889
|
}));
|
|
9695
9890
|
}
|
|
9891
|
+
/** The account's own address, or undefined for an unknown account id. */
|
|
9892
|
+
async getAccountEmail(accountId) {
|
|
9893
|
+
const creds = this.credentials.get(accountId) ?? await this.store.get(accountId);
|
|
9894
|
+
return creds?.email;
|
|
9895
|
+
}
|
|
9696
9896
|
async getProvider(accountId) {
|
|
9697
9897
|
const existing = this.providers.get(accountId);
|
|
9698
9898
|
if (existing) {
|
|
@@ -10807,8 +11007,8 @@ function getErrorMap() {
|
|
|
10807
11007
|
|
|
10808
11008
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
10809
11009
|
var makeIssue = (params) => {
|
|
10810
|
-
const { data, path:
|
|
10811
|
-
const fullPath = [...
|
|
11010
|
+
const { data, path: path5, errorMaps, issueData } = params;
|
|
11011
|
+
const fullPath = [...path5, ...issueData.path || []];
|
|
10812
11012
|
const fullIssue = {
|
|
10813
11013
|
...issueData,
|
|
10814
11014
|
path: fullPath
|
|
@@ -10923,11 +11123,11 @@ var errorUtil;
|
|
|
10923
11123
|
|
|
10924
11124
|
// node_modules/zod/v3/types.js
|
|
10925
11125
|
var ParseInputLazyPath = class {
|
|
10926
|
-
constructor(parent, value,
|
|
11126
|
+
constructor(parent, value, path5, key) {
|
|
10927
11127
|
this._cachedPath = [];
|
|
10928
11128
|
this.parent = parent;
|
|
10929
11129
|
this.data = value;
|
|
10930
|
-
this._path =
|
|
11130
|
+
this._path = path5;
|
|
10931
11131
|
this._key = key;
|
|
10932
11132
|
}
|
|
10933
11133
|
get path() {
|
|
@@ -14850,10 +15050,10 @@ function mergeDefs(...defs) {
|
|
|
14850
15050
|
function cloneDef(schema) {
|
|
14851
15051
|
return mergeDefs(schema._zod.def);
|
|
14852
15052
|
}
|
|
14853
|
-
function getElementAtPath(obj,
|
|
14854
|
-
if (!
|
|
15053
|
+
function getElementAtPath(obj, path5) {
|
|
15054
|
+
if (!path5)
|
|
14855
15055
|
return obj;
|
|
14856
|
-
return
|
|
15056
|
+
return path5.reduce((acc, key) => acc?.[key], obj);
|
|
14857
15057
|
}
|
|
14858
15058
|
function promiseAllObject(promisesObj) {
|
|
14859
15059
|
const keys = Object.keys(promisesObj);
|
|
@@ -15236,11 +15436,11 @@ function aborted(x, startIndex = 0) {
|
|
|
15236
15436
|
}
|
|
15237
15437
|
return false;
|
|
15238
15438
|
}
|
|
15239
|
-
function prefixIssues(
|
|
15439
|
+
function prefixIssues(path5, issues) {
|
|
15240
15440
|
return issues.map((iss) => {
|
|
15241
15441
|
var _a2;
|
|
15242
15442
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
15243
|
-
iss.path.unshift(
|
|
15443
|
+
iss.path.unshift(path5);
|
|
15244
15444
|
return iss;
|
|
15245
15445
|
});
|
|
15246
15446
|
}
|
|
@@ -15423,7 +15623,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
|
|
|
15423
15623
|
}
|
|
15424
15624
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
15425
15625
|
const result = { errors: [] };
|
|
15426
|
-
const processError = (error49,
|
|
15626
|
+
const processError = (error49, path5 = []) => {
|
|
15427
15627
|
var _a2, _b;
|
|
15428
15628
|
for (const issue2 of error49.issues) {
|
|
15429
15629
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -15433,7 +15633,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
15433
15633
|
} else if (issue2.code === "invalid_element") {
|
|
15434
15634
|
processError({ issues: issue2.issues }, issue2.path);
|
|
15435
15635
|
} else {
|
|
15436
|
-
const fullpath = [...
|
|
15636
|
+
const fullpath = [...path5, ...issue2.path];
|
|
15437
15637
|
if (fullpath.length === 0) {
|
|
15438
15638
|
result.errors.push(mapper(issue2));
|
|
15439
15639
|
continue;
|
|
@@ -15465,8 +15665,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
15465
15665
|
}
|
|
15466
15666
|
function toDotPath(_path) {
|
|
15467
15667
|
const segs = [];
|
|
15468
|
-
const
|
|
15469
|
-
for (const seg of
|
|
15668
|
+
const path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
15669
|
+
for (const seg of path5) {
|
|
15470
15670
|
if (typeof seg === "number")
|
|
15471
15671
|
segs.push(`[${seg}]`);
|
|
15472
15672
|
else if (typeof seg === "symbol")
|
|
@@ -27872,13 +28072,13 @@ function resolveRef(ref, ctx) {
|
|
|
27872
28072
|
if (!ref.startsWith("#")) {
|
|
27873
28073
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
27874
28074
|
}
|
|
27875
|
-
const
|
|
27876
|
-
if (
|
|
28075
|
+
const path5 = ref.slice(1).split("/").filter(Boolean);
|
|
28076
|
+
if (path5.length === 0) {
|
|
27877
28077
|
return ctx.rootSchema;
|
|
27878
28078
|
}
|
|
27879
28079
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
27880
|
-
if (
|
|
27881
|
-
const key =
|
|
28080
|
+
if (path5[0] === defsKey) {
|
|
28081
|
+
const key = path5[1];
|
|
27882
28082
|
if (!key || !ctx.defs[key]) {
|
|
27883
28083
|
throw new Error(`Reference not found: ${ref}`);
|
|
27884
28084
|
}
|
|
@@ -33807,12 +34007,13 @@ function registerReadingTools(server, accountManager) {
|
|
|
33807
34007
|
"Get a single email by ID",
|
|
33808
34008
|
{
|
|
33809
34009
|
accountId: external_exports3.string(),
|
|
33810
|
-
emailId: external_exports3.string()
|
|
34010
|
+
emailId: external_exports3.string(),
|
|
34011
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
|
|
33811
34012
|
},
|
|
33812
34013
|
async (args) => {
|
|
33813
34014
|
try {
|
|
33814
34015
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33815
|
-
const email3 = await provider.getEmail(args.emailId);
|
|
34016
|
+
const email3 = await provider.getEmail(args.emailId, args.sourceFolder);
|
|
33816
34017
|
return jsonResult2(email3);
|
|
33817
34018
|
} catch (error48) {
|
|
33818
34019
|
return jsonResult2({ error: error48.message });
|
|
@@ -33824,12 +34025,13 @@ function registerReadingTools(server, accountManager) {
|
|
|
33824
34025
|
"Get an email thread by thread ID",
|
|
33825
34026
|
{
|
|
33826
34027
|
accountId: external_exports3.string(),
|
|
33827
|
-
threadId: external_exports3.string()
|
|
34028
|
+
threadId: external_exports3.string(),
|
|
34029
|
+
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
34030
|
},
|
|
33829
34031
|
async (args) => {
|
|
33830
34032
|
try {
|
|
33831
34033
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33832
|
-
const thread = await provider.getThread(args.threadId);
|
|
34034
|
+
const thread = await provider.getThread(args.threadId, args.sourceFolder);
|
|
33833
34035
|
return jsonResult2(thread);
|
|
33834
34036
|
} catch (error48) {
|
|
33835
34037
|
return jsonResult2({ error: error48.message });
|
|
@@ -33842,12 +34044,13 @@ function registerReadingTools(server, accountManager) {
|
|
|
33842
34044
|
{
|
|
33843
34045
|
accountId: external_exports3.string(),
|
|
33844
34046
|
emailId: external_exports3.string(),
|
|
33845
|
-
attachmentId: external_exports3.string()
|
|
34047
|
+
attachmentId: external_exports3.string(),
|
|
34048
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
|
|
33846
34049
|
},
|
|
33847
34050
|
async (args) => {
|
|
33848
34051
|
try {
|
|
33849
34052
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33850
|
-
const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
|
|
34053
|
+
const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId, args.sourceFolder);
|
|
33851
34054
|
return jsonResult2({
|
|
33852
34055
|
data: Buffer.from(data).toString("base64"),
|
|
33853
34056
|
meta: meta3
|
|
@@ -33864,13 +34067,14 @@ function registerReadingTools(server, accountManager) {
|
|
|
33864
34067
|
accountId: external_exports3.string(),
|
|
33865
34068
|
emailId: external_exports3.string(),
|
|
33866
34069
|
attachmentId: external_exports3.string(),
|
|
33867
|
-
outputPath: external_exports3.string().describe("Relative file path (within the downloads directory) to save the attachment as")
|
|
34070
|
+
outputPath: external_exports3.string().describe("Relative file path (within the downloads directory) to save the attachment as"),
|
|
34071
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
|
|
33868
34072
|
},
|
|
33869
34073
|
async (args) => {
|
|
33870
34074
|
try {
|
|
33871
34075
|
const absPath = resolveDownloadPath(args.outputPath);
|
|
33872
34076
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33873
|
-
const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
|
|
34077
|
+
const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId, args.sourceFolder);
|
|
33874
34078
|
fs3.mkdirSync(path3.dirname(absPath), { recursive: true, mode: 448 });
|
|
33875
34079
|
fs3.writeFileSync(absPath, Buffer.from(data), { mode: 384 });
|
|
33876
34080
|
fs3.chmodSync(absPath, 384);
|
|
@@ -33887,6 +34091,161 @@ function registerReadingTools(server, accountManager) {
|
|
|
33887
34091
|
);
|
|
33888
34092
|
}
|
|
33889
34093
|
|
|
34094
|
+
// src/tools/attachments.ts
|
|
34095
|
+
import { closeSync, constants as fsConstants, fstatSync, lstatSync, openSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
34096
|
+
import os4 from "node:os";
|
|
34097
|
+
import path4 from "node:path";
|
|
34098
|
+
var MAX_TOTAL_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
34099
|
+
var ATTACHMENTS_DIR_ENV = "EMAIL_MCP_ATTACHMENTS_DIR";
|
|
34100
|
+
var MIME_BY_EXT = {
|
|
34101
|
+
".pdf": "application/pdf",
|
|
34102
|
+
".png": "image/png",
|
|
34103
|
+
".jpg": "image/jpeg",
|
|
34104
|
+
".jpeg": "image/jpeg",
|
|
34105
|
+
".gif": "image/gif",
|
|
34106
|
+
".webp": "image/webp",
|
|
34107
|
+
".txt": "text/plain",
|
|
34108
|
+
".csv": "text/csv",
|
|
34109
|
+
".html": "text/html",
|
|
34110
|
+
".json": "application/json",
|
|
34111
|
+
".zip": "application/zip",
|
|
34112
|
+
".doc": "application/msword",
|
|
34113
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
34114
|
+
".xls": "application/vnd.ms-excel",
|
|
34115
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
34116
|
+
".ics": "text/calendar"
|
|
34117
|
+
};
|
|
34118
|
+
var AttachmentInputSchema = external_exports3.object({
|
|
34119
|
+
path: external_exports3.string().optional().describe(
|
|
34120
|
+
`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.`
|
|
34121
|
+
),
|
|
34122
|
+
content: external_exports3.string().optional().describe("Base64-encoded file content, used instead of path"),
|
|
34123
|
+
filename: external_exports3.string().optional().describe("File name shown to the recipient. Required with content; defaults to the file name of path"),
|
|
34124
|
+
contentType: external_exports3.string().optional().describe("MIME type. Inferred from the file extension when omitted")
|
|
34125
|
+
}).describe("One attachment: give either path or content (base64) plus filename");
|
|
34126
|
+
var AttachmentsSchema = external_exports3.array(AttachmentInputSchema).optional().describe(`Files to attach. Total size is capped at ${MAX_TOTAL_ATTACHMENT_BYTES / (1024 * 1024)} MB`);
|
|
34127
|
+
function inferContentType(filename) {
|
|
34128
|
+
return MIME_BY_EXT[path4.extname(filename).toLowerCase()] ?? "application/octet-stream";
|
|
34129
|
+
}
|
|
34130
|
+
function megabytes2(bytes) {
|
|
34131
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
34132
|
+
}
|
|
34133
|
+
function isInside(dir, target) {
|
|
34134
|
+
const relative = path4.relative(dir, target);
|
|
34135
|
+
return relative !== "" && !relative.startsWith("..") && !path4.isAbsolute(relative);
|
|
34136
|
+
}
|
|
34137
|
+
function attachmentsDir(env = process.env) {
|
|
34138
|
+
const raw = (env[ATTACHMENTS_DIR_ENV] || "").trim();
|
|
34139
|
+
if (!raw) return null;
|
|
34140
|
+
const expanded = raw === "~" || raw.startsWith("~/") ? path4.join(os4.homedir(), raw.slice(1)) : raw;
|
|
34141
|
+
return path4.resolve(expanded);
|
|
34142
|
+
}
|
|
34143
|
+
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.`;
|
|
34144
|
+
function planPath(input2, env, dataDir) {
|
|
34145
|
+
const given = input2.path;
|
|
34146
|
+
if (given.trim() === "") throw new Error("Attachment path is empty");
|
|
34147
|
+
const dir = attachmentsDir(env);
|
|
34148
|
+
if (!dir) {
|
|
34149
|
+
throw new Error(`Attaching files by path is switched off: ${ATTACHMENTS_DIR_ENV} is not set. ${HOW_TO_ALLOW}`);
|
|
34150
|
+
}
|
|
34151
|
+
let dirReal;
|
|
34152
|
+
try {
|
|
34153
|
+
dirReal = realpathSync(dir);
|
|
34154
|
+
if (!statSync(dirReal).isDirectory()) throw new Error("not a directory");
|
|
34155
|
+
} catch {
|
|
34156
|
+
throw new Error(`The attachments folder ${dir} (${ATTACHMENTS_DIR_ENV}) does not exist or is not a folder.`);
|
|
34157
|
+
}
|
|
34158
|
+
const candidate = path4.resolve(dir, given);
|
|
34159
|
+
if (!isInside(dir, candidate) && !isInside(dirReal, candidate)) {
|
|
34160
|
+
throw new Error(`Only files inside ${dir} can be attached (${ATTACHMENTS_DIR_ENV}). Put the file there and pass its name.`);
|
|
34161
|
+
}
|
|
34162
|
+
let real;
|
|
34163
|
+
try {
|
|
34164
|
+
real = realpathSync(candidate);
|
|
34165
|
+
} catch {
|
|
34166
|
+
throw new Error(`No such file in the attachments folder: ${path4.relative(dir, candidate)}`);
|
|
34167
|
+
}
|
|
34168
|
+
if (!isInside(dirReal, real)) {
|
|
34169
|
+
throw new Error(`Only files inside ${dir} can be attached (${ATTACHMENTS_DIR_ENV}); ${path4.relative(dir, candidate)} points outside it.`);
|
|
34170
|
+
}
|
|
34171
|
+
let ownDir = null;
|
|
34172
|
+
try {
|
|
34173
|
+
ownDir = realpathSync(dataDir);
|
|
34174
|
+
} catch {
|
|
34175
|
+
ownDir = null;
|
|
34176
|
+
}
|
|
34177
|
+
if (ownDir && path4.dirname(real) === ownDir) {
|
|
34178
|
+
throw new Error("Files in the email-mcp data folder itself cannot be attached.");
|
|
34179
|
+
}
|
|
34180
|
+
const stat = lstatSync(real);
|
|
34181
|
+
if (!stat.isFile()) {
|
|
34182
|
+
throw new Error(`Not a file: ${path4.relative(dir, candidate)}`);
|
|
34183
|
+
}
|
|
34184
|
+
const filename = input2.filename ?? path4.basename(real);
|
|
34185
|
+
return {
|
|
34186
|
+
bytes: stat.size,
|
|
34187
|
+
load: () => ({
|
|
34188
|
+
filename,
|
|
34189
|
+
content: readValidatedFile(real, stat, path4.relative(dir, candidate)),
|
|
34190
|
+
contentType: input2.contentType ?? inferContentType(filename)
|
|
34191
|
+
})
|
|
34192
|
+
};
|
|
34193
|
+
}
|
|
34194
|
+
function readValidatedFile(real, validated, display) {
|
|
34195
|
+
let fd;
|
|
34196
|
+
try {
|
|
34197
|
+
fd = openSync(real, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
34198
|
+
} catch {
|
|
34199
|
+
throw new Error(`${display} changed while it was being attached; try again.`);
|
|
34200
|
+
}
|
|
34201
|
+
try {
|
|
34202
|
+
const opened = fstatSync(fd);
|
|
34203
|
+
if (!opened.isFile() || opened.dev !== validated.dev || opened.ino !== validated.ino) {
|
|
34204
|
+
throw new Error(`${display} changed while it was being attached; try again.`);
|
|
34205
|
+
}
|
|
34206
|
+
return readFileSync(fd);
|
|
34207
|
+
} finally {
|
|
34208
|
+
closeSync(fd);
|
|
34209
|
+
}
|
|
34210
|
+
}
|
|
34211
|
+
function planContent(input2) {
|
|
34212
|
+
if (!input2.filename) throw new Error("Attachment with content requires a filename");
|
|
34213
|
+
const filename = input2.filename;
|
|
34214
|
+
const compact = input2.content.replace(/\s+/g, "");
|
|
34215
|
+
if (compact === "") throw new Error(`Attachment ${filename} has empty content`);
|
|
34216
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(compact) || compact.length % 4 === 1) {
|
|
34217
|
+
throw new Error(`Attachment ${filename}: content is not valid base64`);
|
|
34218
|
+
}
|
|
34219
|
+
const padding = compact.endsWith("==") ? 2 : compact.endsWith("=") ? 1 : 0;
|
|
34220
|
+
return {
|
|
34221
|
+
bytes: Math.floor(compact.length * 3 / 4) - padding,
|
|
34222
|
+
load: () => ({
|
|
34223
|
+
filename,
|
|
34224
|
+
content: Buffer.from(compact, "base64"),
|
|
34225
|
+
contentType: input2.contentType ?? inferContentType(filename)
|
|
34226
|
+
})
|
|
34227
|
+
};
|
|
34228
|
+
}
|
|
34229
|
+
function plan(input2, env, dataDir) {
|
|
34230
|
+
const hasPath = input2.path !== void 0;
|
|
34231
|
+
const hasContent = input2.content !== void 0;
|
|
34232
|
+
if (hasPath && hasContent) throw new Error("Attachment must have either path or content, not both");
|
|
34233
|
+
if (hasPath) return planPath(input2, env, dataDir);
|
|
34234
|
+
if (hasContent) return planContent(input2);
|
|
34235
|
+
throw new Error("Attachment needs a path or content");
|
|
34236
|
+
}
|
|
34237
|
+
function resolveAttachments(inputs, env = process.env, dataDir = path4.join(os4.homedir(), ".email-mcp")) {
|
|
34238
|
+
if (inputs === void 0) return void 0;
|
|
34239
|
+
const planned = inputs.map((input2) => plan(input2, env, dataDir));
|
|
34240
|
+
assertAttachmentTotal(planned.reduce((sum, p) => sum + p.bytes, 0));
|
|
34241
|
+
return planned.map((p) => p.load());
|
|
34242
|
+
}
|
|
34243
|
+
function assertAttachmentTotal(totalBytes) {
|
|
34244
|
+
if (totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
|
|
34245
|
+
throw new Error(`Attachments total ${megabytes2(totalBytes)}, over the ${megabytes2(MAX_TOTAL_ATTACHMENT_BYTES)} limit`);
|
|
34246
|
+
}
|
|
34247
|
+
}
|
|
34248
|
+
|
|
33890
34249
|
// src/tools/sending.ts
|
|
33891
34250
|
var ContactSchema = external_exports3.object({
|
|
33892
34251
|
email: external_exports3.string(),
|
|
@@ -33899,6 +34258,31 @@ var BodySchema = external_exports3.object({
|
|
|
33899
34258
|
function jsonResult3(data) {
|
|
33900
34259
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
33901
34260
|
}
|
|
34261
|
+
function messageIds(headerValue2) {
|
|
34262
|
+
return headerValue2?.match(/<[^<>\s]+>/g) ?? [];
|
|
34263
|
+
}
|
|
34264
|
+
function replyThreading(original) {
|
|
34265
|
+
const [messageId] = messageIds(original.headers?.["message-id"]);
|
|
34266
|
+
if (!messageId) return { threadId: original.threadId, replyToGraphId: original.id };
|
|
34267
|
+
const earlier = messageIds(original.headers?.["references"]).filter((id) => id !== messageId);
|
|
34268
|
+
return {
|
|
34269
|
+
inReplyTo: messageId,
|
|
34270
|
+
references: [...earlier, messageId],
|
|
34271
|
+
threadId: original.threadId,
|
|
34272
|
+
replyToGraphId: original.id
|
|
34273
|
+
};
|
|
34274
|
+
}
|
|
34275
|
+
var sameAddress = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
34276
|
+
function withoutDuplicates(to, cc) {
|
|
34277
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34278
|
+
const firstMention = (c) => {
|
|
34279
|
+
const key = c.email.toLowerCase();
|
|
34280
|
+
if (seen.has(key)) return false;
|
|
34281
|
+
seen.add(key);
|
|
34282
|
+
return true;
|
|
34283
|
+
};
|
|
34284
|
+
return { to: to.filter(firstMention), cc: cc?.filter(firstMention) };
|
|
34285
|
+
}
|
|
33902
34286
|
function registerSendingTools(server, accountManager) {
|
|
33903
34287
|
server.tool(
|
|
33904
34288
|
"email_send",
|
|
@@ -33909,7 +34293,8 @@ function registerSendingTools(server, accountManager) {
|
|
|
33909
34293
|
cc: external_exports3.array(ContactSchema).optional(),
|
|
33910
34294
|
bcc: external_exports3.array(ContactSchema).optional(),
|
|
33911
34295
|
subject: external_exports3.string(),
|
|
33912
|
-
body: BodySchema
|
|
34296
|
+
body: BodySchema,
|
|
34297
|
+
attachments: AttachmentsSchema
|
|
33913
34298
|
},
|
|
33914
34299
|
async (args) => {
|
|
33915
34300
|
try {
|
|
@@ -33919,7 +34304,8 @@ function registerSendingTools(server, accountManager) {
|
|
|
33919
34304
|
cc: args.cc,
|
|
33920
34305
|
bcc: args.bcc,
|
|
33921
34306
|
subject: args.subject,
|
|
33922
|
-
body: args.body
|
|
34307
|
+
body: args.body,
|
|
34308
|
+
attachments: resolveAttachments(args.attachments)
|
|
33923
34309
|
};
|
|
33924
34310
|
const result = await provider.sendEmail(params);
|
|
33925
34311
|
return jsonResult3(result);
|
|
@@ -33930,32 +34316,53 @@ function registerSendingTools(server, accountManager) {
|
|
|
33930
34316
|
);
|
|
33931
34317
|
server.tool(
|
|
33932
34318
|
"email_reply",
|
|
33933
|
-
"Reply to an existing email",
|
|
34319
|
+
"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
34320
|
{
|
|
33935
34321
|
accountId: external_exports3.string(),
|
|
33936
34322
|
emailId: external_exports3.string(),
|
|
34323
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)"),
|
|
33937
34324
|
body: BodySchema,
|
|
33938
|
-
replyAll: external_exports3.boolean().optional()
|
|
34325
|
+
replyAll: external_exports3.boolean().optional(),
|
|
34326
|
+
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"),
|
|
34327
|
+
cc: external_exports3.array(ContactSchema).optional().describe("Cc recipients. Replaces any Cc carried over by replyAll"),
|
|
34328
|
+
bcc: external_exports3.array(ContactSchema).optional(),
|
|
34329
|
+
additionalRecipients: external_exports3.array(ContactSchema).optional().describe("Extra To recipients added to the default or given To (deduplicated by address)"),
|
|
34330
|
+
attachments: AttachmentsSchema
|
|
33939
34331
|
},
|
|
33940
34332
|
async (args) => {
|
|
33941
34333
|
try {
|
|
33942
34334
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33943
|
-
const original = await provider.getEmail(args.emailId);
|
|
33944
|
-
const messageId = original.headers?.["message-id"] ?? original.id;
|
|
34335
|
+
const original = await provider.getEmail(args.emailId, args.sourceFolder);
|
|
33945
34336
|
const subject = original.subject.startsWith("Re: ") ? original.subject : `Re: ${original.subject}`;
|
|
33946
34337
|
let to = [original.from];
|
|
33947
34338
|
let cc;
|
|
33948
34339
|
if (args.replyAll) {
|
|
33949
|
-
|
|
33950
|
-
|
|
34340
|
+
const own = await accountManager.getAccountEmail(args.accountId);
|
|
34341
|
+
const others = (c) => !own || !sameAddress(c.email, own);
|
|
34342
|
+
to = [original.from, ...original.to].filter(others);
|
|
34343
|
+
if (to.length === 0) to = [original.from];
|
|
34344
|
+
cc = original.cc?.filter(others);
|
|
34345
|
+
}
|
|
34346
|
+
if (args.to?.length) to = args.to;
|
|
34347
|
+
if (args.additionalRecipients?.length) {
|
|
34348
|
+
const seen = new Set(to.map((c) => c.email.toLowerCase()));
|
|
34349
|
+
for (const r of args.additionalRecipients) {
|
|
34350
|
+
if (!seen.has(r.email.toLowerCase())) {
|
|
34351
|
+
to = [...to, r];
|
|
34352
|
+
seen.add(r.email.toLowerCase());
|
|
34353
|
+
}
|
|
34354
|
+
}
|
|
33951
34355
|
}
|
|
34356
|
+
if (args.cc !== void 0) cc = args.cc;
|
|
34357
|
+
if (args.replyAll) ({ to, cc } = withoutDuplicates(to, cc));
|
|
33952
34358
|
const params = {
|
|
33953
34359
|
to,
|
|
33954
34360
|
cc,
|
|
34361
|
+
bcc: args.bcc,
|
|
33955
34362
|
subject,
|
|
33956
34363
|
body: args.body,
|
|
33957
|
-
|
|
33958
|
-
|
|
34364
|
+
...replyThreading(original),
|
|
34365
|
+
attachments: resolveAttachments(args.attachments)
|
|
33959
34366
|
};
|
|
33960
34367
|
const result = await provider.sendEmail(params);
|
|
33961
34368
|
return jsonResult3(result);
|
|
@@ -33966,17 +34373,22 @@ function registerSendingTools(server, accountManager) {
|
|
|
33966
34373
|
);
|
|
33967
34374
|
server.tool(
|
|
33968
34375
|
"email_forward",
|
|
33969
|
-
"Forward an email to new recipients",
|
|
34376
|
+
"Forward an email to new recipients. The original message's attachments are left out unless includeOriginalAttachments is true",
|
|
33970
34377
|
{
|
|
33971
34378
|
accountId: external_exports3.string(),
|
|
33972
34379
|
emailId: external_exports3.string(),
|
|
33973
34380
|
to: external_exports3.array(ContactSchema),
|
|
33974
|
-
|
|
34381
|
+
cc: external_exports3.array(ContactSchema).optional(),
|
|
34382
|
+
bcc: external_exports3.array(ContactSchema).optional(),
|
|
34383
|
+
body: BodySchema.optional(),
|
|
34384
|
+
attachments: AttachmentsSchema.describe("Files to attach to the forward. Total size, together with forwarded original attachments, is capped at 25 MB"),
|
|
34385
|
+
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"),
|
|
34386
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
|
|
33975
34387
|
},
|
|
33976
34388
|
async (args) => {
|
|
33977
34389
|
try {
|
|
33978
34390
|
const provider = await accountManager.getProvider(args.accountId);
|
|
33979
|
-
const original = await provider.getEmail(args.emailId);
|
|
34391
|
+
const original = await provider.getEmail(args.emailId, args.sourceFolder);
|
|
33980
34392
|
const subject = original.subject.startsWith("Fwd: ") ? original.subject : `Fwd: ${original.subject}`;
|
|
33981
34393
|
const forwardHeader = [
|
|
33982
34394
|
"---------- Forwarded message ----------",
|
|
@@ -34010,10 +34422,27 @@ function registerSendingTools(server, accountManager) {
|
|
|
34010
34422
|
originalHtml
|
|
34011
34423
|
].join("");
|
|
34012
34424
|
}
|
|
34425
|
+
const extra = resolveAttachments(args.attachments) ?? [];
|
|
34426
|
+
const forwarded = [];
|
|
34427
|
+
if (args.includeOriginalAttachments && original.attachments?.length) {
|
|
34428
|
+
const extraBytes = extra.reduce((sum, a) => sum + a.content.length, 0);
|
|
34429
|
+
assertAttachmentTotal(extraBytes + original.attachments.reduce((sum, a) => sum + (a.size || 0), 0));
|
|
34430
|
+
let bytes = extraBytes;
|
|
34431
|
+
for (const att of original.attachments) {
|
|
34432
|
+
const fetched = await provider.getAttachment(original.id, att.id, args.sourceFolder);
|
|
34433
|
+
bytes += fetched.data.length;
|
|
34434
|
+
assertAttachmentTotal(bytes);
|
|
34435
|
+
forwarded.push({ filename: att.filename, content: fetched.data, contentType: att.contentType });
|
|
34436
|
+
}
|
|
34437
|
+
}
|
|
34438
|
+
const attachments = [...forwarded, ...extra];
|
|
34013
34439
|
const params = {
|
|
34014
34440
|
to: args.to,
|
|
34441
|
+
cc: args.cc,
|
|
34442
|
+
bcc: args.bcc,
|
|
34015
34443
|
subject,
|
|
34016
|
-
body: { text: forwardedText, html: forwardedHtml }
|
|
34444
|
+
body: { text: forwardedText, html: forwardedHtml },
|
|
34445
|
+
attachments: attachments.length ? attachments : void 0
|
|
34017
34446
|
};
|
|
34018
34447
|
const result = await provider.sendEmail(params);
|
|
34019
34448
|
return jsonResult3(result);
|
|
@@ -34024,12 +34453,15 @@ function registerSendingTools(server, accountManager) {
|
|
|
34024
34453
|
);
|
|
34025
34454
|
server.tool(
|
|
34026
34455
|
"email_draft_create",
|
|
34027
|
-
"Create a new email draft",
|
|
34456
|
+
"Create a new email draft. Pass inReplyToEmailId to save it as a reply in that message's thread",
|
|
34028
34457
|
{
|
|
34029
34458
|
accountId: external_exports3.string(),
|
|
34030
34459
|
to: external_exports3.array(ContactSchema),
|
|
34031
34460
|
subject: external_exports3.string(),
|
|
34032
|
-
body: BodySchema
|
|
34461
|
+
body: BodySchema,
|
|
34462
|
+
attachments: AttachmentsSchema,
|
|
34463
|
+
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)"),
|
|
34464
|
+
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
34465
|
},
|
|
34034
34466
|
async (args) => {
|
|
34035
34467
|
try {
|
|
@@ -34037,8 +34469,12 @@ function registerSendingTools(server, accountManager) {
|
|
|
34037
34469
|
const params = {
|
|
34038
34470
|
to: args.to,
|
|
34039
34471
|
subject: args.subject,
|
|
34040
|
-
body: args.body
|
|
34472
|
+
body: args.body,
|
|
34473
|
+
attachments: resolveAttachments(args.attachments)
|
|
34041
34474
|
};
|
|
34475
|
+
if (args.inReplyToEmailId) {
|
|
34476
|
+
Object.assign(params, replyThreading(await provider.getEmail(args.inReplyToEmailId, args.inReplyToSourceFolder)));
|
|
34477
|
+
}
|
|
34042
34478
|
const result = await provider.createDraft(params);
|
|
34043
34479
|
return jsonResult3(result);
|
|
34044
34480
|
} catch (error48) {
|
|
@@ -34048,13 +34484,16 @@ function registerSendingTools(server, accountManager) {
|
|
|
34048
34484
|
);
|
|
34049
34485
|
server.tool(
|
|
34050
34486
|
"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
|
|
34487
|
+
"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
34488
|
{
|
|
34053
34489
|
accountId: external_exports3.string(),
|
|
34054
34490
|
draftId: external_exports3.string(),
|
|
34055
34491
|
to: external_exports3.array(ContactSchema),
|
|
34056
34492
|
subject: external_exports3.string(),
|
|
34057
34493
|
body: BodySchema,
|
|
34494
|
+
attachments: AttachmentsSchema.describe(
|
|
34495
|
+
"Files to attach. When given, they replace the draft's current attachments; an empty list removes them all. When omitted, Outlook keeps the existing files but Gmail and IMAP drop them (those providers rewrite the whole message), so pass them again to keep them."
|
|
34496
|
+
),
|
|
34058
34497
|
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when the draft is not in the default Drafts folder)")
|
|
34059
34498
|
},
|
|
34060
34499
|
async (args) => {
|
|
@@ -34063,7 +34502,8 @@ function registerSendingTools(server, accountManager) {
|
|
|
34063
34502
|
const params = {
|
|
34064
34503
|
to: args.to,
|
|
34065
34504
|
subject: args.subject,
|
|
34066
|
-
body: args.body
|
|
34505
|
+
body: args.body,
|
|
34506
|
+
attachments: resolveAttachments(args.attachments)
|
|
34067
34507
|
};
|
|
34068
34508
|
const result = await provider.updateDraft(args.draftId, params, args.sourceFolder);
|
|
34069
34509
|
return jsonResult3(result);
|
|
@@ -34605,7 +35045,7 @@ var FORWARD_ALLOWLIST_ENV = "EMAIL_MCP_FORWARD_ALLOWLIST";
|
|
|
34605
35045
|
function forwardAllowlist(env = process.env) {
|
|
34606
35046
|
return (env[FORWARD_ALLOWLIST_ENV] || "").split(",").map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
|
|
34607
35047
|
}
|
|
34608
|
-
var
|
|
35048
|
+
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
35049
|
function withScopeHint(error48) {
|
|
34610
35050
|
const message = error48?.message || String(error48);
|
|
34611
35051
|
const status = error48?.code ?? error48?.statusCode ?? error48?.status ?? error48?.response?.status;
|
|
@@ -34643,14 +35083,14 @@ function registerForwardingTools(server, accountManager) {
|
|
|
34643
35083
|
if (allowed.length === 0) {
|
|
34644
35084
|
return jsonResult6({
|
|
34645
35085
|
success: false,
|
|
34646
|
-
error: `Forwarding is switched off: ${FORWARD_ALLOWLIST_ENV} is not set. ${
|
|
35086
|
+
error: `Forwarding is switched off: ${FORWARD_ALLOWLIST_ENV} is not set. ${HOW_TO_ALLOW2}`
|
|
34647
35087
|
});
|
|
34648
35088
|
}
|
|
34649
35089
|
const forwardTo = args.forwardTo.trim();
|
|
34650
35090
|
if (!allowed.includes(forwardTo.toLowerCase())) {
|
|
34651
35091
|
return jsonResult6({
|
|
34652
35092
|
success: false,
|
|
34653
|
-
error: `${forwardTo} is not an allowed forwarding target. ${
|
|
35093
|
+
error: `${forwardTo} is not an allowed forwarding target. ${HOW_TO_ALLOW2}`,
|
|
34654
35094
|
allowedTargets: allowed
|
|
34655
35095
|
});
|
|
34656
35096
|
}
|
|
@@ -34716,7 +35156,7 @@ function registerForwardingTools(server, accountManager) {
|
|
|
34716
35156
|
}
|
|
34717
35157
|
|
|
34718
35158
|
// src/server.ts
|
|
34719
|
-
var SERVER_VERSION = "1.
|
|
35159
|
+
var SERVER_VERSION = "1.10.0";
|
|
34720
35160
|
async function createServer(accountManager) {
|
|
34721
35161
|
const mgr = accountManager ?? new AccountManager();
|
|
34722
35162
|
const server = new McpServer({
|