From 20d78ee7beaaa02a0f51d584a341ccc14108f0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 6 Nov 2026 11:15:43 +0100 Subject: [PATCH] v0.2.27 --- CHANGELOG.md | 10 ++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- RELEASE-MANIFEST.txt | 14 ++- RELEASE-VALIDATION.txt | 10 +- docs/IMPLEMENTATION_STATUS.md | 14 ++- src/imap/client.rs | 3 + src/routes/mod.rs | 1 + src/routes/quota.rs | 50 +++++++-- src/web/css/30-mail.css | 47 ++++++++- src/web/css/50-settings.css | 152 ++++++++++++++++++++++++++- src/web/i18n/pl.json | 17 ++- src/web/index.html | 27 ++++- src/web/js/20-preferences-auth.js | 33 +++++- src/web/js/40-messages-reader.js | 52 ++++++--- src/web/js/60-mail-actions-search.js | 22 +++- src/web/js/80-settings-realtime.js | 132 +++++++++++++++++++++++ src/web/js/95-bindings-boot.js | 6 +- src/web/theme-bootstrap.js | 1 + 20 files changed, 545 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77ebf34..db8ef7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ What changed in each RustBox release, written for the people who use and run it. Technical details are in the [documentation](docs/). +## 0.2.27 + +- Monochrome mode now creates a custom copy when editing a predefined interface style, just like direct colour changes. +- Wide message rows give more room to the subject and show the complete subject in a tooltip. +- Read/unread, flag and open actions no longer reserve hidden horizontal space; they appear to the left on hover. +- Shift-click message popups now open as a reader-only embedded view instead of briefly showing the full mailbox. +- Opening a message in a new window or tab now uses the same reader-only embedded view as the popup instead of the full mailbox/fullscreen layout. +- Adaptive HTML mail colours now derive light/dark mode from the actual reader background, so custom palettes stay readable even when their saved theme mode does not match their visual brightness. +- Mailbox storage is now clickable: folder properties can show IMAP-reported size in MB, and a storage dialog visualizes per-folder usage with progress bars. + ## 0.2.26 - RustBox now has a license: the GNU AGPL v3, plus an optional paid commercial license for companies that prefer different terms. The About page explains both. diff --git a/Cargo.lock b/Cargo.lock index 0a7a07c..4f88e6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3506,7 +3506,7 @@ dependencies = [ [[package]] name = "rustbox" -version = "0.2.26" +version = "0.2.27" dependencies = [ "aes-gcm", "async-imap", diff --git a/Cargo.toml b/Cargo.toml index 16ec569..8e42e45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustbox" -version = "0.2.26" +version = "0.2.27" edition = "2024" rust-version = "1.99.1" license = "AGPL-3.0-only" diff --git a/README.md b/README.md index 42130d8..ad2af0b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# RustBox 0.2.26 +# RustBox 0.2.27 **Fast, self-hosted webmail written in Rust.** RustBox connects to any IMAP/SMTP server and ships as one executable with the whole interface built in, so there is no Node.js, PHP or separate frontend to deploy. diff --git a/RELEASE-MANIFEST.txt b/RELEASE-MANIFEST.txt index 92fd3c8..09748ab 100644 --- a/RELEASE-MANIFEST.txt +++ b/RELEASE-MANIFEST.txt @@ -1,4 +1,12 @@ -RustBox 0.2.26 release manifest +RustBox 0.2.27 release manifest + +0.2.27 release changes: +- predefined styles promote to a custom copy when monochrome mode is changed +- wide message rows allocate more space to subjects and expose the complete subject as a tooltip +- message quick actions overlay to the left on hover instead of reserving hidden width +- Shift-click message popups use a reader-only embedded layout +- adaptive HTML mail colors derive light/dark behavior from the actual reader background for custom palettes +- mailbox storage opens a graphical per-folder IMAP size view; folder properties show measured size in MB when available 0.2.26 release changes: - licensing: GNU AGPL v3 (`LICENSE`) with an optional paid commercial license (`LICENSING.md`); `NOTICE.md` keeps the MIT notice for parts originating from Oxi; Cargo metadata declares `AGPL-3.0-only` @@ -118,14 +126,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.26 configuration +- README.md documents 0.2.27 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.26 +- Cargo.toml version = 0.2.27 - 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 9e3814d..1f669a9 100644 --- a/RELEASE-VALIDATION.txt +++ b/RELEASE-VALIDATION.txt @@ -1,7 +1,7 @@ -RustBox 0.2.26 - final source validation +RustBox 0.2.27 - final source validation -0.2.26 scope: -- AGPL v3 + commercial dual licensing, Oxi notice, removal of the Oxi sync tooling, redesigned About page, feature documentation with screenshots, user-facing changelog, comment cleanup +0.2.27 scope: +- monochrome preset promotion, wider subject presentation with tooltip, non-reserving hover actions, reader-only popup embedding, adaptive HTML-mail light/dark inference from the actual reader surface, graphical per-folder IMAP storage usage, plus the 0.2.26 licensing/documentation baseline 0.2.25 scope: - filters: apply now on existing messages (`POST /api/filters/match`, IMAP UID SEARCH bounds + cache), exceptions, new conditions and actions, templates, missing-folder creation, "always move from this sender" @@ -67,7 +67,7 @@ RustBox 0.2.26 - 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-V052 are consolidated into compatibility baseline V052; V053 adds `skin_depth` and `skin_gradients`; V054 adds the theme schedule and account colour; V055 is next -- version metadata identifies 0.2.26 +- version metadata identifies 0.2.27 Completed in this environment: - offline release validator: scripts/validate-release-static.py (455/455 checks passed) @@ -75,7 +75,7 @@ Completed in this environment: - all JSON files parse successfully - Python helper scripts compile successfully - shell scripts pass `bash -n` -- release metadata consistently identifies 0.2.26 +- release metadata consistently identifies 0.2.27 - V052 baseline consolidation plus the V053 skin-depth migration are validated offline - 0.2.25 filter UI (apply now, run now, always move from sender), context submenus and preset editing exercised in Playwright against a mocked API; the 0.2.24 styles rendered at standard and immersive depth with gradients on and off; earlier: all 37 interface styles rendered in light and dark at the immersive depth against a mocked API (Playwright), with no page errors diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 0c0d5bd..1757e05 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,7 +1,15 @@ -# RustBox 0.2.26 - implementation status +# RustBox 0.2.27 - 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.26` 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.27` 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.27 mail-list and appearance polish + +- Monochrome changes on a predefined style promote it to a custom copy before applying the change. +- Wide message rows give the subject more horizontal space and expose its full value in a tooltip. +- Read/unread, flag and open actions appear to the left on hover without reserving hidden row width. +- Shift-click popups use a reader-only embedded layout from initial render. + ## 0.2.26 licensing and documentation @@ -145,7 +153,7 @@ This file tracks the `0.2.26` source snapshot. Storage/search scope and outstand - After moving/deleting the currently opened message (including Spam, Not spam, Archive and Move), the reader automatically opens the next list item and falls back to the previous item when the removed message was last. - Message reading, collapsible thread navigation, attachments, raw headers, full RFC822 `.eml` download via `BODY.PEEK[]` and `.eml` import into a selected IMAP folder via APPEND. - Message/folder context menus with common Roundcube-style actions, including folder properties, full message source, Message-ID/address copy and mailing-list unsubscribe; the reader sender line also exposes compose/contact/search/filter actions. -- Mailbox quota display where the IMAP server exposes quota, including warning/danger states at 80%/95%. +- Mailbox quota display where the IMAP server exposes quota, including warning/danger states at 80%/95%. The storage summary opens a per-folder usage view backed by IMAP message sizes, and folder properties expose the measured size when the server permits the read. - Per-account mailbox behavior: 20/50/100/200 messages per page, date/sender/subject/size sorting in both directions, configurable mark-read delay, threaded/flat reader mode, HTML/plain preference, remote-image policy including Contacts-only loading, quote limiting, optional list snippets and a default “flagged on top” mode. Each folder can inherit, force-enable or force-disable flagged-first ordering; sorting is applied before pagination. - IMAP capability diagnostics for IDLE, MOVE, UIDPLUS, CONDSTORE, QRESYNC, SORT, THREAD, NAMESPACE, SPECIAL-USE, QUOTA, ACL, LIST-EXTENDED and UTF8=ACCEPT. - Forward-as-attachment using `message/rfc822`. diff --git a/src/imap/client.rs b/src/imap/client.rs index 9b47444..92b2f29 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -1373,6 +1373,9 @@ impl ImapClient for RealImapClient { } Err(_) => { tracing::warn!(folder = %folder, "fetch_folder_size timed out after 60s"); + return Err(ImapError::ConnectionFailed( + "folder-size FETCH timed out after 60s".into(), + )); } Ok(Ok(())) => session.recycle(), } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index b807599..d3d5257 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -582,6 +582,7 @@ pub fn create_router_with_storage(config: Arc, services: RouterServic delete(calendar::delete_meeting_template), ) .route("/quota", get(quota::get_quota)) + .route("/folders/{name}/size", get(quota::get_folder_size)) .route("/settings/reset", post(settings::reset_all_settings)); let diagnostics_routes = Router::new() diff --git a/src/routes/quota.rs b/src/routes/quota.rs index e361efa..c74ced2 100644 --- a/src/routes/quota.rs +++ b/src/routes/quota.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use std::time::Duration; +use axum::extract::Path; use axum::response::{IntoResponse, Response}; use axum::{Extension, Json}; use serde::Serialize; @@ -11,6 +12,46 @@ use crate::db; use crate::error::AppError; use crate::imap::client::{ImapClient, ImapCredentials}; +#[derive(Serialize)] +struct FolderSizeResponse { + size_bytes: Option, + available: bool, +} + +fn session_credentials(session: &SessionState) -> ImapCredentials { + ImapCredentials { + host: session.imap_host.clone(), + port: session.imap_port, + tls: session.imap_tls, + tls_verify: session.imap_tls_verify, + email: session.email.clone(), + password: session.password.clone(), + } +} + +pub async fn get_folder_size( + Path(name): Path, + Extension(session): Extension, + Extension(imap_client): Extension>, +) -> Result { + let creds = session_credentials(&session); + let result = crate::imap::pool::background(imap_client.fetch_folder_size(&creds, &name)).await; + let response = match result { + Ok(size_bytes) => FolderSizeResponse { + size_bytes: Some(size_bytes), + available: true, + }, + Err(error) => { + tracing::debug!(folder = %name, error = %error, "Folder size is unavailable from IMAP"); + FolderSizeResponse { + size_bytes: None, + available: false, + } + } + }; + Ok(Json(response).into_response()) +} + #[derive(Serialize)] struct QuotaResponse { usage_bytes: Option, @@ -23,14 +64,7 @@ pub async fn get_quota( Extension(config): Extension>, Extension(imap_client): Extension>, ) -> Result { - let creds = ImapCredentials { - host: session.imap_host.clone(), - port: session.imap_port, - tls: session.imap_tls, - tls_verify: session.imap_tls_verify, - email: session.email.clone(), - password: session.password.clone(), - }; + let creds = session_credentials(&session); let quota = imap_client.get_quota(&creds).await.unwrap_or(None); diff --git a/src/web/css/30-mail.css b/src/web/css/30-mail.css index e4be221..fabc959 100644 --- a/src/web/css/30-mail.css +++ b/src/web/css/30-mail.css @@ -882,8 +882,9 @@ } .message-icons { + position: relative; align-items: center; - min-width: 40px; + min-width: 0; justify-content: flex-end; } @@ -894,11 +895,20 @@ } .message-quick-actions { + position: absolute; + right: calc(100% + 4px); + top: 50%; + z-index: 2; display: inline-flex; align-items: center; gap: 1px; + padding: 1px; + border-radius: var(--radius-control); + background: var(--hover); opacity: 0; pointer-events: none; + transform: translateY(-50%); + white-space: nowrap; } .message-row:hover .message-quick-actions, @@ -1327,6 +1337,36 @@ body.reader-fullscreen-open { overflow: hidden; } +/* Popup message view: render only the reader surface, without briefly exposing the full mailbox shell. */ +html[data-mail-embed="true"] .app-view { + grid-template-rows: minmax(0, 1fr); +} + +html[data-mail-embed="true"] :is(.appbar, .offline-banner, .folder-pane, .message-pane, .folder-drawer-scrim) { + display: none !important; +} + +html[data-mail-embed="true"] .mail-shell { + display: block; + min-height: 0; + height: 100%; +} + +html[data-mail-embed="true"] .reader-pane { + display: block !important; + width: 100%; + height: 100%; + background: var(--panel); +} + +html[data-mail-embed="true"] .reader-content { + height: 100%; +} + +html[data-mail-embed="true"] #reader-fullscreen { + display: none; +} + .reader-pane.reader-fullscreen .reader-content { height: 100dvh; } @@ -1416,11 +1456,10 @@ body.reader-fullscreen-open { } } -/* Single-line rows when the message pane is wide enough. */ @container message-pane (min-width: 520px) { html[data-compact-wide-rows="true"] .message-row { min-height: min(var(--row-height), 36px); - grid-template-columns: 19px minmax(110px, .34fr) minmax(0, 1fr) auto auto; + grid-template-columns: 19px minmax(110px, .24fr) minmax(0, 1fr) auto auto; grid-template-rows: minmax(24px, auto); column-gap: 8px; row-gap: 0; @@ -1476,7 +1515,7 @@ body.reader-fullscreen-open { } html[data-compact-wide-rows="true"] .message-list.selection-mode .message-row { - grid-template-columns: 18px 19px minmax(110px, .34fr) minmax(0, 1fr) auto auto; + grid-template-columns: 18px 19px minmax(110px, .24fr) minmax(0, 1fr) auto auto; } html[data-compact-wide-rows="true"] .message-list.selection-mode .message-row .message-select { diff --git a/src/web/css/50-settings.css b/src/web/css/50-settings.css index bc81777..454b624 100644 --- a/src/web/css/50-settings.css +++ b/src/web/css/50-settings.css @@ -1565,6 +1565,150 @@ align-self: center; } +.storage-open-trigger { + cursor: pointer; +} + +.storage-open-trigger:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.quota-summary.storage-open-trigger:hover { + border-color: var(--accent); +} + +.storage-card { + width: min(720px, calc(100vw - 28px)); + max-height: min(760px, calc(100dvh - 28px)); +} + +.storage-overview { + display: grid; + gap: 10px; + padding: 16px; + border-bottom: 1px solid var(--line); + background: var(--panel-2); +} + +.storage-overview-head { + display: flex; + align-items: end; + justify-content: space-between; + gap: 18px; +} + +.storage-overview-head>div { + display: grid; + gap: 3px; + min-width: 0; +} + +.storage-overview-head strong { + font-size: 16px; + overflow-wrap: anywhere; +} + +.storage-overview-total { + text-align: right; +} + +.storage-overview-track, +.storage-folder-track { + overflow: hidden; + border-radius: 999px; + background: var(--panel-3); +} + +.storage-overview-track { + height: 9px; +} + +.storage-overview-track span, +.storage-folder-track span { + display: block; + width: 0; + height: 100%; + border-radius: inherit; + background: var(--accent); + transition: width .2s ease; +} + +.storage-folders-section { + display: grid; + min-height: 0; + padding: 14px 16px 6px; +} + +.storage-folders-heading { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: baseline; + padding-bottom: 10px; +} + +.storage-folder-list { + display: grid; + gap: 6px; + max-height: min(480px, 55dvh); + overflow: auto; + padding-right: 2px; +} + +.storage-folder-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 6px 12px; + padding: 9px 10px; + border: 1px solid var(--line); + border-radius: var(--radius-control); + background: var(--field); +} + +.storage-folder-name { + display: flex; + align-items: center; + min-width: 0; + gap: 7px; + font-weight: 650; +} + +.storage-folder-name svg { + width: 15px; + height: 15px; + flex: 0 0 auto; + color: var(--muted); +} + +.storage-folder-name span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.storage-folder-value { + color: var(--muted); + font: 11px var(--font-mono); + white-space: nowrap; +} + +.storage-folder-track { + grid-column: 1 / -1; + height: 6px; +} + +.storage-folder-row[data-state="unavailable"] .storage-folder-track span { + width: 0 !important; +} + +.storage-note { + margin: 0; + padding: 8px 16px 15px; + font-size: 11px; + line-height: 1.45; +} + .folder-properties-card { width: min(520px, calc(100vw - 28px)); } @@ -2686,6 +2830,7 @@ kbd { justify-content: flex-end; margin-top: 10px; } + .multi-account-notifications.hidden { display: none; } @@ -2699,8 +2844,6 @@ kbd { white-space: nowrap; } - -/* Theme schedule, account colour, contrast hint, list skeleton, theme cross-fade. */ .theme-schedule-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -2716,7 +2859,7 @@ kbd { align-content: start; } -.account-color-row > span { +.account-color-row>span { margin-bottom: 5px; color: var(--muted); font-size: 11px; @@ -2748,7 +2891,6 @@ kbd { grid-column: 1 / -1; margin: 0; padding: 6px 9px; - /* Fixed colours: must stay readable with any custom palette. */ border: 1px solid #d9a93a; border-radius: var(--radius-control); background: #fff4d6; @@ -2855,4 +2997,4 @@ html[data-reduce-motion="true"] :is(.skeleton-dot, .skeleton-line) { html.theme-transition * { transition: none !important; } -} +} \ No newline at end of file diff --git a/src/web/i18n/pl.json b/src/web/i18n/pl.json index 0f7107d..98a43f8 100644 --- a/src/web/i18n/pl.json +++ b/src/web/i18n/pl.json @@ -1279,6 +1279,21 @@ "private + public": "prywatny + publiczny", "public": "publiczny", "import": "import", + "View mailbox storage": "Pokaż wykorzystanie pamięci skrzynki", + "Server quota": "Limit serwera", + "Measured folders": "Zmierzono w folderach", + "Share of measured folder storage": "Udział w zmierzonej pamięci folderów", + "Folder sizes are calculated from message sizes reported by IMAP. Server quota can include additional data, so the totals may differ.": "Rozmiary folderów są obliczane z rozmiarów wiadomości raportowanych przez IMAP. Limit serwera może obejmować dodatkowe dane, dlatego sumy mogą się różnić.", + "Measuring…": "Obliczanie…", + "No folders available": "Brak dostępnych folderów", + "Size (IMAP)": "Rozmiar (IMAP)", + "Subscribed": "Subskrybowany", + "Messages": "Wiadomości", + "Delimiter": "Separator", + "Special role": "Rola specjalna", + "Attributes": "Atrybuty", + "Yes": "Tak", + "No": "Nie", "default": "domyślna" }, "fixed_interface_style_note": "gotowy styl interfejsu. Włącz edycję poniżej, aby dopasować jego układ i typografię.", @@ -1310,4 +1325,4 @@ "account_color_note": "Pasek u góry aplikacji i kropka przy adresie pokazują, które konto jest aktywne.", "contrast_warning": "Niski kontrast", "edited_interface_style_note": "gotowy styl interfejsu z Twoim układem. Zmiana koloru tworzy własną kopię RustBox." -} +} \ No newline at end of file diff --git a/src/web/index.html b/src/web/index.html index 49d0bcf..36b65ab 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -120,7 +120,8 @@ -
+
Mailbox storageLoading…
@@ -592,6 +593,27 @@
+ +
+
Mailbox storage
+
+
+
Server quotaLoading…
+
Measured folders0.0 MB
+
+ +
+
+
FoldersShare of measured folder storage
+
+
+

Folder sizes are calculated from message sizes reported by IMAP. Server quota can include additional data, so the totals may differ.

+
+
+
Settings