154 lines
6.5 KiB
JavaScript
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("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
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(/<svg\b[\s\S]*?(?:\/assets\/icons\.svg|assets\/icons\.svg)[\s\S]*?<\/svg>/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));
|
|
}
|
|
|