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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user