const LINK_ARCHIVE = "message_tags.message_id:tag=archive"; function getWebmailToken() { const q = new URLSearchParams(location.search).get("token"); if (q) { try { localStorage.setItem("webmail_token", q); } catch (_) {} return q; } try { return localStorage.getItem("webmail_token") || ""; } catch (_) { return ""; } } function clearWebmailToken() { try { localStorage.removeItem("webmail_token"); } catch (_) {} } function setWebmailToken(t) { try { localStorage.setItem("webmail_token", t); } catch (_) {} } function getWebmailAccount() { try { const raw = localStorage.getItem("webmail_account"); return raw ? JSON.parse(raw) : null; } catch (_) { return null; } } function setWebmailAccount(account) { try { if (account) { localStorage.setItem("webmail_account", JSON.stringify(account)); } else { localStorage.removeItem("webmail_account"); } } catch (_) {} } function clearWebmailAccount() { try { localStorage.removeItem("webmail_account"); } catch (_) {} } function getRememberedCreds() { try { const raw = localStorage.getItem("webmail_remember"); if (!raw) return null; const parsed = JSON.parse(raw); if (parsed && typeof parsed.address === "string" && typeof parsed.password === "string") { return parsed; } return null; } catch (_) { return null; } } function setRememberedCreds(creds) { try { if (creds && creds.address) { localStorage.setItem("webmail_remember", JSON.stringify(creds)); } else { localStorage.removeItem("webmail_remember"); } } catch (_) {} } function clearRememberedCreds() { try { localStorage.removeItem("webmail_remember"); } catch (_) {} } function useConfig() { const [cfg, setCfg] = React.useState(null); const [err, setErr] = React.useState(""); const [token, setToken] = React.useState(getWebmailToken); const [account, setAccount] = React.useState(getWebmailAccount); React.useEffect(() => { fetch("/api/config") .then((r) => r.json()) .then((j) => setCfg(j.data || j)) .catch((e) => setErr(String(e))); }, []); return { cfg, err, token, account, setToken: (t) => { setWebmailToken(t); setToken(t); }, setAccount: (a) => { setWebmailAccount(a); setAccount(a); }, clearAuth: () => { clearWebmailToken(); clearWebmailAccount(); setToken(""); setAccount(null); }, }; } function apiFetch(token, path, opts = {}) { const headers = Object.assign( { "Content-Type": "application/json" }, opts.headers || {} ); if (token) headers.Authorization = "Bearer " + token; return fetch(path, Object.assign({}, opts, { headers })).then(async (r) => { const j = await r.json().catch(() => ({})); if (!j.ok) throw new Error(j.error?.message || j.error || "api error " + r.status); return j.data; }); } function bulkFetch(token, body) { return apiFetch(token, "/api/bulk", { method: "POST", body: JSON.stringify(body), }); } function toggleStar(token, id) { return apiFetch(token, "/api/messages/" + encodeURIComponent(id) + "/star", { method: "PUT", }); } function formatWhen(ms) { if (!ms) return ""; const d = new Date(typeof ms === "number" && ms < 1e12 ? ms * 1000 : ms); return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } // Body HTML is sanitized server-side before storage. Do NOT un-escape // sequences here: JSON.parse already decodes genuine \u escapes, so any // remaining "<" is literal text the sender wrote — turning it back into // a tag would rebuild markup that sanitizing had just neutralized. function decodeBodyHtml(s) { return typeof s === "string" ? s : ""; } function rowFromItem(it) { const doc = typeof it.doc === "string" ? JSON.parse(it.doc) : it.doc || {}; return Object.assign({ id: it.id }, doc); } function tagNamesFromPage(data) { return (data?.items || []) .map((it) => { const doc = typeof it.doc === "string" ? JSON.parse(it.doc) : it.doc || {}; return doc.name || doc.tag; }) .filter(Boolean); } function viewLabel(view, tagView) { if (view === "archive") return "Archive"; if (view === "sent") return "Sent"; if (view === "tag") return "#" + tagView; return "Inbox"; } function appendViewLinks(params, view, tagView) { if (view === "archive") { params.append("has_link", LINK_ARCHIVE); } else if (view === "tag" && tagView) { params.append("has_link", "message_tags.message_id:tag=" + tagView); } else if (view !== "sent") { params.append("missing_link", LINK_ARCHIVE); } } // Inbox shows received mail only; Sent shows what this mailbox sent out. function viewWhere(view) { if (view === "sent") return "direction=out"; if (view === "inbox") return "direction=in"; return ""; } function buildListPath(view, tagView, q, limit, offset) { const params = new URLSearchParams(); params.set("limit", String(limit)); params.set("offset", String(offset)); params.set("sort", "created_at"); params.set("order", "desc"); const needle = (q || "").trim().replace(/,/g, " "); const where = [viewWhere(view), needle ? "search_text~=" + needle : ""].filter(Boolean); if (where.length) params.set("where", where.join(",")); appendViewLinks(params, view, tagView); return "/api/messages?" + params.toString(); } function buildUnreadCountPath(view, tagView) { const params = new URLSearchParams(); params.set("where", ["unread=true", viewWhere(view)].filter(Boolean).join(",")); appendViewLinks(params, view, tagView); return "/api/messages/count?" + params.toString(); } function composeMail(token, body) { return apiFetch(token, "/api/compose", { method: "POST", body: JSON.stringify(body), }); } function fetchSendAddresses(token) { return apiFetch(token, "/api/mailbox/addresses") .then((d) => d.addresses || []) .catch(() => []); } function createTag(token, name) { return apiFetch(token, "/api/tags", { method: "POST", body: JSON.stringify({ name }), }); }