@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/dist/setup/wizard.js
CHANGED
|
@@ -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
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
1127
|
-
// single headers
|
|
1128
|
-
|
|
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
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2940
|
-
|
|
2941
|
-
next = { ...
|
|
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
|
}
|