@marlinjai/email-mcp 1.10.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 +1 -1
- package/dist/index.js +85 -19
- package/dist/index.js.map +2 -2
- package/dist/setup/wizard.js +83 -17
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
|
|
|
141
141
|
|
|
142
142
|
### Sending & Drafts (6)
|
|
143
143
|
|
|
144
|
-
`email_send`, `email_reply`, `email_forward`, `email_draft_create` and `email_draft_update` take an optional `attachments` list. Each entry is either `{ path }` (a file in the attachments folder, see below) or `{ content, filename }` (base64), with an optional `contentType` (inferred from the extension otherwise). The total is capped at 25 MB; Outlook accepts up to 3 MB per message through this server. On `email_draft_update` a list replaces the draft's files
|
|
144
|
+
`email_send`, `email_reply`, `email_forward`, `email_draft_create` and `email_draft_update` take an optional `attachments` list. Each entry is either `{ path }` (a file in the attachments folder, see below) or `{ content, filename }` (base64), with an optional `contentType` (inferred from the extension otherwise). The total is capped at 25 MB; Outlook accepts up to 3 MB per message through this server. On `email_draft_update` a list replaces the draft's files, an empty list removes them, and no list keeps the files the draft has.
|
|
145
145
|
|
|
146
146
|
Replies stay in their thread: `email_reply` sends inside the original conversation on Gmail and Outlook (and sets the `In-Reply-To` and `References` headers everywhere), `email_draft_create` with `inReplyToEmailId` saves a reply draft in that thread, and `email_draft_update` keeps a reply draft there.
|
|
147
147
|
|
package/dist/index.js
CHANGED
|
@@ -7386,27 +7386,39 @@ var init_adapter = __esm({
|
|
|
7386
7386
|
return { id: res.data.id || "" };
|
|
7387
7387
|
}
|
|
7388
7388
|
/**
|
|
7389
|
-
* drafts.update replaces the whole message, so
|
|
7390
|
-
*
|
|
7391
|
-
*
|
|
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.
|
|
7392
7395
|
*/
|
|
7393
7396
|
async updateDraft(draftId, update) {
|
|
7394
7397
|
const gmail = this.ensureConnected();
|
|
7395
7398
|
let params = update;
|
|
7396
|
-
|
|
7399
|
+
const keepThread = update.threadId === void 0;
|
|
7400
|
+
const keepFiles = update.attachments === void 0;
|
|
7401
|
+
if (keepThread || keepFiles) {
|
|
7397
7402
|
const existing = await gmail.users.drafts.get({
|
|
7398
7403
|
userId: "me",
|
|
7399
7404
|
id: draftId,
|
|
7400
|
-
//
|
|
7401
|
-
// single headers
|
|
7402
|
-
|
|
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"
|
|
7403
7409
|
});
|
|
7404
7410
|
const message = existing.data.message;
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
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 };
|
|
7410
7422
|
}
|
|
7411
7423
|
}
|
|
7412
7424
|
const raw = await this.buildRaw(params);
|
|
@@ -7803,6 +7815,41 @@ var init_adapter = __esm({
|
|
|
7803
7815
|
const belongs = params.threadId && params.inReplyTo && params.references?.length && params.subject.trim();
|
|
7804
7816
|
return belongs ? { threadId: params.threadId } : {};
|
|
7805
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
|
+
}
|
|
7806
7853
|
/**
|
|
7807
7854
|
* Builds the base64url `raw` payload Gmail expects for send, draft create and
|
|
7808
7855
|
* draft update: a full MIME message with a text part, an HTML part when one
|
|
@@ -9548,7 +9595,11 @@ var init_adapter3 = __esm({
|
|
|
9548
9595
|
*
|
|
9549
9596
|
* A reply draft keeps its place in the thread: when the caller passes no
|
|
9550
9597
|
* `inReplyTo`, the In-Reply-To and References headers of the old revision
|
|
9551
|
-
* are copied into the new one.
|
|
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.
|
|
9552
9603
|
*/
|
|
9553
9604
|
async updateDraft(draftId, params, sourceFolder) {
|
|
9554
9605
|
const client = await this.ensureConnected();
|
|
@@ -9562,13 +9613,28 @@ var init_adapter3 = __esm({
|
|
|
9562
9613
|
let rawMessage;
|
|
9563
9614
|
try {
|
|
9564
9615
|
let next = params;
|
|
9565
|
-
|
|
9616
|
+
const keepThread = params.inReplyTo === void 0;
|
|
9617
|
+
const keepFiles = params.attachments === void 0;
|
|
9618
|
+
if (keepThread || keepFiles) {
|
|
9566
9619
|
const old = await client.fetchOne(String(draftId), { source: true, uid: true }, { uid: true });
|
|
9567
9620
|
if (old && old.source) {
|
|
9568
9621
|
const parsed = await simpleParser(old.source);
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
next = { ...
|
|
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 };
|
|
9572
9638
|
}
|
|
9573
9639
|
}
|
|
9574
9640
|
}
|
|
@@ -34492,7 +34558,7 @@ function registerSendingTools(server, accountManager) {
|
|
|
34492
34558
|
subject: external_exports3.string(),
|
|
34493
34559
|
body: BodySchema,
|
|
34494
34560
|
attachments: AttachmentsSchema.describe(
|
|
34495
|
-
"Files to attach. When given, they replace the draft's current attachments; an empty list removes them all. When omitted,
|
|
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."
|
|
34496
34562
|
),
|
|
34497
34563
|
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when the draft is not in the default Drafts folder)")
|
|
34498
34564
|
},
|
|
@@ -35156,7 +35222,7 @@ function registerForwardingTools(server, accountManager) {
|
|
|
35156
35222
|
}
|
|
35157
35223
|
|
|
35158
35224
|
// src/server.ts
|
|
35159
|
-
var SERVER_VERSION = "1.10.
|
|
35225
|
+
var SERVER_VERSION = "1.10.1";
|
|
35160
35226
|
async function createServer(accountManager) {
|
|
35161
35227
|
const mgr = accountManager ?? new AccountManager();
|
|
35162
35228
|
const server = new McpServer({
|