215 lines
8.9 KiB
JavaScript
215 lines
8.9 KiB
JavaScript
|
|
(() => {
|
|
"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: 300,
|
|
total: 0,
|
|
loadingMoreMessages: false,
|
|
loadingAllMessages: false,
|
|
messageLoadPending: 0,
|
|
messages: [],
|
|
selected: null,
|
|
selectedMessages: new Set(),
|
|
selectionMode: false,
|
|
selectionAnchorKey: null,
|
|
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, pgpEnabled: false, sieveEnabled: false, diagnosticsEnabled: false, aboutEnabled: false, multiAccountEnabled: false },
|
|
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" },
|
|
notificationPrefsByAccount: new Map(),
|
|
notificationAccountIds: new Set(),
|
|
mailPrefs: { messages_per_page: 300, message_loading_mode: "pagination", 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, compact_wide_rows: true },
|
|
currentFolderFlaggedOnTop: false,
|
|
markReadTimer: null,
|
|
ws: null,
|
|
wsRetryTimer: null,
|
|
wsRetryAttempt: 0,
|
|
wsWatchdogTimer: null,
|
|
wsLastHeartbeatAt: 0,
|
|
wsShouldConnect: false,
|
|
wsConnectedOnce: false,
|
|
realtimeRefreshTimer: null,
|
|
realtimeRefreshEvents: [],
|
|
realtimePollTimer: null,
|
|
realtimeSyncPromise: null,
|
|
realtimeSeenEvents: new Map(),
|
|
realtimeNotifiedMessages: new Map(),
|
|
realtimeNotificationSuppressions: new Map(),
|
|
realtimeNotificationSuppressedMessages: new Map(),
|
|
realtimeNotificationsReady: false,
|
|
realtimeNotificationsArmRequested: false,
|
|
realtimeNotificationsReadyTimer: null,
|
|
realtimeWatchedFolder: null,
|
|
startupToastsMuted: true,
|
|
indexingActive: false,
|
|
indexingPending: 0,
|
|
imapSyncActive: false,
|
|
imapSyncFolder: null,
|
|
imapSyncPending: 0,
|
|
imapSyncInventoryPending: 0,
|
|
calendarReminderTimer: null,
|
|
calendarSettings: null,
|
|
composeDraftId: null,
|
|
composeRemoteDraft: null,
|
|
composeAttachments: [],
|
|
composeDirty: false,
|
|
composeNeedsRemoteSync: false,
|
|
composeUploading: false,
|
|
composeUploadController: null,
|
|
composeReplyMeta: null,
|
|
composeAction: "new",
|
|
composeSignatureGeneration: 0,
|
|
composeInsertRange: null,
|
|
composeSelectedImage: null,
|
|
identitySignatureRange: null,
|
|
identitySignatureHtmlRange: null,
|
|
inlineAttachmentIds: new Set(),
|
|
signatureAttachmentIds: new Set(),
|
|
inlineImageCleanupTimer: null,
|
|
imageEditorBitmap: null,
|
|
imageEditorBlob: null,
|
|
draftSaveTimer: null,
|
|
prefs: {
|
|
theme: root.dataset.defaultTheme || "system", interfaceSkin: "default", skinDepth: "standard", skinGradients: false, themeLightFrom: "07:00", themeDarkFrom: "19:00", accountColor: "", 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: "", adaptiveEmailColors: false,
|
|
},
|
|
translations: {},
|
|
quotaWarningLevel: null,
|
|
networkOnline: navigator.onLine,
|
|
listStateTimer: null,
|
|
readerFullscreenHistory: false,
|
|
readerScrollTopBeforeFullscreen: 0,
|
|
};
|
|
|
|
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 ?? "");
|
|
|
|
|
|
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 sanitizeMessagePreviewHtml(html) {
|
|
const source = stripRustBoxUiLeak(html);
|
|
const sourceDoc = new DOMParser().parseFromString(source, "text/html");
|
|
const headStyles = [...sourceDoc.head.querySelectorAll("style")]
|
|
.map((node) => String(node.textContent || "").replace(/@import\s+(?:url\()?[^;]+;?/gi, ""))
|
|
.filter((css) => css.trim());
|
|
const doc = new DOMParser().parseFromString(sanitizeMessageHtml(source), "text/html");
|
|
for (const css of headStyles.reverse()) {
|
|
const style = doc.createElement("style");
|
|
style.textContent = css;
|
|
doc.body.prepend(style);
|
|
}
|
|
doc.querySelectorAll("a[href]").forEach((node) => {
|
|
if (!node.hasAttribute("target")) node.setAttribute("target", "_blank");
|
|
const target = String(node.getAttribute("target") || "").toLowerCase();
|
|
if (target && !["_self", "_parent", "_top"].includes(target)) {
|
|
const rel = new Set(String(node.getAttribute("rel") || "").split(/\s+/).filter(Boolean));
|
|
rel.add("noopener");
|
|
rel.add("noreferrer");
|
|
node.setAttribute("rel", [...rel].join(" "));
|
|
}
|
|
});
|
|
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));
|
|
}
|