@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 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 and an empty list removes them.
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 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.
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
- if (update.threadId === void 0) {
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
- // 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"
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
- 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] };
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
- if (params.inReplyTo === void 0) {
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
- const references = [].concat(parsed.references ?? []);
9570
- if (parsed.inReplyTo) {
9571
- next = { ...params, inReplyTo: parsed.inReplyTo, references: params.references ?? references };
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, Outlook keeps the existing files but Gmail and IMAP drop them (those providers rewrite the whole message), so pass them again to keep them."
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.0";
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({