Files
RustBox/src/web/js/00-bootstrap.js
T
Mateusz Gruszczyński 93762fe9ef
RustBox CI / rustbox (push) Waiting to run
v0.0.3
2026-10-30 00:12:09 +01:00

154 lines
6.5 KiB
JavaScript

/* RustBox UI module: 00-bootstrap.js */
(() => {
"use strict";
const root = document.documentElement;
const configuredBase = root.dataset.basePath || "/";
const BASE = configuredBase === "/" ? "" : configuredBase.replace(/\/$/, "");
const API = `${BASE}/api`;
const ICONS = `${BASE}/assets/icons.svg`;
const state = {
accounts: [],
accountId: localStorage.getItem("rustbox.activeAccount") || null,
folders: [],
folder: "INBOX",
collapsedFolders: new Set(),
page: 0,
perPage: 50,
total: 0,
messages: [],
selected: null,
selectedMessages: new Set(),
selectionMode: false,
searchContext: null,
searchAllResults: null,
currentDetail: null,
specialFolders: { sent: null, drafts: null, trash: null, junk: null, archive: null },
tags: [],
sieveScripts: [],
sieveInfo: null,
sieveEditingRuleId: null,
identities: [],
pgpKeys: [],
pgpPrefs: { wkdEnabled: true, hkpEnabled: false, requireTrustedEncryption: false },
pgpRecipientStatus: [],
authPolicy: { rememberMeEnabled: true, rememberMeDays: 30, sessionTimeoutHours: 24 },
contacts: [],
trustedSenders: new Set(),
contactGroups: [],
contactPhotoUrl: null,
calendarCursor: new Date(),
calendarView: localStorage.getItem("rustbox.calendarView") || "month",
threadExpanded: localStorage.getItem("rustbox.threadExpanded") !== "false",
calendarEvents: [],
savedSearches: [],
responseTemplates: [],
notificationPrefs: { enabled: true, sound: false, folders: ["INBOX"], dnd_enabled: false, dnd_start: "22:00", dnd_end: "07:00" },
mailPrefs: { messages_per_page: 50, mark_read_delay_ms: 0, remote_images: "ask", preferred_body: "auto", thread_mode: "threaded", collapse_quotes: true, show_snippets: true, default_sort: "date_desc", flagged_on_top: false, sync_imap_keywords: false },
currentFolderFlaggedOnTop: false,
markReadTimer: null,
ws: null,
wsRetryTimer: null,
realtimeRefreshTimer: null,
calendarReminderTimer: null,
composeDraftId: null,
composeAttachments: [],
composeDirty: false,
composeNeedsRemoteSync: false,
composeUploading: false,
composeReplyMeta: null,
composeInsertRange: null,
composeSelectedImage: null,
composeSignatureGeneration: 0,
identitySignatureRange: null,
inlineAttachmentIds: new Set(),
inlineImageCleanupTimer: null,
imageEditorBitmap: null,
imageEditorBlob: null,
draftSaveTimer: null,
prefs: {
theme: root.dataset.defaultTheme || "system", interfaceSkin: "default", presetScaleUnlocked: false, density: "compact", preview: "right", scale: "normal", language: "en", composeFormat: "html",
composeAutosaveMs: 3000, defaultPriority: "normal", requestMdnDefault: false, attachmentReminder: true, composeFontFamily: "system", replyPosition: "above", quoteStyle: "standard", forwardMode: "inline", spellcheckEnabled: true,
colorPreset: "sage", accentColor: "#6f9c86", backgroundColor: "#121513", panelColor: "#181c19", textColor: "#edf0ec", fontFamily: "system", fontSize: 13,
rowHeight: 58, folderPaneWidth: 228, messagePaneWidth: 420, borderRadius: 4,
contrast: "normal", showAvatars: true, showAttachmentIcons: true, showFlags: true, reduceMotion: false, dateFormat: "locale", timeFormat: "locale",
iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 29, readerMaxWidth: 0, composerWidth: 900, selectionColor: "#28352e", unreadFontWeight: 730, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false, folderBackground: "", messageBackground: "", readerBackground: "",
},
translations: {},
quotaWarningLevel: null,
networkOnline: navigator.onLine,
listStateTimer: null,
};
const $ = (selector, parent = document) => parent.querySelector(selector);
const $$ = (selector, parent = document) => [...parent.querySelectorAll(selector)];
const el = (tag, className, text) => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
};
function svgIcon(name) {
return `<svg aria-hidden="true"><use href="${ICONS}#icon-${name}"></use></svg>`;
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function stripRustBoxUiLeak(value) {
let text = String(value ?? "");
// Strip only RustBox's own embedded SVG sprite fragments if malformed MIME
// content captured adjacent UI markup. Legitimate SVG/code in mail remains.
text = text.replace(/<svg\b[^>]*>\s*<use\b[^>]*(?:\/assets\/icons\.svg|assets\/icons\.svg)[^>]*><\/use>\s*<\/svg>/gi, "");
text = text.replace(/&lt;svg\b[\s\S]*?(?:\/assets\/icons\.svg|assets\/icons\.svg)[\s\S]*?&lt;\/svg&gt;/gi, "");
return text;
}
function cleanPlainMessageText(value) {
return stripRustBoxUiLeak(value);
}
function sanitizeMessageHtml(html) {
const doc = new DOMParser().parseFromString(stripRustBoxUiLeak(html), "text/html");
doc.querySelectorAll("script,iframe,frame,frameset,object,embed,applet,form,input,button,select,textarea,base,meta,link,svg,math").forEach((node) => node.remove());
doc.querySelectorAll("*").forEach((node) => {
for (const attr of [...node.attributes]) {
const name = attr.name.toLowerCase();
const value = attr.value.trim();
if (name.startsWith("on") || ["srcdoc", "formaction"].includes(name)) node.removeAttribute(attr.name);
if (["href", "src", "background", "action", "poster"].includes(name) && /^\s*javascript:/i.test(value)) node.removeAttribute(attr.name);
}
});
return doc.body.innerHTML;
}
function sanitizeAttachmentHtml(html) {
const doc = new DOMParser().parseFromString(sanitizeMessageHtml(html), "text/html");
doc.querySelectorAll("a[href]").forEach((node) => node.removeAttribute("href"));
doc.querySelectorAll("[src],[background],[poster]").forEach((node) => {
for (const name of ["src", "background", "poster"]) {
if (!node.hasAttribute(name)) continue;
const value = String(node.getAttribute(name) || "").trim();
if (!/^data:/i.test(value)) node.removeAttribute(name);
}
});
return doc.body.innerHTML;
}
function apiPath(path) {
return `${API}${path.startsWith("/") ? path : `/${path}`}`;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}