diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d466f..b2e9ecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # RustBox changelog +### 0.2.19 + +- Improved vertical space management in the HTML message reader: the sandboxed message iframe now expands to the actual space remaining below the reader toolbar, message header and auxiliary bars instead of stopping at a fixed `68vh`. +- Recalculate the HTML message viewport when the reader, header, attachments, security bars, window size or fullscreen state changes, avoiding an unnecessary short inner scrollbar while preserving the existing iframe sandbox. +- Bumped the application/source release to 0.2.19. + ### 0.2.18 - Fixed new-message sender selection so the composer prefers the configured default identity, then the identity matching the active account address, then the first available identity; the visible From field now shows the active account address instead of an ambiguous `Account default` label when no identity is selected. diff --git a/Cargo.lock b/Cargo.lock index 5d09f0f..36b4baa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3506,7 +3506,7 @@ dependencies = [ [[package]] name = "rustbox" -version = "0.2.18" +version = "0.2.19" dependencies = [ "aes-gcm", "async-imap", diff --git a/Cargo.toml b/Cargo.toml index de3bb0f..29afb43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustbox" -version = "0.2.18" +version = "0.2.19" edition = "2024" rust-version = "1.99.1" default-run = "rustbox" diff --git a/NOTICE.md b/NOTICE.md index bec2f27..6c67c7b 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,6 +1,6 @@ # RustBox attribution / notice -RustBox 0.2.18 is derived from the Oxi webmail project: +RustBox 0.2.19 is derived from the Oxi webmail project: - Upstream: `c0h1b4/oxi` - Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d` diff --git a/README.md b/README.md index d11596d..7dcc6b7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# RustBox 0.2.18 +# RustBox 0.2.19 RustBox is a webmail derived from `c0h1b4/oxi`. The consumed Oxi revision is recorded in `UPSTREAM_BASE`. diff --git a/RELEASE-MANIFEST.txt b/RELEASE-MANIFEST.txt index dc2d869..2b73f59 100644 --- a/RELEASE-MANIFEST.txt +++ b/RELEASE-MANIFEST.txt @@ -1,6 +1,7 @@ -RustBox 0.2.18 release manifest +RustBox 0.2.19 release manifest -0.2.18 release changes and retained feature set: +0.2.19 release changes and retained feature set: +- expands sandboxed HTML message previews to the real remaining reader height and keeps that height synchronized across layout/fullscreen changes - adds equal-height text/HTML signature fields and a dedicated raw-HTML + visual signature editor with image insertion - keeps synchronization activity visible during page/progressive/all mailbox loads and individual message retrieval - adds Shift-click contiguous message selection and direct first/last pagination controls @@ -70,14 +71,14 @@ Docker examples: Operations/docs: - standalone `scripts/migrate_roundcube.py` imports Roundcube contacts/identities using RustBox config.toml and supports SQLite/MySQL/PostgreSQL targets -- README.md documents 0.2.18 configuration +- README.md documents 0.2.19 configuration - docker/README.md documents Compose variants - docs/STORAGE_SEARCH_OPERATIONS.md documents failure modes, shared-storage requirements and migration behavior - TODO.md leaves live SILO/MinIO/AWS/Ceph and real multi-node acceptance validation explicitly pending Build/release metadata: - `scripts/package-release.sh` runs the full Rust release gate before creating ZIP/SHA256 artifacts -- Cargo.toml version = 0.2.18 +- Cargo.toml version = 0.2.19 - rust-toolchain.toml pins Rust 1.99.1 - Cargo.lock is retained for reproducible application builds; the release gate generates it only when missing - CI and scripts/build-release.sh use the retained Cargo.lock (generating it only if missing), then run locked tests/Clippy/release build diff --git a/RELEASE-VALIDATION.txt b/RELEASE-VALIDATION.txt index 3f5e5bc..0822ae1 100644 --- a/RELEASE-VALIDATION.txt +++ b/RELEASE-VALIDATION.txt @@ -1,6 +1,7 @@ -RustBox 0.2.18 - final source validation +RustBox 0.2.19 - final source validation -0.2.18 scope: +0.2.19 scope: +- HTML message reader iframe height follows the actual remaining reader viewport rather than a fixed 68vh, with resize/fullscreen recalculation - multi-account sessions expose a top-bar quick switcher, per-browser combined notification selection, per-account notification preferences and the administrator `multi_account_enabled` feature gate - plain-text and HTML signature controls have aligned heights; raw HTML can be pasted into a modal, previewed/edited visually and extended with inline images - synchronization spinner covers mailbox pagination, progressive/full-folder loading and message/draft body retrieval @@ -32,15 +33,15 @@ RustBox 0.2.18 - final source validation - Composing shows only compact attachment/message size-limit values on one line - 0.2.9 realtime notification gating, browser shortcut preservation and 300-message defaults remain intact - V047-V050 are consolidated into compatibility baseline V050; V051 migrates existing tenants with adaptive HTML-mail colors and mailbox loading mode; V052 is next -- version metadata remains at 0.2.18 +- version metadata identifies 0.2.19 Completed in this environment: -- offline release validator: scripts/validate-release-static.py (413/413 checks passed) +- offline release validator: scripts/validate-release-static.py (420/420 checks passed) - browser JavaScript assembled in manifest order passes `node --check` - all JSON files parse successfully - Python helper scripts compile successfully - shell scripts pass `bash -n` -- release metadata consistently identifies 0.2.18 +- release metadata consistently identifies 0.2.19 - V050 baseline consolidation plus the V051 adaptive-colors/mailbox-loading migration are validated offline diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index a99c7e9..2a16009 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,10 +1,11 @@ -# RustBox 0.2.18 - implementation status +# RustBox 0.2.19 - implementation status - Draft saves are realtime-silent: Drafts folder/list updates continue, but saving a draft does not surface as a new-mail notification. -This file tracks the `0.2.18` source snapshot. Storage/search scope and outstanding acceptance work are tracked in `TODO.md`. A feature is only listed as implemented when working code exists in this source tree; partial items are called out explicitly. +This file tracks the `0.2.19` source snapshot. Storage/search scope and outstanding acceptance work are tracked in `TODO.md`. A feature is only listed as implemented when working code exists in this source tree; partial items are called out explicitly. -## 0.2.18 browser mail UX +## 0.2.19 browser mail UX +- HTML message previews use the full remaining vertical reader area and recompute their iframe height as reader chrome/layout changes. - Signature management uses equal-height text and HTML fields and includes a raw-HTML modal with a sanitized visual editor and inline image import. - Mail loading activity is represented by the app-bar spinner for pagination, progressive/full-folder retrieval and individual message/draft loading. - Shift-click selects a contiguous range in the visible message list, and pagination includes direct first/last page navigation. diff --git a/scripts/validate-release-static.py b/scripts/validate-release-static.py index facd8a7..0b585c6 100755 --- a/scripts/validate-release-static.py +++ b/scripts/validate-release-static.py @@ -152,6 +152,7 @@ require('suppressRealtimeNotificationsForFolder(notificationDestination)' in mai require('realtimeNotificationSuppressedMessages: new Map()' in bootstrap_js and 'suppressRealtimeNotificationForMessage' in realtime_js and 'consumeRealtimeMessageNotificationSuppression' in realtime_js and 'suppressIntentionalMoveNotification' in mail_actions_js, "intentional message moves suppress the matching realtime new-mail event by destination UID") require('finishAuthenticatedStartup({ showLoginToast = false } = {})' in auth_js and 'finishAuthenticatedStartup({ showLoginToast: true })' in auth_js and bindings_js.count('finishAuthenticatedStartup();') >= 1, "login success toast is limited to real login, not refresh/session restoration") require('${csp}' in reader_js, "message preview srcdoc explicitly declares UTF-8") +require('function resizeReaderMessageFrame()' in reader_js and 'new ResizeObserver(scheduleReaderMessageFrameResize)' in reader_js and '--reader-message-frame-height' in mail_css and 'min-height: 68vh' not in mail_css, "HTML message preview uses the full remaining reader height instead of fixed 68vh") require('function sanitizeMessagePreviewHtml(html)' in bootstrap_js and 'if (!node.hasAttribute("target")) node.setAttribute("target", "_blank");' in bootstrap_js and '!["_self", "_parent", "_top"].includes(target)' in bootstrap_js and 'rel.add("noopener");' in bootstrap_js and 'rel.add("noreferrer");' in bootstrap_js and 'sanitizeMessagePreviewHtml(detail.html)' in reader_js, "message preview links default to safe new-window navigation without overriding explicit targets") require('sourceDoc.head.querySelectorAll("style")' in bootstrap_js and 'messagePreviewBodyPresentation' in reader_js and 'iframe.style.background = previewTheme.surface;' in reader_js and 'background: var(--reader-bg);' in mail_css, "adaptive HTML-mail preview preserves head CSS/body presentation and never exposes a hard-coded white iframe canvas") require('attachmentContentUrl(folder, uid, attachment)' in reader_js and 'frame.src = attachmentContentUrl(folder, uid, attachment);' in reader_js and 'URL.createObjectURL(blob)' in reader_js, "PDF preview uses authenticated inline URL while other attachment previews retain blob handling") diff --git a/src/web/css/30-mail.css b/src/web/css/30-mail.css index 3ad627b..0d6f1ac 100644 --- a/src/web/css/30-mail.css +++ b/src/web/css/30-mail.css @@ -593,7 +593,8 @@ .message-body iframe { display: block; width: 100%; - min-height: 68vh; + height: var(--reader-message-frame-height, 260px); + min-height: 260px; border: 0; background: var(--reader-bg); } diff --git a/src/web/js/40-messages-reader.js b/src/web/js/40-messages-reader.js index ac9d2fc..d85de61 100644 --- a/src/web/js/40-messages-reader.js +++ b/src/web/js/40-messages-reader.js @@ -503,6 +503,7 @@ function setReaderFullscreen(enabled, options = {}) { } restoreReaderScroll(); } + scheduleReaderMessageFrameResize(); } function enterReaderFullscreen() { @@ -1164,10 +1165,62 @@ function adaptMessagePreviewColors(html, detail, body) { return { html: doc.body.innerHTML, targetTheme, surface: adaptiveEmailRgbCss(messageSurface), text: adaptiveEmailRgbCss(messageText) }; } +let readerMessageFrameObserver = null; +let readerMessageFrameResizeRaf = 0; +let readerMessageFrameWindowResizeBound = false; + +function resizeReaderMessageFrame() { + readerMessageFrameResizeRaf = 0; + const reader = $("#reader-content"); + const body = $("#message-body"); + const iframe = body?.querySelector(":scope > iframe"); + if (!reader || !body || !iframe || reader.classList.contains("hidden")) return; + + let occupiedHeight = 0; + for (const child of reader.children) { + if (child === body) break; + const style = getComputedStyle(child); + if (style.display === "none" || style.position === "absolute" || style.position === "fixed") continue; + const rect = child.getBoundingClientRect(); + occupiedHeight += rect.height; + occupiedHeight += Number.parseFloat(style.marginTop) || 0; + occupiedHeight += Number.parseFloat(style.marginBottom) || 0; + } + + const availableHeight = Math.max(260, Math.floor(reader.clientHeight - occupiedHeight)); + body.style.setProperty("--reader-message-frame-height", `${availableHeight}px`); +} + +function scheduleReaderMessageFrameResize() { + if (readerMessageFrameResizeRaf) cancelAnimationFrame(readerMessageFrameResizeRaf); + readerMessageFrameResizeRaf = requestAnimationFrame(resizeReaderMessageFrame); +} + +function observeReaderMessageFrameLayout() { + const reader = $("#reader-content"); + const body = $("#message-body"); + if (!reader || !body) return; + + if (typeof ResizeObserver === "function") { + if (!readerMessageFrameObserver) readerMessageFrameObserver = new ResizeObserver(scheduleReaderMessageFrameResize); + readerMessageFrameObserver.disconnect(); + readerMessageFrameObserver.observe(reader); + for (const child of reader.children) { + if (child !== body) readerMessageFrameObserver.observe(child); + } + } + + if (!readerMessageFrameWindowResizeBound) { + window.addEventListener("resize", scheduleReaderMessageFrameResize, { passive: true }); + readerMessageFrameWindowResizeBound = true; + } +} + function renderMessageBody(detail, forceRemote = false) { const body = $("#message-body"); const remoteBar = $("#remote-image-bar"); body.replaceChildren(); + body.style.removeProperty("--reader-message-frame-height"); remoteBar.classList.add("hidden"); const preferredBody = state.mailPrefs.preferred_body || "auto"; if (preferredBody === "text" || !detail?.html) { @@ -1208,6 +1261,8 @@ function renderMessageBody(detail, forceRemote = false) { ].filter(Boolean).join(" "); iframe.srcdoc = `${csp}${safeHtml}`; body.append(iframe); + observeReaderMessageFrameLayout(); + resizeReaderMessageFrame(); } async function downloadAttachment(folder, uid, attachment) {