/** * Several webmail accounts signed in at once, and a session that stays alive * for as long as it is used. * * Each signed-in account keeps its own cookie, lm_acct_, next to * lm_user_session. lm_user_session stays the *active* account: every existing * route, the Next middleware and the protocols service keep reading that one * cookie and need no change. Switching accounts is copying one of the * lm_acct_ tokens into lm_user_session. */ /** A webmail session lasts this long without use, and is renewed while in use. */ export const USER_SESSION_TTL_SECONDS = 30 * 24 * 3600; /** The console is the sensitive surface: it keeps the short, fixed lifetime. */ export const ADMIN_SESSION_TTL_SECONDS = 8 * 3600; /** A token is replaced once it is older than this, so a busy day costs one write. */ export const RENEW_AFTER_SECONDS = 24 * 3600; /** How long a replaced token keeps working, for requests already in flight. */ export const REPLACED_GRACE_SECONDS = 120; const ACCOUNT_COOKIE_PREFIX = "lm_acct_"; const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; /** More than this and the Cookie header risks the proxies' 8 KB limit. */ export const MAX_ACCOUNTS = 6; export function accountCookieName(mailboxId: string): string { return `${ACCOUNT_COOKIE_PREFIX}${mailboxId}`; } export function parseCookies(header: string | undefined): Record { const out: Record = {}; if (!header) return out; for (const part of header.split(";")) { const trimmed = part.trim(); const eq = trimmed.indexOf("="); // sliced rather than split so a value containing "=" survives intact. if (eq > 0) out[trimmed.slice(0, eq)] = trimmed.slice(eq + 1); } return out; } /** The account cookies a browser sent, as {mailbox id, token}. Names that are not a UUID are ignored. */ export function accountTokens(header: string | undefined): { id: string; token: string }[] { const found: { id: string; token: string }[] = []; for (const [name, token] of Object.entries(parseCookies(header))) { if (!name.startsWith(ACCOUNT_COOKIE_PREFIX) || !token) continue; const id = name.slice(ACCOUNT_COOKIE_PREFIX.length); if (UUID.test(id)) found.push({ id: id.toLowerCase(), token }); } return found; } /** Whether a token has aged enough to be replaced with a fresh one. */ export function shouldRenew(iat: number, nowSeconds: number): boolean { return nowSeconds - iat >= RENEW_AFTER_SECONDS; }