@marlinjai/email-mcp 1.4.3 → 1.5.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 +23 -3
- package/dist/index.js +304 -120
- package/dist/index.js.map +3 -3
- package/dist/setup/wizard.js +229 -92
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +1 -1
package/dist/setup/wizard.js
CHANGED
|
@@ -108,6 +108,7 @@ function askPassword(message) {
|
|
|
108
108
|
|
|
109
109
|
// src/auth/credential-store.ts
|
|
110
110
|
import crypto from "node:crypto";
|
|
111
|
+
import { execFileSync } from "node:child_process";
|
|
111
112
|
import fs from "node:fs";
|
|
112
113
|
import path from "node:path";
|
|
113
114
|
import os from "node:os";
|
|
@@ -116,15 +117,59 @@ var KEY_LENGTH = 32;
|
|
|
116
117
|
var IV_LENGTH = 16;
|
|
117
118
|
var SALT_LENGTH = 32;
|
|
118
119
|
var PBKDF2_ITERATIONS = 1e5;
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
var cachedMachineId;
|
|
121
|
+
function getStableMachineId() {
|
|
122
|
+
if (cachedMachineId !== void 0) return cachedMachineId;
|
|
123
|
+
cachedMachineId = computeStableMachineId();
|
|
124
|
+
return cachedMachineId;
|
|
121
125
|
}
|
|
122
|
-
function
|
|
123
|
-
|
|
126
|
+
function computeStableMachineId() {
|
|
127
|
+
try {
|
|
128
|
+
if (process.platform === "darwin") {
|
|
129
|
+
const out = execFileSync("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
130
|
+
encoding: "utf-8"
|
|
131
|
+
});
|
|
132
|
+
const match = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
133
|
+
return match ? match[1] : null;
|
|
134
|
+
}
|
|
135
|
+
if (process.platform === "linux") {
|
|
136
|
+
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
137
|
+
if (fs.existsSync(p)) {
|
|
138
|
+
const id = fs.readFileSync(p, "utf-8").trim();
|
|
139
|
+
if (id) return id;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (process.platform === "win32") {
|
|
145
|
+
const out = execFileSync(
|
|
146
|
+
"reg",
|
|
147
|
+
["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"],
|
|
148
|
+
{ encoding: "utf-8" }
|
|
149
|
+
);
|
|
150
|
+
const match = out.match(/MachineGuid\s+REG_SZ\s+([A-Za-z0-9-]+)/);
|
|
151
|
+
return match ? match[1] : null;
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
function getSeedCandidates() {
|
|
158
|
+
const seeds = [];
|
|
159
|
+
const username = os.userInfo().username;
|
|
160
|
+
const envKey = process.env.EMAIL_MCP_KEY;
|
|
161
|
+
if (envKey) seeds.push(`email-mcp:env:${envKey}`);
|
|
162
|
+
const machineId = getStableMachineId();
|
|
163
|
+
if (machineId) seeds.push(`email-mcp:machine:${machineId}:${username}`);
|
|
164
|
+
seeds.push(`email-mcp:${os.hostname()}:${username}`);
|
|
165
|
+
return seeds;
|
|
124
166
|
}
|
|
125
|
-
function
|
|
167
|
+
function deriveKey(seed, salt) {
|
|
168
|
+
return crypto.pbkdf2Sync(seed, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
169
|
+
}
|
|
170
|
+
function encrypt(plaintext, seed) {
|
|
126
171
|
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
127
|
-
const key = deriveKey(salt);
|
|
172
|
+
const key = deriveKey(seed, salt);
|
|
128
173
|
const iv = crypto.randomBytes(IV_LENGTH);
|
|
129
174
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
130
175
|
let encrypted = cipher.update(plaintext, "utf-8", "hex");
|
|
@@ -137,14 +182,25 @@ function encrypt(plaintext) {
|
|
|
137
182
|
data: encrypted
|
|
138
183
|
});
|
|
139
184
|
}
|
|
140
|
-
function decrypt(ciphertext) {
|
|
185
|
+
function decrypt(ciphertext, seeds) {
|
|
141
186
|
const { salt, iv, authTag, data } = JSON.parse(ciphertext);
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
let
|
|
146
|
-
|
|
147
|
-
|
|
187
|
+
const saltBuf = Buffer.from(salt, "hex");
|
|
188
|
+
const ivBuf = Buffer.from(iv, "hex");
|
|
189
|
+
const authTagBuf = Buffer.from(authTag, "hex");
|
|
190
|
+
let lastError;
|
|
191
|
+
for (const seed of seeds) {
|
|
192
|
+
try {
|
|
193
|
+
const key = deriveKey(seed, saltBuf);
|
|
194
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, ivBuf);
|
|
195
|
+
decipher.setAuthTag(authTagBuf);
|
|
196
|
+
let decrypted = decipher.update(data, "hex", "utf-8");
|
|
197
|
+
decrypted += decipher.final("utf-8");
|
|
198
|
+
return { plaintext: decrypted, seedUsed: seed };
|
|
199
|
+
} catch (err) {
|
|
200
|
+
lastError = err;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
throw lastError ?? new Error("Unable to decrypt credentials");
|
|
148
204
|
}
|
|
149
205
|
var CredentialStore = class {
|
|
150
206
|
filePath;
|
|
@@ -160,12 +216,20 @@ var CredentialStore = class {
|
|
|
160
216
|
return { accounts: {} };
|
|
161
217
|
}
|
|
162
218
|
const raw = fs.readFileSync(this.filePath, "utf-8");
|
|
163
|
-
const
|
|
164
|
-
|
|
219
|
+
const seeds = getSeedCandidates();
|
|
220
|
+
const { plaintext, seedUsed } = decrypt(raw, seeds);
|
|
221
|
+
const data = JSON.parse(plaintext);
|
|
222
|
+
if (seedUsed !== seeds[0]) {
|
|
223
|
+
try {
|
|
224
|
+
this.write(data);
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return data;
|
|
165
229
|
}
|
|
166
230
|
write(data) {
|
|
167
231
|
const json = JSON.stringify(data);
|
|
168
|
-
const encrypted = encrypt(json);
|
|
232
|
+
const encrypted = encrypt(json, getSeedCandidates()[0]);
|
|
169
233
|
fs.writeFileSync(this.filePath, encrypted, { mode: 384 });
|
|
170
234
|
}
|
|
171
235
|
async save(creds) {
|
|
@@ -1037,7 +1101,7 @@ var GmailAdapter = class {
|
|
|
1037
1101
|
criteria.query = rule.value;
|
|
1038
1102
|
break;
|
|
1039
1103
|
}
|
|
1040
|
-
const action = rule.action === "delete" ? { addLabelIds: ["TRASH"] } : {
|
|
1104
|
+
const action = rule.action === "delete" ? { addLabelIds: ["TRASH"] } : { removeLabelIds: ["INBOX"] };
|
|
1041
1105
|
const res = await gmail.users.settings.filters.create({
|
|
1042
1106
|
userId: "me",
|
|
1043
1107
|
requestBody: { criteria, action }
|
|
@@ -1823,6 +1887,7 @@ var ImapAdapter = class {
|
|
|
1823
1887
|
accountId = "";
|
|
1824
1888
|
email = "";
|
|
1825
1889
|
passwordCreds = null;
|
|
1890
|
+
connectingPromise = null;
|
|
1826
1891
|
async connect(credentials) {
|
|
1827
1892
|
if (!credentials.password) {
|
|
1828
1893
|
throw new Error("IMAP adapter requires password credentials");
|
|
@@ -1830,23 +1895,87 @@ var ImapAdapter = class {
|
|
|
1830
1895
|
this.accountId = credentials.id;
|
|
1831
1896
|
this.email = credentials.email;
|
|
1832
1897
|
this.passwordCreds = credentials.password;
|
|
1833
|
-
this.
|
|
1834
|
-
host: credentials.password.host,
|
|
1835
|
-
port: credentials.password.port,
|
|
1836
|
-
secure: credentials.password.tls,
|
|
1837
|
-
auth: {
|
|
1838
|
-
user: credentials.email,
|
|
1839
|
-
pass: credentials.password.password
|
|
1840
|
-
},
|
|
1841
|
-
logger: false
|
|
1842
|
-
});
|
|
1843
|
-
await this.client.connect();
|
|
1898
|
+
await this.ensureConnected();
|
|
1844
1899
|
}
|
|
1845
1900
|
async disconnect() {
|
|
1846
1901
|
if (this.client) {
|
|
1847
|
-
|
|
1902
|
+
try {
|
|
1903
|
+
await this.client.logout();
|
|
1904
|
+
} catch {
|
|
1905
|
+
}
|
|
1848
1906
|
this.client = null;
|
|
1849
1907
|
}
|
|
1908
|
+
this.passwordCreds = null;
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Builds a fresh ImapFlow client from stored credentials and wires up
|
|
1912
|
+
* close/error listeners so a dropped socket clears `this.client`, forcing
|
|
1913
|
+
* the next `ensureConnected()` call to reconnect.
|
|
1914
|
+
*/
|
|
1915
|
+
buildClient() {
|
|
1916
|
+
if (!this.passwordCreds) {
|
|
1917
|
+
throw new Error("Cannot build IMAP client without stored credentials");
|
|
1918
|
+
}
|
|
1919
|
+
const client = new ImapFlow({
|
|
1920
|
+
host: this.passwordCreds.host,
|
|
1921
|
+
port: this.passwordCreds.port,
|
|
1922
|
+
secure: this.passwordCreds.tls,
|
|
1923
|
+
auth: { user: this.email, pass: this.passwordCreds.password },
|
|
1924
|
+
logger: false
|
|
1925
|
+
});
|
|
1926
|
+
const drop = () => {
|
|
1927
|
+
if (this.client === client) this.client = null;
|
|
1928
|
+
};
|
|
1929
|
+
client.on("close", drop);
|
|
1930
|
+
client.on("error", drop);
|
|
1931
|
+
return client;
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Returns true while the underlying ImapFlow socket is healthy.
|
|
1935
|
+
* ImapFlow sets `usable=false` when the connection is closed, in the
|
|
1936
|
+
* middle of logging out, or otherwise unsafe to issue commands on.
|
|
1937
|
+
*/
|
|
1938
|
+
isClientUsable() {
|
|
1939
|
+
return !!this.client && this.client.usable === true;
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Guarantees a usable IMAP client and returns it. Reconnects transparently
|
|
1943
|
+
* if the previous client was dropped (idle timeout, server-side disconnect,
|
|
1944
|
+
* network flap). Concurrent callers share a single in-flight reconnect to
|
|
1945
|
+
* avoid duplicate sessions.
|
|
1946
|
+
*
|
|
1947
|
+
* Callers should bind the result to a local `const client` and use that
|
|
1948
|
+
* within the method instead of `this.client`, so a `'close'` event mid-call
|
|
1949
|
+
* does not race the active operation.
|
|
1950
|
+
*/
|
|
1951
|
+
async ensureConnected() {
|
|
1952
|
+
if (this.connectingPromise) {
|
|
1953
|
+
await this.connectingPromise;
|
|
1954
|
+
}
|
|
1955
|
+
if (!this.isClientUsable()) {
|
|
1956
|
+
if (!this.passwordCreds) {
|
|
1957
|
+
throw new Error("Not connected");
|
|
1958
|
+
}
|
|
1959
|
+
this.connectingPromise = (async () => {
|
|
1960
|
+
if (this.client) {
|
|
1961
|
+
try {
|
|
1962
|
+
await this.client.logout();
|
|
1963
|
+
} catch {
|
|
1964
|
+
}
|
|
1965
|
+
this.client = null;
|
|
1966
|
+
}
|
|
1967
|
+
const client = this.buildClient();
|
|
1968
|
+
await client.connect();
|
|
1969
|
+
this.client = client;
|
|
1970
|
+
})().finally(() => {
|
|
1971
|
+
this.connectingPromise = null;
|
|
1972
|
+
});
|
|
1973
|
+
await this.connectingPromise;
|
|
1974
|
+
}
|
|
1975
|
+
if (!this.client) {
|
|
1976
|
+
throw new Error("IMAP client unavailable after reconnect attempt");
|
|
1977
|
+
}
|
|
1978
|
+
return this.client;
|
|
1850
1979
|
}
|
|
1851
1980
|
async testConnection() {
|
|
1852
1981
|
try {
|
|
@@ -1857,14 +1986,14 @@ var ImapAdapter = class {
|
|
|
1857
1986
|
}
|
|
1858
1987
|
}
|
|
1859
1988
|
async listFolders() {
|
|
1860
|
-
|
|
1861
|
-
const imapFolders = await
|
|
1989
|
+
const client = await this.ensureConnected();
|
|
1990
|
+
const imapFolders = await client.list();
|
|
1862
1991
|
return imapFolders.map(mapImapFolder);
|
|
1863
1992
|
}
|
|
1864
1993
|
async createFolder(name, parentPath) {
|
|
1865
|
-
|
|
1994
|
+
const client = await this.ensureConnected();
|
|
1866
1995
|
const fullPath = parentPath ? `${parentPath}/${name}` : name;
|
|
1867
|
-
const result = await
|
|
1996
|
+
const result = await client.mailboxCreate(fullPath);
|
|
1868
1997
|
return {
|
|
1869
1998
|
id: result.path,
|
|
1870
1999
|
name: result.name || name,
|
|
@@ -1893,7 +2022,7 @@ var ImapAdapter = class {
|
|
|
1893
2022
|
* and matching by name or special-use type.
|
|
1894
2023
|
*/
|
|
1895
2024
|
async resolveFolder(folder) {
|
|
1896
|
-
|
|
2025
|
+
const client = await this.ensureConnected();
|
|
1897
2026
|
const FOLDER_ALIASES = {
|
|
1898
2027
|
junk: ["Junk", "Spam", "Bulk Mail", "Junk E-mail", "Junk Email"],
|
|
1899
2028
|
spam: ["Junk", "Spam", "Bulk Mail", "Junk E-mail", "Junk Email"],
|
|
@@ -1902,7 +2031,7 @@ var ImapAdapter = class {
|
|
|
1902
2031
|
drafts: ["Drafts", "Draft"],
|
|
1903
2032
|
archive: ["Archive", "All Mail"]
|
|
1904
2033
|
};
|
|
1905
|
-
const folders = await
|
|
2034
|
+
const folders = await client.list();
|
|
1906
2035
|
const lowerFolder = folder.toLowerCase();
|
|
1907
2036
|
const exactMatch = folders.find(
|
|
1908
2037
|
(f) => f.path === folder || f.path.toLowerCase() === lowerFolder
|
|
@@ -1940,7 +2069,7 @@ var ImapAdapter = class {
|
|
|
1940
2069
|
return folder;
|
|
1941
2070
|
}
|
|
1942
2071
|
async search(query) {
|
|
1943
|
-
|
|
2072
|
+
const client = await this.ensureConnected();
|
|
1944
2073
|
const requestedFolder = query.folder || "INBOX";
|
|
1945
2074
|
let folder;
|
|
1946
2075
|
try {
|
|
@@ -1949,17 +2078,25 @@ var ImapAdapter = class {
|
|
|
1949
2078
|
folder = requestedFolder;
|
|
1950
2079
|
}
|
|
1951
2080
|
try {
|
|
1952
|
-
await
|
|
2081
|
+
await client.noop();
|
|
1953
2082
|
} catch {
|
|
1954
2083
|
}
|
|
1955
2084
|
let lock;
|
|
1956
2085
|
try {
|
|
1957
|
-
lock = await
|
|
2086
|
+
lock = await client.getMailboxLock(folder);
|
|
1958
2087
|
} catch (error) {
|
|
1959
2088
|
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
1960
2089
|
}
|
|
1961
2090
|
try {
|
|
1962
|
-
|
|
2091
|
+
let mailboxExists = client.mailbox?.exists ?? 0;
|
|
2092
|
+
if (mailboxExists === 0) {
|
|
2093
|
+
try {
|
|
2094
|
+
const status = await client.status(folder, { messages: true });
|
|
2095
|
+
mailboxExists = status?.messages ?? 0;
|
|
2096
|
+
} catch {
|
|
2097
|
+
mailboxExists = 0;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
1963
2100
|
if (mailboxExists === 0) {
|
|
1964
2101
|
return [];
|
|
1965
2102
|
}
|
|
@@ -1967,14 +2104,14 @@ var ImapAdapter = class {
|
|
|
1967
2104
|
const hasCriteria = Object.keys(criteria).length > 0;
|
|
1968
2105
|
let allUids = [];
|
|
1969
2106
|
try {
|
|
1970
|
-
const searchResult = await
|
|
2107
|
+
const searchResult = await client.search(
|
|
1971
2108
|
hasCriteria ? criteria : { all: true },
|
|
1972
2109
|
{ uid: true }
|
|
1973
2110
|
);
|
|
1974
2111
|
allUids = Array.isArray(searchResult) ? searchResult : [];
|
|
1975
2112
|
} catch {
|
|
1976
2113
|
try {
|
|
1977
|
-
allUids = await this.collectUidsViaFetch(query);
|
|
2114
|
+
allUids = await this.collectUidsViaFetch(client, query);
|
|
1978
2115
|
} catch {
|
|
1979
2116
|
return [];
|
|
1980
2117
|
}
|
|
@@ -1982,7 +2119,7 @@ var ImapAdapter = class {
|
|
|
1982
2119
|
const offset = query.offset || 0;
|
|
1983
2120
|
const slicedUids = query.limit ? allUids.slice(offset, offset + query.limit) : allUids.slice(offset);
|
|
1984
2121
|
if (slicedUids.length === 0) return [];
|
|
1985
|
-
return await this.fetchEmails(slicedUids, folder, query.returnBody);
|
|
2122
|
+
return await this.fetchEmails(client, slicedUids, folder, query.returnBody);
|
|
1986
2123
|
} catch (error) {
|
|
1987
2124
|
throw formatImapError(error, `Search failed in folder "${folder}"`);
|
|
1988
2125
|
} finally {
|
|
@@ -1996,13 +2133,12 @@ var ImapAdapter = class {
|
|
|
1996
2133
|
* 2. FETCH 1:N using mailbox.exists as explicit range
|
|
1997
2134
|
* 3. Individual sequence number fetches (slowest but most resilient)
|
|
1998
2135
|
*/
|
|
1999
|
-
async collectUidsViaFetch(query) {
|
|
2000
|
-
if (!this.client) return [];
|
|
2136
|
+
async collectUidsViaFetch(client, query) {
|
|
2001
2137
|
const uids = [];
|
|
2002
|
-
const exists =
|
|
2138
|
+
const exists = client.mailbox?.exists ?? 0;
|
|
2003
2139
|
const safeFetch = async (range) => {
|
|
2004
2140
|
try {
|
|
2005
|
-
const messages = await
|
|
2141
|
+
const messages = await client.fetchAll(range, { uid: true, flags: true });
|
|
2006
2142
|
for (const msg of messages) {
|
|
2007
2143
|
if (query.unreadOnly && msg.flags?.has("\\Seen")) continue;
|
|
2008
2144
|
if (query.starredOnly && !msg.flags?.has("\\Flagged")) continue;
|
|
@@ -2022,7 +2158,7 @@ var ImapAdapter = class {
|
|
|
2022
2158
|
const count = exists > 0 ? exists : 50;
|
|
2023
2159
|
for (let seq = 1; seq <= count; seq++) {
|
|
2024
2160
|
try {
|
|
2025
|
-
const msgs = await
|
|
2161
|
+
const msgs = await client.fetchAll(String(seq), { uid: true, flags: true });
|
|
2026
2162
|
for (const msg of msgs) {
|
|
2027
2163
|
if (query.unreadOnly && msg.flags?.has("\\Seen")) continue;
|
|
2028
2164
|
if (query.starredOnly && !msg.flags?.has("\\Flagged")) continue;
|
|
@@ -2036,19 +2172,18 @@ var ImapAdapter = class {
|
|
|
2036
2172
|
/**
|
|
2037
2173
|
* Fetches email data for a set of UIDs, either with full body or lightweight headers.
|
|
2038
2174
|
*/
|
|
2039
|
-
async fetchEmails(uids, folder, returnBody) {
|
|
2040
|
-
if (!this.client) return [];
|
|
2175
|
+
async fetchEmails(client, uids, folder, returnBody) {
|
|
2041
2176
|
const emails = [];
|
|
2042
2177
|
const fetchOpts = returnBody ? { source: true, uid: true, flags: true } : { envelope: true, uid: true, flags: true, bodyStructure: true };
|
|
2043
2178
|
const uidRange = uids.join(",");
|
|
2044
2179
|
let messages;
|
|
2045
2180
|
try {
|
|
2046
|
-
messages = await
|
|
2181
|
+
messages = await client.fetchAll(uidRange, fetchOpts, { uid: true });
|
|
2047
2182
|
} catch {
|
|
2048
2183
|
messages = [];
|
|
2049
2184
|
for (const uid of uids) {
|
|
2050
2185
|
try {
|
|
2051
|
-
const batch = await
|
|
2186
|
+
const batch = await client.fetchAll(String(uid), fetchOpts, { uid: true });
|
|
2052
2187
|
messages.push(...batch);
|
|
2053
2188
|
} catch {
|
|
2054
2189
|
}
|
|
@@ -2087,17 +2222,17 @@ var ImapAdapter = class {
|
|
|
2087
2222
|
return emails;
|
|
2088
2223
|
}
|
|
2089
2224
|
async getEmail(id, folder) {
|
|
2090
|
-
|
|
2225
|
+
const client = await this.ensureConnected();
|
|
2091
2226
|
const targetFolder = folder || "INBOX";
|
|
2092
2227
|
let lock;
|
|
2093
2228
|
try {
|
|
2094
|
-
lock = await
|
|
2229
|
+
lock = await client.getMailboxLock(targetFolder);
|
|
2095
2230
|
} catch (error) {
|
|
2096
2231
|
throw formatImapError(error, `Failed to open folder "${targetFolder}"`);
|
|
2097
2232
|
}
|
|
2098
2233
|
try {
|
|
2099
2234
|
const uid = parseInt(id, 10);
|
|
2100
|
-
const msg = await
|
|
2235
|
+
const msg = await client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
|
|
2101
2236
|
if (!msg) throw new Error(`Email ${id} not found`);
|
|
2102
2237
|
const parsed = await simpleParser(msg.source);
|
|
2103
2238
|
parsed.flags = msg.flags;
|
|
@@ -2107,22 +2242,22 @@ var ImapAdapter = class {
|
|
|
2107
2242
|
}
|
|
2108
2243
|
}
|
|
2109
2244
|
async getThread(threadId) {
|
|
2110
|
-
|
|
2245
|
+
const client = await this.ensureConnected();
|
|
2111
2246
|
let lock;
|
|
2112
2247
|
try {
|
|
2113
|
-
lock = await
|
|
2248
|
+
lock = await client.getMailboxLock("INBOX");
|
|
2114
2249
|
} catch (error) {
|
|
2115
2250
|
throw formatImapError(error, 'Failed to open folder "INBOX"');
|
|
2116
2251
|
}
|
|
2117
2252
|
try {
|
|
2118
|
-
const searchResult = await
|
|
2253
|
+
const searchResult = await client.search(
|
|
2119
2254
|
{ or: [{ header: { "message-id": threadId } }, { header: { references: threadId } }, { header: { "in-reply-to": threadId } }] },
|
|
2120
2255
|
{ uid: true }
|
|
2121
2256
|
);
|
|
2122
2257
|
const uids = Array.isArray(searchResult) ? searchResult : [];
|
|
2123
2258
|
if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
|
|
2124
2259
|
const messages = [];
|
|
2125
|
-
for await (const msg of
|
|
2260
|
+
for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
|
|
2126
2261
|
const parsed = await simpleParser(msg.source);
|
|
2127
2262
|
parsed.flags = msg.flags;
|
|
2128
2263
|
messages.push(mapParsedEmail(parsed, "INBOX", this.accountId, msg.uid));
|
|
@@ -2148,15 +2283,15 @@ var ImapAdapter = class {
|
|
|
2148
2283
|
}
|
|
2149
2284
|
}
|
|
2150
2285
|
async getAttachment(emailId, attachmentId) {
|
|
2151
|
-
|
|
2286
|
+
const client = await this.ensureConnected();
|
|
2152
2287
|
let lock;
|
|
2153
2288
|
try {
|
|
2154
|
-
lock = await
|
|
2289
|
+
lock = await client.getMailboxLock("INBOX");
|
|
2155
2290
|
} catch (error) {
|
|
2156
2291
|
throw formatImapError(error, 'Failed to open folder "INBOX"');
|
|
2157
2292
|
}
|
|
2158
2293
|
try {
|
|
2159
|
-
const msg = await
|
|
2294
|
+
const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
|
|
2160
2295
|
if (!msg) throw new Error(`Email ${emailId} not found`);
|
|
2161
2296
|
const parsed = await simpleParser(msg.source);
|
|
2162
2297
|
const attachment = (parsed.attachments || []).find(
|
|
@@ -2183,7 +2318,7 @@ var ImapAdapter = class {
|
|
|
2183
2318
|
return { id: messageId };
|
|
2184
2319
|
}
|
|
2185
2320
|
async createDraft(params) {
|
|
2186
|
-
|
|
2321
|
+
const client = await this.ensureConnected();
|
|
2187
2322
|
const lines = [];
|
|
2188
2323
|
lines.push(`From: ${this.email}`);
|
|
2189
2324
|
lines.push(`To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`);
|
|
@@ -2199,7 +2334,9 @@ var ImapAdapter = class {
|
|
|
2199
2334
|
lines.push("");
|
|
2200
2335
|
lines.push(params.body.text || "");
|
|
2201
2336
|
const rawMessage = lines.join("\r\n");
|
|
2202
|
-
const
|
|
2337
|
+
const draftsFolder = await this.resolveFolder("Drafts");
|
|
2338
|
+
const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
|
|
2339
|
+
if (!result) throw new Error("Failed to append draft");
|
|
2203
2340
|
return { id: String(result.uid || result) };
|
|
2204
2341
|
}
|
|
2205
2342
|
async listDrafts(limit, offset) {
|
|
@@ -2210,16 +2347,16 @@ var ImapAdapter = class {
|
|
|
2210
2347
|
});
|
|
2211
2348
|
}
|
|
2212
2349
|
async moveEmail(emailId, targetFolder, sourceFolder) {
|
|
2213
|
-
|
|
2350
|
+
const client = await this.ensureConnected();
|
|
2214
2351
|
const folder = sourceFolder || "INBOX";
|
|
2215
2352
|
let lock;
|
|
2216
2353
|
try {
|
|
2217
|
-
lock = await
|
|
2354
|
+
lock = await client.getMailboxLock(folder);
|
|
2218
2355
|
} catch (error) {
|
|
2219
2356
|
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
2220
2357
|
}
|
|
2221
2358
|
try {
|
|
2222
|
-
await
|
|
2359
|
+
await client.messageMove(emailId, targetFolder, { uid: true });
|
|
2223
2360
|
} finally {
|
|
2224
2361
|
lock.release();
|
|
2225
2362
|
}
|
|
@@ -2249,44 +2386,44 @@ var ImapAdapter = class {
|
|
|
2249
2386
|
}
|
|
2250
2387
|
}
|
|
2251
2388
|
async deleteEmail(emailId, permanent, sourceFolder) {
|
|
2252
|
-
|
|
2389
|
+
const client = await this.ensureConnected();
|
|
2253
2390
|
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
2254
2391
|
const trashFolder = await this.resolveFolder("Trash");
|
|
2255
2392
|
let lock;
|
|
2256
2393
|
try {
|
|
2257
|
-
lock = await
|
|
2394
|
+
lock = await client.getMailboxLock(folder);
|
|
2258
2395
|
} catch (error) {
|
|
2259
2396
|
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
2260
2397
|
}
|
|
2261
2398
|
try {
|
|
2262
2399
|
if (permanent || folder === trashFolder) {
|
|
2263
|
-
await
|
|
2400
|
+
await client.messageDelete(emailId, { uid: true });
|
|
2264
2401
|
} else {
|
|
2265
|
-
await
|
|
2402
|
+
await client.messageMove(emailId, trashFolder, { uid: true });
|
|
2266
2403
|
}
|
|
2267
2404
|
} finally {
|
|
2268
2405
|
lock.release();
|
|
2269
2406
|
}
|
|
2270
2407
|
}
|
|
2271
2408
|
async markEmail(emailId, flags, sourceFolder) {
|
|
2272
|
-
|
|
2409
|
+
const client = await this.ensureConnected();
|
|
2273
2410
|
const folder = sourceFolder || "INBOX";
|
|
2274
2411
|
let lock;
|
|
2275
2412
|
try {
|
|
2276
|
-
lock = await
|
|
2413
|
+
lock = await client.getMailboxLock(folder);
|
|
2277
2414
|
} catch (error) {
|
|
2278
2415
|
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
2279
2416
|
}
|
|
2280
2417
|
try {
|
|
2281
2418
|
if (flags.read === true) {
|
|
2282
|
-
await
|
|
2419
|
+
await client.messageFlagsAdd(emailId, ["\\Seen"], { uid: true });
|
|
2283
2420
|
} else if (flags.read === false) {
|
|
2284
|
-
await
|
|
2421
|
+
await client.messageFlagsRemove(emailId, ["\\Seen"], { uid: true });
|
|
2285
2422
|
}
|
|
2286
2423
|
if (flags.starred === true || flags.flagged === true) {
|
|
2287
|
-
await
|
|
2424
|
+
await client.messageFlagsAdd(emailId, ["\\Flagged"], { uid: true });
|
|
2288
2425
|
} else if (flags.starred === false || flags.flagged === false) {
|
|
2289
|
-
await
|
|
2426
|
+
await client.messageFlagsRemove(emailId, ["\\Flagged"], { uid: true });
|
|
2290
2427
|
}
|
|
2291
2428
|
} finally {
|
|
2292
2429
|
lock.release();
|
|
@@ -2319,32 +2456,32 @@ var ImapAdapter = class {
|
|
|
2319
2456
|
return { id: String(result?.uid ?? "") };
|
|
2320
2457
|
}
|
|
2321
2458
|
async batchDelete(emailIds, permanent, sourceFolder) {
|
|
2322
|
-
|
|
2459
|
+
const client = await this.ensureConnected();
|
|
2323
2460
|
const result = { succeeded: [], failed: [] };
|
|
2324
2461
|
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
2325
2462
|
const trashFolder = await this.resolveFolder("Trash");
|
|
2326
2463
|
const shouldPermanentDelete = permanent || folder === trashFolder;
|
|
2327
2464
|
let lock;
|
|
2328
2465
|
try {
|
|
2329
|
-
lock = await
|
|
2466
|
+
lock = await client.getMailboxLock(folder);
|
|
2330
2467
|
} catch (error) {
|
|
2331
2468
|
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
2332
2469
|
}
|
|
2333
2470
|
try {
|
|
2334
2471
|
const uidRange = emailIds.join(",");
|
|
2335
2472
|
if (shouldPermanentDelete) {
|
|
2336
|
-
await
|
|
2473
|
+
await client.messageDelete(uidRange, { uid: true });
|
|
2337
2474
|
} else {
|
|
2338
|
-
await
|
|
2475
|
+
await client.messageMove(uidRange, trashFolder, { uid: true });
|
|
2339
2476
|
}
|
|
2340
2477
|
result.succeeded = [...emailIds];
|
|
2341
2478
|
} catch (error) {
|
|
2342
2479
|
for (const id of emailIds) {
|
|
2343
2480
|
try {
|
|
2344
2481
|
if (shouldPermanentDelete) {
|
|
2345
|
-
await
|
|
2482
|
+
await client.messageDelete(id, { uid: true });
|
|
2346
2483
|
} else {
|
|
2347
|
-
await
|
|
2484
|
+
await client.messageMove(id, trashFolder, { uid: true });
|
|
2348
2485
|
}
|
|
2349
2486
|
result.succeeded.push(id);
|
|
2350
2487
|
} catch (e) {
|
|
@@ -2357,23 +2494,23 @@ var ImapAdapter = class {
|
|
|
2357
2494
|
return result;
|
|
2358
2495
|
}
|
|
2359
2496
|
async batchMove(emailIds, targetFolder, sourceFolder) {
|
|
2360
|
-
|
|
2497
|
+
const client = await this.ensureConnected();
|
|
2361
2498
|
const result = { succeeded: [], failed: [] };
|
|
2362
2499
|
const folder = sourceFolder || "INBOX";
|
|
2363
2500
|
let lock;
|
|
2364
2501
|
try {
|
|
2365
|
-
lock = await
|
|
2502
|
+
lock = await client.getMailboxLock(folder);
|
|
2366
2503
|
} catch (error) {
|
|
2367
2504
|
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
2368
2505
|
}
|
|
2369
2506
|
try {
|
|
2370
2507
|
const uidRange = emailIds.join(",");
|
|
2371
|
-
await
|
|
2508
|
+
await client.messageMove(uidRange, targetFolder, { uid: true });
|
|
2372
2509
|
result.succeeded = [...emailIds];
|
|
2373
2510
|
} catch (error) {
|
|
2374
2511
|
for (const id of emailIds) {
|
|
2375
2512
|
try {
|
|
2376
|
-
await
|
|
2513
|
+
await client.messageMove(id, targetFolder, { uid: true });
|
|
2377
2514
|
result.succeeded.push(id);
|
|
2378
2515
|
} catch (e) {
|
|
2379
2516
|
result.failed.push({ id, error: e.message });
|
|
@@ -2385,26 +2522,26 @@ var ImapAdapter = class {
|
|
|
2385
2522
|
return result;
|
|
2386
2523
|
}
|
|
2387
2524
|
async batchMark(emailIds, flags, sourceFolder) {
|
|
2388
|
-
|
|
2525
|
+
const client = await this.ensureConnected();
|
|
2389
2526
|
const result = { succeeded: [], failed: [] };
|
|
2390
2527
|
const folder = sourceFolder || "INBOX";
|
|
2391
2528
|
let lock;
|
|
2392
2529
|
try {
|
|
2393
|
-
lock = await
|
|
2530
|
+
lock = await client.getMailboxLock(folder);
|
|
2394
2531
|
} catch (error) {
|
|
2395
2532
|
throw formatImapError(error, `Failed to open folder "${folder}"`);
|
|
2396
2533
|
}
|
|
2397
2534
|
try {
|
|
2398
2535
|
const uidRange = emailIds.join(",");
|
|
2399
2536
|
if (flags.read === true) {
|
|
2400
|
-
await
|
|
2537
|
+
await client.messageFlagsAdd(uidRange, ["\\Seen"], { uid: true });
|
|
2401
2538
|
} else if (flags.read === false) {
|
|
2402
|
-
await
|
|
2539
|
+
await client.messageFlagsRemove(uidRange, ["\\Seen"], { uid: true });
|
|
2403
2540
|
}
|
|
2404
2541
|
if (flags.starred === true || flags.flagged === true) {
|
|
2405
|
-
await
|
|
2542
|
+
await client.messageFlagsAdd(uidRange, ["\\Flagged"], { uid: true });
|
|
2406
2543
|
} else if (flags.starred === false || flags.flagged === false) {
|
|
2407
|
-
await
|
|
2544
|
+
await client.messageFlagsRemove(uidRange, ["\\Flagged"], { uid: true });
|
|
2408
2545
|
}
|
|
2409
2546
|
result.succeeded = [...emailIds];
|
|
2410
2547
|
} catch (error) {
|