@marlinjai/email-mcp 1.9.0 → 1.10.0
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 +523 -83
- package/dist/index.js.map +4 -4
- package/dist/setup/wizard.js +228 -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,41 @@ 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 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.
|
|
1118
|
+
*/
|
|
1119
|
+
async updateDraft(draftId, update) {
|
|
1106
1120
|
const gmail = this.ensureConnected();
|
|
1121
|
+
let params = update;
|
|
1122
|
+
if (update.threadId === void 0) {
|
|
1123
|
+
const existing = await gmail.users.drafts.get({
|
|
1124
|
+
userId: "me",
|
|
1125
|
+
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"
|
|
1129
|
+
});
|
|
1130
|
+
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] };
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1107
1138
|
const raw = await this.buildRaw(params);
|
|
1108
1139
|
const res = await gmail.users.drafts.update({
|
|
1109
1140
|
userId: "me",
|
|
1110
1141
|
id: draftId,
|
|
1111
1142
|
requestBody: {
|
|
1112
|
-
message: { raw }
|
|
1143
|
+
message: { raw, ...this.threadFor(params) }
|
|
1113
1144
|
}
|
|
1114
1145
|
});
|
|
1115
1146
|
return { id: res.data.id || draftId };
|
|
@@ -1488,6 +1519,16 @@ var GmailAdapter = class {
|
|
|
1488
1519
|
}
|
|
1489
1520
|
await this.deleteBlockRule(ruleId);
|
|
1490
1521
|
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Gmail files a message into an existing thread only when the threadId
|
|
1524
|
+
* comes with In-Reply-To and References headers and a subject (which has to
|
|
1525
|
+
* match the thread's; the reply tool builds "Re: <original subject>"). A
|
|
1526
|
+
* threadId without them is left out, and the message starts its own thread.
|
|
1527
|
+
*/
|
|
1528
|
+
threadFor(params) {
|
|
1529
|
+
const belongs = params.threadId && params.inReplyTo && params.references?.length && params.subject.trim();
|
|
1530
|
+
return belongs ? { threadId: params.threadId } : {};
|
|
1531
|
+
}
|
|
1491
1532
|
/**
|
|
1492
1533
|
* Builds the base64url `raw` payload Gmail expects for send, draft create and
|
|
1493
1534
|
* draft update: a full MIME message with a text part, an HTML part when one
|
|
@@ -1648,6 +1689,35 @@ function buildGraphFilter(query) {
|
|
|
1648
1689
|
}
|
|
1649
1690
|
|
|
1650
1691
|
// src/providers/outlook/adapter.ts
|
|
1692
|
+
var toGraphAttachment = (att) => ({
|
|
1693
|
+
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
1694
|
+
name: att.filename,
|
|
1695
|
+
contentType: att.contentType,
|
|
1696
|
+
contentBytes: att.content.toString("base64")
|
|
1697
|
+
});
|
|
1698
|
+
var OUTLOOK_MAX_ATTACHMENT_BYTES = 3 * 1024 * 1024;
|
|
1699
|
+
var OUTLOOK_MAX_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
1700
|
+
var UPLOAD_SESSION_HINT = "Larger files need an upload session, which email-mcp does not implement for Outlook yet.";
|
|
1701
|
+
function megabytes(bytes) {
|
|
1702
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1703
|
+
}
|
|
1704
|
+
function assertOutlookAttachmentSizes(attachments) {
|
|
1705
|
+
if (!attachments?.length) return;
|
|
1706
|
+
const limit = megabytes(OUTLOOK_MAX_ATTACHMENT_BYTES);
|
|
1707
|
+
for (const att of attachments) {
|
|
1708
|
+
if (att.content.length > OUTLOOK_MAX_ATTACHMENT_BYTES) {
|
|
1709
|
+
throw new Error(`Outlook accepts attachments up to ${limit} through email-mcp; ${att.filename} is ${megabytes(att.content.length)}. ${UPLOAD_SESSION_HINT}`);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
function assertOutlookRequestSize(body) {
|
|
1714
|
+
const bytes = Buffer.byteLength(JSON.stringify(body));
|
|
1715
|
+
if (bytes > OUTLOOK_MAX_REQUEST_BYTES) {
|
|
1716
|
+
throw new Error(
|
|
1717
|
+
`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}`
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1651
1721
|
var OutlookAdapter = class _OutlookAdapter {
|
|
1652
1722
|
providerType = ProviderType.Outlook;
|
|
1653
1723
|
client = null;
|
|
@@ -1787,31 +1857,125 @@ var OutlookAdapter = class _OutlookAdapter {
|
|
|
1787
1857
|
meta: mapGraphAttachment(attachment)
|
|
1788
1858
|
};
|
|
1789
1859
|
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Graph only threads a reply (conversationId, subject, quoted history)
|
|
1862
|
+
* through the createReply flow: POST /me/messages and /me/sendMail always
|
|
1863
|
+
* start a new conversation whatever In-Reply-To says. Build the reply draft,
|
|
1864
|
+
* overlay our subject, body, recipients and attachments, and return its id.
|
|
1865
|
+
* Note the body replaces Graph's pre-filled quoted history.
|
|
1866
|
+
*
|
|
1867
|
+
* Each attachment goes up in a request of its own, so the sizes are checked
|
|
1868
|
+
* per file, and before createReply so that a file that is too big leaves no
|
|
1869
|
+
* draft behind. A draft that cannot be filled is deleted again. (A draft
|
|
1870
|
+
* whose send failed is kept, see `sendEmail`.)
|
|
1871
|
+
*/
|
|
1872
|
+
async createReplyDraft(params, replyToId) {
|
|
1873
|
+
const client = this.ensureClient();
|
|
1874
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
1875
|
+
const draft = await client.api(`/me/messages/${encodeURIComponent(replyToId)}/createReply`).post({});
|
|
1876
|
+
try {
|
|
1877
|
+
await this.fillReplyDraft(draft.id, params);
|
|
1878
|
+
} catch (error) {
|
|
1879
|
+
throw await this.discardDraft(draft.id, error);
|
|
1880
|
+
}
|
|
1881
|
+
return draft.id;
|
|
1882
|
+
}
|
|
1883
|
+
async fillReplyDraft(draftId, params) {
|
|
1884
|
+
const client = this.ensureClient();
|
|
1885
|
+
const toGraphRecipient = (c) => ({
|
|
1886
|
+
emailAddress: { name: c.name, address: c.email }
|
|
1887
|
+
});
|
|
1888
|
+
const patch = {
|
|
1889
|
+
// Always the caller's subject: Graph pre-fills its own "RE: ..." one.
|
|
1890
|
+
subject: params.subject,
|
|
1891
|
+
body: { contentType: params.body.html ? "html" : "text", content: params.body.html || params.body.text || "" }
|
|
1892
|
+
};
|
|
1893
|
+
if (params.to?.length) patch.toRecipients = params.to.map(toGraphRecipient);
|
|
1894
|
+
if (params.cc !== void 0) patch.ccRecipients = params.cc.map(toGraphRecipient);
|
|
1895
|
+
if (params.bcc !== void 0) patch.bccRecipients = params.bcc.map(toGraphRecipient);
|
|
1896
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(patch);
|
|
1897
|
+
for (const att of params.attachments ?? []) {
|
|
1898
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post(toGraphAttachment(att));
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Deletes a reply draft that could not be filled, so a failed reply leaves
|
|
1903
|
+
* nothing half-built in Drafts. Returns the error to throw: the original
|
|
1904
|
+
* one, or, when the delete fails as well, the original message with a note
|
|
1905
|
+
* that the draft is still there.
|
|
1906
|
+
*/
|
|
1907
|
+
async discardDraft(draftId, cause) {
|
|
1908
|
+
try {
|
|
1909
|
+
await this.ensureClient().api(`/me/messages/${encodeURIComponent(draftId)}`).delete();
|
|
1910
|
+
return cause;
|
|
1911
|
+
} catch (deleteError) {
|
|
1912
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1913
|
+
return new Error(
|
|
1914
|
+
`${message} (the reply draft ${draftId} could not be removed and was left in Drafts: ${deleteError?.message ?? deleteError})`,
|
|
1915
|
+
{ cause }
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1790
1919
|
async sendEmail(params) {
|
|
1791
1920
|
const client = this.ensureClient();
|
|
1921
|
+
if (params.replyToGraphId) {
|
|
1922
|
+
const draftId = await this.createReplyDraft(params, params.replyToGraphId);
|
|
1923
|
+
try {
|
|
1924
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}/send`).post({});
|
|
1925
|
+
} catch (error) {
|
|
1926
|
+
throw new Error(
|
|
1927
|
+
`${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)`,
|
|
1928
|
+
{ cause: error }
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
return { id: draftId };
|
|
1932
|
+
}
|
|
1933
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
1792
1934
|
const message = this.buildGraphMessage(params);
|
|
1793
1935
|
const payload = { message };
|
|
1794
1936
|
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
|
-
}));
|
|
1937
|
+
payload.message.attachments = params.attachments.map(toGraphAttachment);
|
|
1801
1938
|
}
|
|
1939
|
+
assertOutlookRequestSize(payload);
|
|
1802
1940
|
await client.api("/me/sendMail").post(payload);
|
|
1803
1941
|
return { id: `sent-${Date.now()}` };
|
|
1804
1942
|
}
|
|
1805
1943
|
async createDraft(params) {
|
|
1806
1944
|
const client = this.ensureClient();
|
|
1945
|
+
if (params.replyToGraphId) {
|
|
1946
|
+
return { id: await this.createReplyDraft(params, params.replyToGraphId) };
|
|
1947
|
+
}
|
|
1948
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
1807
1949
|
const message = this.buildGraphMessage(params);
|
|
1950
|
+
if (params.attachments?.length) {
|
|
1951
|
+
message.attachments = params.attachments.map(toGraphAttachment);
|
|
1952
|
+
}
|
|
1953
|
+
assertOutlookRequestSize(message);
|
|
1808
1954
|
const result = await client.api("/me/messages").post(message);
|
|
1809
1955
|
return { id: result.id };
|
|
1810
1956
|
}
|
|
1811
1957
|
async updateDraft(draftId, params) {
|
|
1812
1958
|
const client = this.ensureClient();
|
|
1959
|
+
assertOutlookAttachmentSizes(params.attachments);
|
|
1813
1960
|
const message = this.buildGraphMessage(params);
|
|
1814
1961
|
await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(message);
|
|
1962
|
+
if (params.attachments) {
|
|
1963
|
+
const attachmentsPath = `/me/messages/${encodeURIComponent(draftId)}/attachments`;
|
|
1964
|
+
const previousIds = [];
|
|
1965
|
+
let page = await client.api(attachmentsPath).select("id").get();
|
|
1966
|
+
for (; ; ) {
|
|
1967
|
+
previousIds.push(...(page.value || []).map((att) => att.id));
|
|
1968
|
+
const next = page["@odata.nextLink"];
|
|
1969
|
+
if (!next) break;
|
|
1970
|
+
page = await client.api(next).get();
|
|
1971
|
+
}
|
|
1972
|
+
for (const att of params.attachments) {
|
|
1973
|
+
await client.api(attachmentsPath).post(toGraphAttachment(att));
|
|
1974
|
+
}
|
|
1975
|
+
for (const id of previousIds) {
|
|
1976
|
+
await client.api(`${attachmentsPath}/${encodeURIComponent(id)}`).delete();
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1815
1979
|
return { id: draftId };
|
|
1816
1980
|
}
|
|
1817
1981
|
async listDrafts(limit, offset) {
|
|
@@ -2182,6 +2346,12 @@ function mapImapFolder(imapFolder) {
|
|
|
2182
2346
|
unreadCount: imapFolder.status?.unseen
|
|
2183
2347
|
};
|
|
2184
2348
|
}
|
|
2349
|
+
function imapAttachmentId(att, index) {
|
|
2350
|
+
return att.contentId || `att-${index}`;
|
|
2351
|
+
}
|
|
2352
|
+
function headerValue(line) {
|
|
2353
|
+
return line.slice(line.indexOf(":") + 1).replace(/\r?\n[ \t]+/g, " ").trim();
|
|
2354
|
+
}
|
|
2185
2355
|
function mapParsedEmail(parsed, folder, accountId, uid) {
|
|
2186
2356
|
const mapContact = (addr) => ({
|
|
2187
2357
|
name: addr.name || void 0,
|
|
@@ -2192,7 +2362,7 @@ function mapParsedEmail(parsed, folder, accountId, uid) {
|
|
|
2192
2362
|
return addrs.value.map(mapContact);
|
|
2193
2363
|
};
|
|
2194
2364
|
const attachments = (parsed.attachments || []).map((att, i) => ({
|
|
2195
|
-
id: att
|
|
2365
|
+
id: imapAttachmentId(att, i),
|
|
2196
2366
|
filename: att.filename || `attachment-${i}`,
|
|
2197
2367
|
contentType: att.contentType || "application/octet-stream",
|
|
2198
2368
|
size: att.size || 0
|
|
@@ -2221,7 +2391,7 @@ function mapParsedEmail(parsed, folder, accountId, uid) {
|
|
|
2221
2391
|
flagged: parsed.flags?.has("\\Flagged") || false,
|
|
2222
2392
|
draft: parsed.flags?.has("\\Draft") || false
|
|
2223
2393
|
},
|
|
2224
|
-
headers: parsed.headerLines ? Object.fromEntries(parsed.headerLines.map((h) => [h.key, h.line])) : void 0
|
|
2394
|
+
headers: parsed.headerLines ? Object.fromEntries(parsed.headerLines.map((h) => [h.key, headerValue(h.line)])) : void 0
|
|
2225
2395
|
};
|
|
2226
2396
|
}
|
|
2227
2397
|
|
|
@@ -2618,7 +2788,7 @@ var ImapAdapter = class {
|
|
|
2618
2788
|
}
|
|
2619
2789
|
async getEmail(id, folder) {
|
|
2620
2790
|
const client = await this.ensureConnected();
|
|
2621
|
-
const targetFolder = folder
|
|
2791
|
+
const targetFolder = folder ? await this.resolveFolder(folder) : "INBOX";
|
|
2622
2792
|
let lock;
|
|
2623
2793
|
try {
|
|
2624
2794
|
lock = await client.getMailboxLock(targetFolder);
|
|
@@ -2637,13 +2807,20 @@ var ImapAdapter = class {
|
|
|
2637
2807
|
lock.release();
|
|
2638
2808
|
}
|
|
2639
2809
|
}
|
|
2640
|
-
|
|
2810
|
+
/**
|
|
2811
|
+
* IMAP has no thread object: the thread is the set of messages in ONE
|
|
2812
|
+
* folder whose Message-ID, References or In-Reply-To name `threadId`.
|
|
2813
|
+
* Messages of the same conversation in other folders (the own replies in
|
|
2814
|
+
* Sent, for example) are not part of the result.
|
|
2815
|
+
*/
|
|
2816
|
+
async getThread(threadId, sourceFolder) {
|
|
2641
2817
|
const client = await this.ensureConnected();
|
|
2818
|
+
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
2642
2819
|
let lock;
|
|
2643
2820
|
try {
|
|
2644
|
-
lock = await client.getMailboxLock(
|
|
2821
|
+
lock = await client.getMailboxLock(folder);
|
|
2645
2822
|
} catch (error) {
|
|
2646
|
-
throw formatImapError(error,
|
|
2823
|
+
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
2647
2824
|
}
|
|
2648
2825
|
try {
|
|
2649
2826
|
const searchResult = await client.search(
|
|
@@ -2651,13 +2828,13 @@ var ImapAdapter = class {
|
|
|
2651
2828
|
{ uid: true }
|
|
2652
2829
|
);
|
|
2653
2830
|
const uids = Array.isArray(searchResult) ? searchResult : [];
|
|
2654
|
-
if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
|
|
2831
|
+
if (uids.length === 0) throw new Error(`Thread ${threadId} not found in "${folder}"`);
|
|
2655
2832
|
const messages = [];
|
|
2656
2833
|
for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
|
|
2657
2834
|
if (!msg.source) continue;
|
|
2658
2835
|
const parsed = await simpleParser(msg.source);
|
|
2659
2836
|
parsed.flags = msg.flags;
|
|
2660
|
-
messages.push(mapParsedEmail(parsed,
|
|
2837
|
+
messages.push(mapParsedEmail(parsed, folder, this.accountId, msg.uid));
|
|
2661
2838
|
}
|
|
2662
2839
|
const participantMap = /* @__PURE__ */ new Map();
|
|
2663
2840
|
for (const msg of messages) {
|
|
@@ -2679,27 +2856,29 @@ var ImapAdapter = class {
|
|
|
2679
2856
|
lock.release();
|
|
2680
2857
|
}
|
|
2681
2858
|
}
|
|
2682
|
-
async getAttachment(emailId, attachmentId) {
|
|
2859
|
+
async getAttachment(emailId, attachmentId, sourceFolder) {
|
|
2683
2860
|
const client = await this.ensureConnected();
|
|
2861
|
+
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
2684
2862
|
let lock;
|
|
2685
2863
|
try {
|
|
2686
|
-
lock = await client.getMailboxLock(
|
|
2864
|
+
lock = await client.getMailboxLock(folder);
|
|
2687
2865
|
} catch (error) {
|
|
2688
|
-
throw formatImapError(error,
|
|
2866
|
+
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
2689
2867
|
}
|
|
2690
2868
|
try {
|
|
2691
2869
|
const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
|
|
2692
|
-
if (!msg) throw new Error(`Email ${emailId} not found`);
|
|
2870
|
+
if (!msg) throw new Error(`Email ${emailId} not found in "${folder}"`);
|
|
2693
2871
|
if (!msg.source) throw new Error(`Email ${emailId} has no source`);
|
|
2694
2872
|
const parsed = await simpleParser(msg.source);
|
|
2695
|
-
const
|
|
2696
|
-
|
|
2697
|
-
);
|
|
2698
|
-
if (
|
|
2873
|
+
const attachments = parsed.attachments || [];
|
|
2874
|
+
let index = attachments.findIndex((att, i) => imapAttachmentId(att, i) === attachmentId);
|
|
2875
|
+
if (index === -1) index = attachments.findIndex((att) => att.filename === attachmentId);
|
|
2876
|
+
if (index === -1) throw new Error(`Attachment ${attachmentId} not found in email ${emailId}`);
|
|
2877
|
+
const attachment = attachments[index];
|
|
2699
2878
|
return {
|
|
2700
2879
|
data: attachment.content,
|
|
2701
2880
|
meta: {
|
|
2702
|
-
id: attachment
|
|
2881
|
+
id: imapAttachmentId(attachment, index),
|
|
2703
2882
|
filename: attachment.filename || "attachment",
|
|
2704
2883
|
contentType: attachment.contentType || "application/octet-stream",
|
|
2705
2884
|
size: attachment.size || 0
|
|
@@ -2736,18 +2915,34 @@ var ImapAdapter = class {
|
|
|
2736
2915
|
* Drafts folder, and IMAP messages are immutable once appended. This
|
|
2737
2916
|
* deletes the old revision and appends a new one, so the returned `id`
|
|
2738
2917
|
* is a NEW uid; the old id no longer resolves after this call.
|
|
2918
|
+
*
|
|
2919
|
+
* A reply draft keeps its place in the thread: when the caller passes no
|
|
2920
|
+
* `inReplyTo`, the In-Reply-To and References headers of the old revision
|
|
2921
|
+
* are copied into the new one.
|
|
2739
2922
|
*/
|
|
2740
2923
|
async updateDraft(draftId, params, sourceFolder) {
|
|
2741
2924
|
const client = await this.ensureConnected();
|
|
2742
2925
|
const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
|
|
2743
|
-
const rawMessage = await this.buildDraftMessage(params);
|
|
2744
2926
|
let lock;
|
|
2745
2927
|
try {
|
|
2746
2928
|
lock = await client.getMailboxLock(draftsFolder);
|
|
2747
2929
|
} catch (error) {
|
|
2748
2930
|
throw formatImapError(error, `Failed to open folder "${draftsFolder}"`);
|
|
2749
2931
|
}
|
|
2932
|
+
let rawMessage;
|
|
2750
2933
|
try {
|
|
2934
|
+
let next = params;
|
|
2935
|
+
if (params.inReplyTo === void 0) {
|
|
2936
|
+
const old = await client.fetchOne(String(draftId), { source: true, uid: true }, { uid: true });
|
|
2937
|
+
if (old && old.source) {
|
|
2938
|
+
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 };
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
rawMessage = await this.buildDraftMessage(next);
|
|
2751
2946
|
await client.messageDelete(draftId, { uid: true });
|
|
2752
2947
|
} finally {
|
|
2753
2948
|
lock.release();
|
|
@@ -3034,6 +3229,11 @@ var AccountManager = class {
|
|
|
3034
3229
|
connected: this.providers.has(c.id)
|
|
3035
3230
|
}));
|
|
3036
3231
|
}
|
|
3232
|
+
/** The account's own address, or undefined for an unknown account id. */
|
|
3233
|
+
async getAccountEmail(accountId) {
|
|
3234
|
+
const creds = this.credentials.get(accountId) ?? await this.store.get(accountId);
|
|
3235
|
+
return creds?.email;
|
|
3236
|
+
}
|
|
3037
3237
|
async getProvider(accountId) {
|
|
3038
3238
|
const existing = this.providers.get(accountId);
|
|
3039
3239
|
if (existing) {
|