Mail opens faster: the page draws at once from a no-network status, the list, folders and unread counts load side by side, unread counts are cached and time-limited per account, IMAP keeps connections warmer and limits folder counting, messages are prefetched on hover
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
833342fced
commit
fb7aabff61
4 files changed
+56
-36
No files matched your search
+15
-14
@@ -39,18 +39,19 @@ function friendly(a, e) {
|
||||
}
|
||||
|
||||
// ---- connections: one per account, kept for a minute after the last use
|
||||
const IDLE = 5 * 60e3; // a connection is kept this long after its last use: opening one (TLS and login) is the slowest part of a request
|
||||
const pool = new Map(); // account id -> { client, timer, key }
|
||||
const forget = id => { const e = pool.get(id); if (e) { clearTimeout(e.timer); try { e.client.close(); } catch {} pool.delete(id); } };
|
||||
const forget = id => { folderCache.delete(id); boxCache.delete(id); const e = pool.get(id); if (e) { clearTimeout(e.timer); try { e.client.close(); } catch {} pool.delete(id); } };
|
||||
const keyOf = a => a.address + '\n' + a.pass;
|
||||
async function open(a) {
|
||||
const e = pool.get(a.id);
|
||||
if (e && e.key === keyOf(a) && e.client.usable) { clearTimeout(e.timer); e.timer = setTimeout(() => forget(a.id), 60e3).unref(); return e.client; }
|
||||
if (e && e.key === keyOf(a) && e.client.usable) { clearTimeout(e.timer); e.timer = setTimeout(() => forget(a.id), IDLE).unref(); return e.client; }
|
||||
forget(a.id);
|
||||
const c = cfgOf(a);
|
||||
const client = new ImapFlow({ host: c.host, port: c.port, secure: c.secure, ...(c.test ? { doSTARTTLS: false, tls: { rejectUnauthorized: false } } : {}), auth: { user: a.address, pass: a.pass }, logger: false, emitLogs: false, socketTimeout: 60e3, greetingTimeout: 20e3, connectionTimeout: 20e3 });
|
||||
client.on('error', () => {}); client.on('close', () => { const cur = pool.get(a.id); if (cur && cur.client === client) pool.delete(a.id); });
|
||||
try { await client.connect(); } catch (err) { throw fail(err.authenticationFailed ? 401 : 502, friendly(a, err)); }
|
||||
pool.set(a.id, { client, key: keyOf(a), timer: setTimeout(() => forget(a.id), 60e3).unref() });
|
||||
pool.set(a.id, { client, key: keyOf(a), timer: setTimeout(() => forget(a.id), IDLE).unref() });
|
||||
return client;
|
||||
}
|
||||
/** Run `fn(client)`; if the connection had gone stale, reconnect once. */
|
||||
@@ -92,20 +93,20 @@ async function pathOf(a, client, folder) {
|
||||
const addrOf = x => x ? { name: x.name || '', address: x.address || '' } : { name: '', address: '' };
|
||||
const hasAttachment = n => !!n && (((n.disposition || '').toLowerCase() === 'attachment') || (n.childNodes || []).some(hasAttachment));
|
||||
|
||||
const folderCache = new Map(); // account id -> { t, v }
|
||||
async function folders(a) {
|
||||
return withClient(a, async client => {
|
||||
const c = folderCache.get(a.id); if (c && Date.now() - c.t < 30e3) return c.v;
|
||||
const v = await withClient(a, async client => {
|
||||
const { list, alias } = await boxes(a, client), byPath = {}; for (const [k, p] of Object.entries(alias)) byPath[p] = k;
|
||||
const skip = b => b.flags.has('\\Noselect') || ['\\Flagged', '\\Important'].includes(b.specialUse);
|
||||
const rows = [];
|
||||
for (const b of list) {
|
||||
if (skip(b)) continue;
|
||||
let st = {}; try { st = await client.status(b.path, { messages: true, unseen: true }); } catch {}
|
||||
const k = byPath[b.path];
|
||||
rows.push({ id: k || 'p-' + Buffer.from(b.path).toString('base64url'), name: k ? NAMES[k] : b.path.replace(/^\[.*?\]\//, ''), unread: st.unseen || 0, total: st.messages || 0, well: !!k });
|
||||
}
|
||||
const keep = list.filter(b => !skip(b)), wellFirst = [...keep.filter(b => byPath[b.path]), ...keep.filter(b => !byPath[b.path])];
|
||||
const counts = new Map(); // one STATUS per folder is a round trip each: the usual six first, then at most ten more (the rest show no number)
|
||||
for (const b of wellFirst.slice(0, Object.keys(byPath).length + 10)) { try { counts.set(b.path, await client.status(b.path, { messages: true, unseen: true })); } catch {} }
|
||||
const rows = keep.map(b => { const k = byPath[b.path], st = counts.get(b.path) || {}; return { id: k || 'p-' + Buffer.from(b.path).toString('base64url'), name: k ? NAMES[k] : b.path.replace(/^\[.*?\]\//, ''), unread: st.unseen || 0, total: st.messages || 0, well: !!k }; });
|
||||
const order = Object.keys(NAMES);
|
||||
return rows.sort((x, y) => (x.well ? order.indexOf(x.id) : 99) - (y.well ? order.indexOf(y.id) : 99) || x.name.localeCompare(y.name));
|
||||
});
|
||||
folderCache.set(a.id, { t: Date.now(), v }); return v;
|
||||
}
|
||||
|
||||
async function list(a, { folder, skip, q }) {
|
||||
@@ -144,7 +145,8 @@ async function source(a, mid) {
|
||||
if (!dl || !dl.content) throw fail(404, 'That message is gone');
|
||||
const chunks = []; let n = 0;
|
||||
for await (const ch of dl.content) { n += ch.length; if (n > MAX_SOURCE) throw fail(400, 'That message is too large to open here'); chunks.push(ch); }
|
||||
return { parsed: await simpleParser(Buffer.concat(chunks)), path, uid };
|
||||
const meta = await client.fetchOne(String(uid), { flags: true }, { uid: true }).catch(() => null);
|
||||
return { parsed: await simpleParser(Buffer.concat(chunks)), path, uid, flags: meta ? meta.flags : new Set() };
|
||||
} finally { lock.release(); }
|
||||
});
|
||||
}
|
||||
@@ -152,11 +154,10 @@ const pick = (list, f) => (list || []).map(x => f(x));
|
||||
const addrs = v => (v && v.value ? v.value : []).map(x => ({ name: x.name || '', address: x.address || '' }));
|
||||
|
||||
async function message(a, mid) {
|
||||
const { parsed: p, path, uid } = await source(a, mid);
|
||||
const { parsed: p, flags } = await source(a, mid);
|
||||
let html = p.html || (p.textAsHtml ? p.textAsHtml : textToHtml(p.text || ''));
|
||||
const atts = p.attachments || [];
|
||||
for (const x of atts) if (x.cid && /^image\//i.test(x.contentType || '') && x.size < 1.5 * 1024 * 1024) html = html.split('cid:' + x.cid).join(`data:${x.contentType};base64,${x.content.toString('base64')}`);
|
||||
const flags = await withClient(a, async client => { const lock = await client.getMailboxLock(path); try { const m = await client.fetchOne(String(uid), { flags: true }, { uid: true }); return m ? m.flags : new Set(); } finally { lock.release(); } });
|
||||
return { id: mid, subject: p.subject || '(No subject)', from: addrs(p.from)[0] || { name: '', address: '' }, to: addrs(p.to), cc: addrs(p.cc), date: (p.date || new Date()).toISOString(), read: flags.has('\\Seen'), flagged: flags.has('\\Flagged'),
|
||||
html, attachments: atts.map((x, i) => ({ x, i })).filter(({ x }) => !x.related).map(({ x, i }) => ({ id: String(i), name: x.filename || 'attachment', type: x.contentType, size: x.size })) };
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user