@marlinjai/email-mcp 1.9.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 +31 -9
- package/dist/index.js +589 -83
- package/dist/index.js.map +4 -4
- package/dist/setup/wizard.js +294 -28
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +1 -1
package/dist/setup/wizard.js
CHANGED
|
@@ -830,6 +830,14 @@ function extractAttachments(payload) {
|
|
|
830
830
|
}
|
|
831
831
|
return attachments;
|
|
832
832
|
}
|
|
833
|
+
function threadingHeaders(headers) {
|
|
834
|
+
const picked = {};
|
|
835
|
+
for (const name of ["Message-ID", "In-Reply-To", "References"]) {
|
|
836
|
+
const value = getHeader(headers, name);
|
|
837
|
+
if (value) picked[name.toLowerCase()] = value;
|
|
838
|
+
}
|
|
839
|
+
return Object.keys(picked).length > 0 ? picked : void 0;
|
|
840
|
+
}
|
|
833
841
|
function mapGmailMessage(message, accountId) {
|
|
834
842
|
const headers = message.payload?.headers || [];
|
|
835
843
|
const labelIds = message.labelIds || [];
|
|
@@ -864,6 +872,7 @@ function mapGmailMessage(message, accountId) {
|
|
|
864
872
|
snippet: message.snippet,
|
|
865
873
|
attachments,
|
|
866
874
|
labels: labelIds,
|
|
875
|
+
headers: threadingHeaders(headers),
|
|
867
876
|
flags: {
|
|
868
877
|
read: !labelIds.includes("UNREAD"),
|
|
869
878
|
starred: labelIds.includes("STARRED"),
|
|
@@ -1084,7 +1093,7 @@ var GmailAdapter = class {
|
|
|
1084
1093
|
const raw = await this.buildRaw(params);
|
|
1085
1094
|
const res = await gmail.users.messages.send({
|
|
1086
1095
|
userId: "me",
|
|
1087
|
-
requestBody: { raw }
|
|
1096
|
+
requestBody: { raw, ...this.threadFor(params) }
|
|
1088
1097
|
});
|
|
1089
1098
|
return {
|
|
1090
1099
|
id: res.data.id || "",
|
|
@@ -1097,19 +1106,53 @@ var GmailAdapter = class {
|
|
|
1097
1106
|
const res = await gmail.users.drafts.create({
|
|
1098
1107
|
userId: "me",
|
|
1099
1108
|
requestBody: {
|
|
1100
|
-
message: { raw }
|
|
1109
|
+
message: { raw, ...this.threadFor(params) }
|
|
1101
1110
|
}
|
|
1102
1111
|
});
|
|
1103
1112
|
return { id: res.data.id || "" };
|
|
1104
1113
|
}
|
|
1105
|
-
|
|
1114
|
+
/**
|
|
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.
|
|
1121
|
+
*/
|
|
1122
|
+
async updateDraft(draftId, update) {
|
|
1106
1123
|
const gmail = this.ensureConnected();
|
|
1124
|
+
let params = update;
|
|
1125
|
+
const keepThread = update.threadId === void 0;
|
|
1126
|
+
const keepFiles = update.attachments === void 0;
|
|
1127
|
+
if (keepThread || keepFiles) {
|
|
1128
|
+
const existing = await gmail.users.drafts.get({
|
|
1129
|
+
userId: "me",
|
|
1130
|
+
id: draftId,
|
|
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"
|
|
1135
|
+
});
|
|
1136
|
+
const message = existing.data.message;
|
|
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 };
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1107
1150
|
const raw = await this.buildRaw(params);
|
|
1108
1151
|
const res = await gmail.users.drafts.update({
|
|
1109
1152
|
userId: "me",
|
|
1110
1153
|
id: draftId,
|
|
1111
1154
|
requestBody: {
|
|
1112
|
-
message: { raw }
|
|
1155
|
+
message: { raw, ...this.threadFor(params) }
|
|
1113
1156
|
}
|
|
1114
1157
|
});
|
|
1115
1158
|
return { id: res.data.id || draftId };
|
|
@@ -1488,6 +1531,51 @@ var GmailAdapter = class {
|
|
|
1488
1531
|
}
|
|
1489
1532
|
await this.deleteBlockRule(ruleId);
|
|
1490
1533
|
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Gmail files a message into an existing thread only when the threadId
|
|
1536
|
+
* comes with In-Reply-To and References headers and a subject (which has to
|
|
1537
|
+
* match the thread's; the reply tool builds "Re: <original subject>"). A
|
|
1538
|
+
* threadId without them is left out, and the message starts its own thread.
|
|
1539
|
+
*/
|
|
1540
|
+
threadFor(params) {
|
|
1541
|
+
const belongs = params.threadId && params.inReplyTo && params.references?.length && params.subject.trim();
|
|
1542
|
+
return belongs ? { threadId: params.threadId } : {};
|
|
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
|
+
}
|
|
1491
1579
|
/**
|
|
1492
1580
|
* Builds the base64url `raw` payload Gmail expects for send, draft create and
|
|
1493
1581
|
* draft update: a full MIME message with a text part, an HTML part when one
|
|
@@ -1648,6 +1736,35 @@ function buildGraphFilter(query) {
|
|
|
1648
1736
|
}
|
|
1649
1737
|
|
|
1650
1738
|
// src/providers/outlook/adapter.ts
|
|
1739
|
+
var toGraphAttachment = (att) => ({
|
|
1740
|
+
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
1741
|
+
name: att.filename,
|
|
1742
|
+
contentType: att.contentType,
|
|
1743
|
+
contentBytes: att.content.toString("base64")
|
|
1744
|
+
});
|
|
1745
|
+
var OUTLOOK_MAX_ATTACHMENT_BYTES = 3 * 1024 * 1024;
|
|
1746
|
+
var OUTLOOK_MAX_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
1747
|
+
var UPLOAD_SESSION_HINT = "Larger files need an upload session, which email-mcp does not implement for Outlook yet.";
|
|
1748
|
+
function megabytes(bytes) {
|
|
1749
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1750
|
+
}
|
|
1751
|
+
function assertOutlookAttachmentSizes(attachments) {
|
|
1752
|
+
if (!attachments?.length) return;
|
|
1753
|
+
const limit = megabytes(OUTLOOK_MAX_ATTACHMENT_BYTES);
|
|
1754
|
+
for (const att of attachments) {
|
|
1755
|
+
if (att.content.length > OUTLOOK_MAX_ATTACHMENT_BYTES) {
|
|
1756
|
+
throw new Error(`Outlook accepts attachments up to ${limit} through email-mcp; ${att.filename} is ${megabytes(att.content.length)}. ${UPLOAD_SESSION_HINT}`);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
function assertOutlookRequestSize(body) {
|
|
1761
|
+
const bytes = Buffer.byteLength(JSON.stringify(body));
|
|
1762
|
+
if (bytes > OUTLOOK_MAX_REQUEST_BYTES) {
|
|
1763
|
+
throw new Error(
|
|
1764
|
+
`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}`
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1651
1768
|
var OutlookAdapter = class _OutlookAdapter {
|
|
1652
1769
|
providerType = ProviderType.Outlook;
|
|
1653
1770
|
client = null;
|
|
@@ -1787,31 +1904,125 @@ var OutlookAdapter = class _OutlookAdapter {
|
|
|
1787
1904
|
meta: mapGraphAttachment(attachment)
|
|
1788
1905
|
};
|
|
1789
1906
|
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Graph only threads a reply (conversationId, subject, quoted history)
|
|
1909
|
+
* through the createReply flow: POST /me/messages and /me/sendMail always
|
|
1910
|
+
* start a new conversation whatever In-Reply-To says. Build the reply draft,
|
|
1911
|
+
* overlay our subject, body, recipients and attachments, and return its id.
|
|
1912
|
+
* Note the body replaces Graph's pre-filled quoted history.
|
|
1913
|
+
*
|
|
1914
|
+
* Each attachment goes up in a request of its own, so the sizes are checked
|
|
1915
|
+
* per file, and before createReply so that a file that is too big leaves no
|
|
1916
|
+
* draft behind. A draft that cannot be filled is deleted again. (A draft
|
|
1917
|
+
* whose send failed is kept, see `sendEmail`.)
|
|
1918
|
+
*/
|
|
1919
|
+
async createReplyDraft(params, replyToId) {
|
|
1920
|
+
const client = this.ensureClient();
|
|
1921
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
1922
|
+
const draft = await client.api(`/me/messages/${encodeURIComponent(replyToId)}/createReply`).post({});
|
|
1923
|
+
try {
|
|
1924
|
+
await this.fillReplyDraft(draft.id, params);
|
|
1925
|
+
} catch (error) {
|
|
1926
|
+
throw await this.discardDraft(draft.id, error);
|
|
1927
|
+
}
|
|
1928
|
+
return draft.id;
|
|
1929
|
+
}
|
|
1930
|
+
async fillReplyDraft(draftId, params) {
|
|
1931
|
+
const client = this.ensureClient();
|
|
1932
|
+
const toGraphRecipient = (c) => ({
|
|
1933
|
+
emailAddress: { name: c.name, address: c.email }
|
|
1934
|
+
});
|
|
1935
|
+
const patch = {
|
|
1936
|
+
// Always the caller's subject: Graph pre-fills its own "RE: ..." one.
|
|
1937
|
+
subject: params.subject,
|
|
1938
|
+
body: { contentType: params.body.html ? "html" : "text", content: params.body.html || params.body.text || "" }
|
|
1939
|
+
};
|
|
1940
|
+
if (params.to?.length) patch.toRecipients = params.to.map(toGraphRecipient);
|
|
1941
|
+
if (params.cc !== void 0) patch.ccRecipients = params.cc.map(toGraphRecipient);
|
|
1942
|
+
if (params.bcc !== void 0) patch.bccRecipients = params.bcc.map(toGraphRecipient);
|
|
1943
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(patch);
|
|
1944
|
+
for (const att of params.attachments ?? []) {
|
|
1945
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post(toGraphAttachment(att));
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
/**
|
|
1949
|
+
* Deletes a reply draft that could not be filled, so a failed reply leaves
|
|
1950
|
+
* nothing half-built in Drafts. Returns the error to throw: the original
|
|
1951
|
+
* one, or, when the delete fails as well, the original message with a note
|
|
1952
|
+
* that the draft is still there.
|
|
1953
|
+
*/
|
|
1954
|
+
async discardDraft(draftId, cause) {
|
|
1955
|
+
try {
|
|
1956
|
+
await this.ensureClient().api(`/me/messages/${encodeURIComponent(draftId)}`).delete();
|
|
1957
|
+
return cause;
|
|
1958
|
+
} catch (deleteError) {
|
|
1959
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1960
|
+
return new Error(
|
|
1961
|
+
`${message} (the reply draft ${draftId} could not be removed and was left in Drafts: ${deleteError?.message ?? deleteError})`,
|
|
1962
|
+
{ cause }
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1790
1966
|
async sendEmail(params) {
|
|
1791
1967
|
const client = this.ensureClient();
|
|
1968
|
+
if (params.replyToGraphId) {
|
|
1969
|
+
const draftId = await this.createReplyDraft(params, params.replyToGraphId);
|
|
1970
|
+
try {
|
|
1971
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}/send`).post({});
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
throw new Error(
|
|
1974
|
+
`${error?.message ?? error} (the reply was saved as draft ${draftId} in Drafts and was not confirmed as sent; check Sent Items before sending it again)`,
|
|
1975
|
+
{ cause: error }
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
return { id: draftId };
|
|
1979
|
+
}
|
|
1980
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
1792
1981
|
const message = this.buildGraphMessage(params);
|
|
1793
1982
|
const payload = { message };
|
|
1794
1983
|
if (params.attachments?.length) {
|
|
1795
|
-
payload.message.attachments = params.attachments.map(
|
|
1796
|
-
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
1797
|
-
name: att.filename,
|
|
1798
|
-
contentType: att.contentType,
|
|
1799
|
-
contentBytes: att.content.toString("base64")
|
|
1800
|
-
}));
|
|
1984
|
+
payload.message.attachments = params.attachments.map(toGraphAttachment);
|
|
1801
1985
|
}
|
|
1986
|
+
assertOutlookRequestSize(payload);
|
|
1802
1987
|
await client.api("/me/sendMail").post(payload);
|
|
1803
1988
|
return { id: `sent-${Date.now()}` };
|
|
1804
1989
|
}
|
|
1805
1990
|
async createDraft(params) {
|
|
1806
1991
|
const client = this.ensureClient();
|
|
1992
|
+
if (params.replyToGraphId) {
|
|
1993
|
+
return { id: await this.createReplyDraft(params, params.replyToGraphId) };
|
|
1994
|
+
}
|
|
1995
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
1807
1996
|
const message = this.buildGraphMessage(params);
|
|
1997
|
+
if (params.attachments?.length) {
|
|
1998
|
+
message.attachments = params.attachments.map(toGraphAttachment);
|
|
1999
|
+
}
|
|
2000
|
+
assertOutlookRequestSize(message);
|
|
1808
2001
|
const result = await client.api("/me/messages").post(message);
|
|
1809
2002
|
return { id: result.id };
|
|
1810
2003
|
}
|
|
1811
2004
|
async updateDraft(draftId, params) {
|
|
1812
2005
|
const client = this.ensureClient();
|
|
2006
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
1813
2007
|
const message = this.buildGraphMessage(params);
|
|
1814
2008
|
await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(message);
|
|
2009
|
+
if (params.attachments) {
|
|
2010
|
+
const attachmentsPath = `/me/messages/${encodeURIComponent(draftId)}/attachments`;
|
|
2011
|
+
const previousIds = [];
|
|
2012
|
+
let page = await client.api(attachmentsPath).select("id").get();
|
|
2013
|
+
for (; ; ) {
|
|
2014
|
+
previousIds.push(...(page.value || []).map((att) => att.id));
|
|
2015
|
+
const next = page["@odata.nextLink"];
|
|
2016
|
+
if (!next) break;
|
|
2017
|
+
page = await client.api(next).get();
|
|
2018
|
+
}
|
|
2019
|
+
for (const att of params.attachments) {
|
|
2020
|
+
await client.api(attachmentsPath).post(toGraphAttachment(att));
|
|
2021
|
+
}
|
|
2022
|
+
for (const id of previousIds) {
|
|
2023
|
+
await client.api(`${attachmentsPath}/${encodeURIComponent(id)}`).delete();
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
1815
2026
|
return { id: draftId };
|
|
1816
2027
|
}
|
|
1817
2028
|
async listDrafts(limit, offset) {
|
|
@@ -2182,6 +2393,12 @@ function mapImapFolder(imapFolder) {
|
|
|
2182
2393
|
unreadCount: imapFolder.status?.unseen
|
|
2183
2394
|
};
|
|
2184
2395
|
}
|
|
2396
|
+
function imapAttachmentId(att, index) {
|
|
2397
|
+
return att.contentId || `att-${index}`;
|
|
2398
|
+
}
|
|
2399
|
+
function headerValue(line) {
|
|
2400
|
+
return line.slice(line.indexOf(":") + 1).replace(/\r?\n[ \t]+/g, " ").trim();
|
|
2401
|
+
}
|
|
2185
2402
|
function mapParsedEmail(parsed, folder, accountId, uid) {
|
|
2186
2403
|
const mapContact = (addr) => ({
|
|
2187
2404
|
name: addr.name || void 0,
|
|
@@ -2192,7 +2409,7 @@ function mapParsedEmail(parsed, folder, accountId, uid) {
|
|
|
2192
2409
|
return addrs.value.map(mapContact);
|
|
2193
2410
|
};
|
|
2194
2411
|
const attachments = (parsed.attachments || []).map((att, i) => ({
|
|
2195
|
-
id: att
|
|
2412
|
+
id: imapAttachmentId(att, i),
|
|
2196
2413
|
filename: att.filename || `attachment-${i}`,
|
|
2197
2414
|
contentType: att.contentType || "application/octet-stream",
|
|
2198
2415
|
size: att.size || 0
|
|
@@ -2221,7 +2438,7 @@ function mapParsedEmail(parsed, folder, accountId, uid) {
|
|
|
2221
2438
|
flagged: parsed.flags?.has("\\Flagged") || false,
|
|
2222
2439
|
draft: parsed.flags?.has("\\Draft") || false
|
|
2223
2440
|
},
|
|
2224
|
-
headers: parsed.headerLines ? Object.fromEntries(parsed.headerLines.map((h) => [h.key, h.line])) : void 0
|
|
2441
|
+
headers: parsed.headerLines ? Object.fromEntries(parsed.headerLines.map((h) => [h.key, headerValue(h.line)])) : void 0
|
|
2225
2442
|
};
|
|
2226
2443
|
}
|
|
2227
2444
|
|
|
@@ -2618,7 +2835,7 @@ var ImapAdapter = class {
|
|
|
2618
2835
|
}
|
|
2619
2836
|
async getEmail(id, folder) {
|
|
2620
2837
|
const client = await this.ensureConnected();
|
|
2621
|
-
const targetFolder = folder
|
|
2838
|
+
const targetFolder = folder ? await this.resolveFolder(folder) : "INBOX";
|
|
2622
2839
|
let lock;
|
|
2623
2840
|
try {
|
|
2624
2841
|
lock = await client.getMailboxLock(targetFolder);
|
|
@@ -2637,13 +2854,20 @@ var ImapAdapter = class {
|
|
|
2637
2854
|
lock.release();
|
|
2638
2855
|
}
|
|
2639
2856
|
}
|
|
2640
|
-
|
|
2857
|
+
/**
|
|
2858
|
+
* IMAP has no thread object: the thread is the set of messages in ONE
|
|
2859
|
+
* folder whose Message-ID, References or In-Reply-To name `threadId`.
|
|
2860
|
+
* Messages of the same conversation in other folders (the own replies in
|
|
2861
|
+
* Sent, for example) are not part of the result.
|
|
2862
|
+
*/
|
|
2863
|
+
async getThread(threadId, sourceFolder) {
|
|
2641
2864
|
const client = await this.ensureConnected();
|
|
2865
|
+
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
2642
2866
|
let lock;
|
|
2643
2867
|
try {
|
|
2644
|
-
lock = await client.getMailboxLock(
|
|
2868
|
+
lock = await client.getMailboxLock(folder);
|
|
2645
2869
|
} catch (error) {
|
|
2646
|
-
throw formatImapError(error,
|
|
2870
|
+
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
2647
2871
|
}
|
|
2648
2872
|
try {
|
|
2649
2873
|
const searchResult = await client.search(
|
|
@@ -2651,13 +2875,13 @@ var ImapAdapter = class {
|
|
|
2651
2875
|
{ uid: true }
|
|
2652
2876
|
);
|
|
2653
2877
|
const uids = Array.isArray(searchResult) ? searchResult : [];
|
|
2654
|
-
if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
|
|
2878
|
+
if (uids.length === 0) throw new Error(`Thread ${threadId} not found in "${folder}"`);
|
|
2655
2879
|
const messages = [];
|
|
2656
2880
|
for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
|
|
2657
2881
|
if (!msg.source) continue;
|
|
2658
2882
|
const parsed = await simpleParser(msg.source);
|
|
2659
2883
|
parsed.flags = msg.flags;
|
|
2660
|
-
messages.push(mapParsedEmail(parsed,
|
|
2884
|
+
messages.push(mapParsedEmail(parsed, folder, this.accountId, msg.uid));
|
|
2661
2885
|
}
|
|
2662
2886
|
const participantMap = /* @__PURE__ */ new Map();
|
|
2663
2887
|
for (const msg of messages) {
|
|
@@ -2679,27 +2903,29 @@ var ImapAdapter = class {
|
|
|
2679
2903
|
lock.release();
|
|
2680
2904
|
}
|
|
2681
2905
|
}
|
|
2682
|
-
async getAttachment(emailId, attachmentId) {
|
|
2906
|
+
async getAttachment(emailId, attachmentId, sourceFolder) {
|
|
2683
2907
|
const client = await this.ensureConnected();
|
|
2908
|
+
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
2684
2909
|
let lock;
|
|
2685
2910
|
try {
|
|
2686
|
-
lock = await client.getMailboxLock(
|
|
2911
|
+
lock = await client.getMailboxLock(folder);
|
|
2687
2912
|
} catch (error) {
|
|
2688
|
-
throw formatImapError(error,
|
|
2913
|
+
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
2689
2914
|
}
|
|
2690
2915
|
try {
|
|
2691
2916
|
const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
|
|
2692
|
-
if (!msg) throw new Error(`Email ${emailId} not found`);
|
|
2917
|
+
if (!msg) throw new Error(`Email ${emailId} not found in "${folder}"`);
|
|
2693
2918
|
if (!msg.source) throw new Error(`Email ${emailId} has no source`);
|
|
2694
2919
|
const parsed = await simpleParser(msg.source);
|
|
2695
|
-
const
|
|
2696
|
-
|
|
2697
|
-
);
|
|
2698
|
-
if (
|
|
2920
|
+
const attachments = parsed.attachments || [];
|
|
2921
|
+
let index = attachments.findIndex((att, i) => imapAttachmentId(att, i) === attachmentId);
|
|
2922
|
+
if (index === -1) index = attachments.findIndex((att) => att.filename === attachmentId);
|
|
2923
|
+
if (index === -1) throw new Error(`Attachment ${attachmentId} not found in email ${emailId}`);
|
|
2924
|
+
const attachment = attachments[index];
|
|
2699
2925
|
return {
|
|
2700
2926
|
data: attachment.content,
|
|
2701
2927
|
meta: {
|
|
2702
|
-
id: attachment
|
|
2928
|
+
id: imapAttachmentId(attachment, index),
|
|
2703
2929
|
filename: attachment.filename || "attachment",
|
|
2704
2930
|
contentType: attachment.contentType || "application/octet-stream",
|
|
2705
2931
|
size: attachment.size || 0
|
|
@@ -2736,18 +2962,53 @@ var ImapAdapter = class {
|
|
|
2736
2962
|
* Drafts folder, and IMAP messages are immutable once appended. This
|
|
2737
2963
|
* deletes the old revision and appends a new one, so the returned `id`
|
|
2738
2964
|
* is a NEW uid; the old id no longer resolves after this call.
|
|
2965
|
+
*
|
|
2966
|
+
* A reply draft keeps its place in the thread: when the caller passes no
|
|
2967
|
+
* `inReplyTo`, the In-Reply-To and References headers of the old revision
|
|
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.
|
|
2739
2973
|
*/
|
|
2740
2974
|
async updateDraft(draftId, params, sourceFolder) {
|
|
2741
2975
|
const client = await this.ensureConnected();
|
|
2742
2976
|
const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
|
|
2743
|
-
const rawMessage = await this.buildDraftMessage(params);
|
|
2744
2977
|
let lock;
|
|
2745
2978
|
try {
|
|
2746
2979
|
lock = await client.getMailboxLock(draftsFolder);
|
|
2747
2980
|
} catch (error) {
|
|
2748
2981
|
throw formatImapError(error, `Failed to open folder "${draftsFolder}"`);
|
|
2749
2982
|
}
|
|
2983
|
+
let rawMessage;
|
|
2750
2984
|
try {
|
|
2985
|
+
let next = params;
|
|
2986
|
+
const keepThread = params.inReplyTo === void 0;
|
|
2987
|
+
const keepFiles = params.attachments === void 0;
|
|
2988
|
+
if (keepThread || keepFiles) {
|
|
2989
|
+
const old = await client.fetchOne(String(draftId), { source: true, uid: true }, { uid: true });
|
|
2990
|
+
if (old && old.source) {
|
|
2991
|
+
const parsed = await simpleParser(old.source);
|
|
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 };
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
rawMessage = await this.buildDraftMessage(next);
|
|
2751
3012
|
await client.messageDelete(draftId, { uid: true });
|
|
2752
3013
|
} finally {
|
|
2753
3014
|
lock.release();
|
|
@@ -3034,6 +3295,11 @@ var AccountManager = class {
|
|
|
3034
3295
|
connected: this.providers.has(c.id)
|
|
3035
3296
|
}));
|
|
3036
3297
|
}
|
|
3298
|
+
/** The account's own address, or undefined for an unknown account id. */
|
|
3299
|
+
async getAccountEmail(accountId) {
|
|
3300
|
+
const creds = this.credentials.get(accountId) ?? await this.store.get(accountId);
|
|
3301
|
+
return creds?.email;
|
|
3302
|
+
}
|
|
3037
3303
|
async getProvider(accountId) {
|
|
3038
3304
|
const existing = this.providers.get(accountId);
|
|
3039
3305
|
if (existing) {
|