@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.
@@ -1112,27 +1112,39 @@ var GmailAdapter = class {
1112
1112
  return { id: res.data.id || "" };
1113
1113
  }
1114
1114
  /**
1115
- * drafts.update replaces the whole message, so a reply draft would fall out
1116
- * of its thread. When the caller names no thread, the thread and the reply
1117
- * headers of the stored draft are carried over into the new message.
1115
+ * drafts.update replaces the whole message, so what the caller does not
1116
+ * name would be lost: a reply draft would fall out of its thread and a
1117
+ * draft's files would be dropped. So when the caller names no thread, the
1118
+ * thread and the reply headers of the stored draft are carried over, and
1119
+ * when the caller names no attachments, the stored draft's files are. A
1120
+ * given list (an empty one included) replaces the files.
1118
1121
  */
1119
1122
  async updateDraft(draftId, update) {
1120
1123
  const gmail = this.ensureConnected();
1121
1124
  let params = update;
1122
- if (update.threadId === void 0) {
1125
+ const keepThread = update.threadId === void 0;
1126
+ const keepFiles = update.attachments === void 0;
1127
+ if (keepThread || keepFiles) {
1123
1128
  const existing = await gmail.users.drafts.get({
1124
1129
  userId: "me",
1125
1130
  id: draftId,
1126
- // Ids, labels and headers, no body. drafts.get has no way to ask for
1127
- // single headers (messages.get does), so all of them come back.
1128
- format: "metadata"
1131
+ // 'metadata' is ids, labels and headers, no body (drafts.get has no
1132
+ // way to ask for single headers, so all of them come back). The parts
1133
+ // that list the files only come with 'full'.
1134
+ format: keepFiles ? "full" : "metadata"
1129
1135
  });
1130
1136
  const message = existing.data.message;
1131
- const header = (name) => (message?.payload?.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || void 0;
1132
- const inReplyTo = update.inReplyTo ?? header("in-reply-to");
1133
- const references = update.references ?? header("references")?.split(/\s+/).filter(Boolean);
1134
- if (message?.threadId && inReplyTo) {
1135
- params = { ...update, threadId: message.threadId, inReplyTo, references: references?.length ? references : [inReplyTo] };
1137
+ if (keepThread) {
1138
+ const header = (name) => (message?.payload?.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || void 0;
1139
+ const inReplyTo = update.inReplyTo ?? header("in-reply-to");
1140
+ const references = update.references ?? header("references")?.split(/\s+/).filter(Boolean);
1141
+ if (message?.threadId && inReplyTo) {
1142
+ params = { ...params, threadId: message.threadId, inReplyTo, references: references?.length ? references : [inReplyTo] };
1143
+ }
1144
+ }
1145
+ if (keepFiles && message?.id) {
1146
+ const stored = await this.storedAttachments(message.id, message.payload);
1147
+ if (stored.length) params = { ...params, attachments: stored };
1136
1148
  }
1137
1149
  }
1138
1150
  const raw = await this.buildRaw(params);
@@ -1529,6 +1541,41 @@ var GmailAdapter = class {
1529
1541
  const belongs = params.threadId && params.inReplyTo && params.references?.length && params.subject.trim();
1530
1542
  return belongs ? { threadId: params.threadId } : {};
1531
1543
  }
1544
+ /**
1545
+ * The files of a stored message, with their bytes: every part that has a
1546
+ * file name, at any depth. Large files are referenced by an attachment id
1547
+ * and fetched; small ones can come inline in the part. A fetch that fails
1548
+ * is not swallowed: a draft update built on it would drop that file.
1549
+ */
1550
+ async storedAttachments(messageId, payload) {
1551
+ const gmail = this.ensureConnected();
1552
+ const files = [];
1553
+ const html = (extractBody(payload).html || "").toLowerCase();
1554
+ const visit = async (part) => {
1555
+ if (!part) return;
1556
+ const partHeader = (name) => (part.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || "";
1557
+ const contentId = partHeader("content-id").trim().replace(/^<|>$/g, "").toLowerCase();
1558
+ const embedded = /^\s*inline/i.test(partHeader("content-disposition")) && contentId !== "" && html.includes(`cid:${contentId}`);
1559
+ if (part.filename && !embedded) {
1560
+ let data = part.body?.data;
1561
+ if (!data && part.body?.attachmentId) {
1562
+ const res = await gmail.users.messages.attachments.get({ userId: "me", messageId, id: part.body.attachmentId });
1563
+ data = res.data.data;
1564
+ }
1565
+ if (data === void 0 || data === null) {
1566
+ throw new Error(`Could not read the draft's attachment ${part.filename}; the draft was not changed.`);
1567
+ }
1568
+ files.push({
1569
+ filename: part.filename,
1570
+ content: Buffer.from(data, "base64url"),
1571
+ contentType: part.mimeType || "application/octet-stream"
1572
+ });
1573
+ }
1574
+ for (const child of part.parts || []) await visit(child);
1575
+ };
1576
+ await visit(payload);
1577
+ return files;
1578
+ }
1532
1579
  /**
1533
1580
  * Builds the base64url `raw` payload Gmail expects for send, draft create and
1534
1581
  * draft update: a full MIME message with a text part, an HTML part when one
@@ -2918,7 +2965,11 @@ var ImapAdapter = class {
2918
2965
  *
2919
2966
  * A reply draft keeps its place in the thread: when the caller passes no
2920
2967
  * `inReplyTo`, the In-Reply-To and References headers of the old revision
2921
- * are copied into the new one.
2968
+ * are copied into the new one. The same goes for files: when the caller
2969
+ * passes no `attachments`, the old revision's attachments are copied over
2970
+ * (a given list, an empty one included, replaces them). Images embedded in
2971
+ * the old HTML body are not files and are not copied: the new body replaces
2972
+ * the old one.
2922
2973
  */
2923
2974
  async updateDraft(draftId, params, sourceFolder) {
2924
2975
  const client = await this.ensureConnected();
@@ -2932,13 +2983,28 @@ var ImapAdapter = class {
2932
2983
  let rawMessage;
2933
2984
  try {
2934
2985
  let next = params;
2935
- if (params.inReplyTo === void 0) {
2986
+ const keepThread = params.inReplyTo === void 0;
2987
+ const keepFiles = params.attachments === void 0;
2988
+ if (keepThread || keepFiles) {
2936
2989
  const old = await client.fetchOne(String(draftId), { source: true, uid: true }, { uid: true });
2937
2990
  if (old && old.source) {
2938
2991
  const parsed = await simpleParser(old.source);
2939
- const references = [].concat(parsed.references ?? []);
2940
- if (parsed.inReplyTo) {
2941
- next = { ...params, inReplyTo: parsed.inReplyTo, references: params.references ?? references };
2992
+ if (keepThread && parsed.inReplyTo) {
2993
+ const references = [].concat(parsed.references ?? []);
2994
+ next = { ...next, inReplyTo: parsed.inReplyTo, references: params.references ?? references };
2995
+ }
2996
+ if (keepFiles) {
2997
+ const html = (parsed.html || "").toLowerCase();
2998
+ const files = (parsed.attachments || []).filter((att) => {
2999
+ if (!att.related) return true;
3000
+ const cid = String(att.cid || att.contentId || "").replace(/^<|>$/g, "").toLowerCase();
3001
+ return !cid || !html.includes(`cid:${cid}`);
3002
+ }).map((att, i) => ({
3003
+ filename: att.filename || `attachment-${i + 1}`,
3004
+ content: att.content,
3005
+ contentType: att.contentType || "application/octet-stream"
3006
+ }));
3007
+ if (files.length) next = { ...next, attachments: files };
2942
3008
  }
2943
3009
  }
2944
3010
  }