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:
nmemmertandClaude Sonnet 5.5 committed 2026-10-22 10:33:25 -04:00
1 parent 833342fced
commit fb7aabff61
4 files changed
+56 -36

No files matched your search

+13 -12
View File
@@ -149,7 +149,7 @@ const safeName = n => String(n || 'attachment').replace(/[\r\n"\\/]+/g, '_').sli
const cleanAttachments = list => { let total = 0; const out = (Array.isArray(list) ? list : []).slice(0, 10).map(a => { const data = String(a.data || ''); total += Buffer.from(data, 'base64').length; return { name: safeName(a.name), type: String(a.type || 'application/octet-stream').slice(0, 100), data }; }); if (total > MAX_ATTACH) throw fail(400, 'Attachments can total about 3 MB'); return out; };
let folderCache = new Map(); // account id -> { t, v }
const dropFolderCache = id => id ? folderCache.delete(id) : folderCache.clear();
const dropFolderCache = id => { if (id) { folderCache.delete(id); unreadCache.delete(id); } else { folderCache.clear(); unreadCache.clear(); } };
// ---- Outlook.com through Microsoft Graph. Every function takes the account and Graph's own ids.
const outlook = {
@@ -205,12 +205,8 @@ const providerOf = a => a.type === 'outlook' ? outlook : imap;
async function withInline(a, html, attachments, mid) {
// (contentId only exists on file attachments, so it cannot be selected in the list: fetch each small inline picture whole)
const inl = (attachments || []).filter(x => x.isInline && /^image\//i.test(x.contentType || '') && (x.size || 0) < 1.5 * 1024 * 1024).slice(0, 15);
for (const x of inl) {
try {
const full = await graph(a, `/me/messages/${id(mid)}/attachments/${id(x.id)}`);
if (full && full.contentBytes && full.contentId) html = html.split('cid:' + String(full.contentId).replace(/^<|>$/g, '')).join(`data:${full.contentType || x.contentType};base64,${full.contentBytes}`);
} catch { /* leave that picture out */ }
}
const fulls = await Promise.all(inl.map(x => graph(a, `/me/messages/${id(mid)}/attachments/${id(x.id)}`).catch(() => null))); // all at once
inl.forEach((x, i) => { const full = fulls[i]; if (full && full.contentBytes && full.contentId) html = html.split('cid:' + String(full.contentId).replace(/^<|>$/g, '')).join(`data:${full.contentType || x.contentType};base64,${full.contentBytes}`); });
return html;
}
@@ -246,12 +242,17 @@ async function adminHandle(req, res, p) {
json(res, 404, { error: 'Not found' });
}
/** Every unread count in one go (an account that cannot be reached is just left out of the total). */
async function overview() {
const unreadCache = new Map(); // account id -> { t, n }: counts are shown at once from here and refreshed when older than 30 s
const withTimeout = (p, ms) => Promise.race([p, new Promise((_, no) => setTimeout(() => no(Object.assign(new Error('That account did not answer in time'), { status: 504 })), ms))]);
/** The accounts, with unread counts. `light` makes no network calls at all (what the Mail tab needs to draw itself); otherwise counts older than
* 30 seconds are refreshed, each account on its own 6-second limit so one slow or unreachable account cannot hold up the others. */
async function overview(light) {
const list = accounts();
const rows = await Promise.all(list.map(async a => {
try { return { ...publicAcct(a), unread: await providerOf(a).unread(a) }; }
catch (e) { return { ...publicAcct(a), unread: null, error: e.message, expired: e.status === 409 }; }
const c = unreadCache.get(a.id);
if (light || (c && Date.now() - c.t < 30e3)) return { ...publicAcct(a), unread: c ? c.n : null };
try { const n = await withTimeout(providerOf(a).unread(a), 6000); unreadCache.set(a.id, { t: Date.now(), n }); return { ...publicAcct(a), unread: n }; }
catch (e) { return { ...publicAcct(a), unread: c ? c.n : null, error: e.message, expired: e.status === 409 }; }
}));
const live = accounts(); // (an account whose sign-in Microsoft revoked was dropped meanwhile)
const shown = rows.filter(r => live.some(x => x.id === r.id) || !r.expired);
@@ -263,7 +264,7 @@ async function handle(req, res) {
const u = new URL(req.url, 'http://x'), p = u.pathname, q = u.searchParams;
try {
if (p === '/api/mail/admin' || p.startsWith('/api/mail/admin/')) return await adminHandle(req, res, p);
if (p === '/api/mail/status') return json(res, 200, { ...(await overview()), outlook: outlookOn(), providers: imap.PRESETS });
if (p === '/api/mail/status') return json(res, 200, { ...(await overview(q.get('light') === '1')), outlook: outlookOn(), providers: imap.PRESETS });
if (p === '/api/mail/connect' && req.method === 'GET') { if (!outlookOn()) throw fail(403, 'Outlook.com is not set up on this server (an admin adds the Microsoft app in Settings)'); return json(res, 200, { url: startUrl(req), redirect: redirectUri(req) }); }
if (p === '/api/mail/folders') {
const a = findAccount(q.get('acct') || undefined);