diff --git a/CHANGELOG.md b/CHANGELOG.md index 1510da3..77ebf34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,568 +1,292 @@ # RustBox changelog -### 0.2.25 +What changed in each RustBox release, written for the people who use and run it. Technical details are in the [documentation](docs/). -- Filters: **Apply now** runs a rule on mail that is already in a folder (Sieve itself only sees new mail). *Check matches* in the rule editor previews how many messages in the chosen folder match; each rule in the list has its own *Apply now*. Matching uses IMAP `UID SEARCH` on the server (`POST /api/filters/match`) with an exact lower bound and a superset upper bound; messages in between are decided from the local cache, and anything that still cannot be decided exactly is reported and left untouched. Flags and labels are applied first, then copies, then a single move. Redirect, discard and *set flags* are skipped for existing messages and listed in the confirmation. -- Filters: **exceptions** (*Except if …*, written as `allof (…, not anyof (…))` and read back by the editor), new conditions **To address**, **Cc address**, **Size larger/smaller than** (`500K`, `2M`, `1G`), **Has attachment** (Sieve `mime`) and **High priority**, and new actions **Add label** (RustBox label, via its IMAP keyword) and **Move to Trash**. -- Filters: **templates** at the top of the rule editor (sender to folder, subject words, VIP flag, newsletters, alias, large attachments, forward a copy), an offer to **create a missing target folder** when a rule moves mail to one, and a message context-menu entry **Always move messages from this sender…** that adds the rule and offers to apply it to the current folder. The quick filter dialog can also apply the new rule to existing messages. -- Filter summaries (*When / Then*, AND / OR / NOT, *except if*) are translated. -- Confirmation dialogs translate their button labels. -- Fixed editing a predefined style mixing two styles. *Allow editing predefined style* no longer turns the style into a RustBox custom copy on the first change: it copies the style's layout and typography into your settings and lets you adjust them while the style keeps its palette, shapes, tokens and gradients. Only changing a colour creates a RustBox custom copy, after a confirmation; the copy takes all current colours, layout and typography from the screen (never values saved earlier for another style) and fixes the theme to the current light/dark mode, because a palette copy belongs to one mode. Switching to another style locks editing again; changing the style depth re-seeds the layout from the same style. -- Fixed *Soft* contrast removing all borders when no palette overrode them (the line colour referred to itself, which CSS treats as invalid). -- The message context menu is shorter: forwarding, labels, snooze, filters, sender actions and technical tools (headers, source, Message-ID, .eml, print) are grouped into submenus. The menu and its submenus never exceed the window height (they scroll instead), so *Delete* stays reachable on small screens. -- No database migration (still V054; V055 is next). -- Bumped the application/source release to 0.2.25. +## 0.2.26 -### 0.2.24 +- 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. +- The About page was redesigned: author, repository, license and a short note on the project's origin. +- RustBox is now developed independently of Oxi, the project its first version was forked from. The tools for synchronising with Oxi were removed. +- New documentation: a complete feature list with screenshots, a rewritten README and this user-friendly changelog. +- Settings notes in Polish now appear in Polish right after the page loads. +- Code cleanup with no change in behaviour. -- Fixed **Style gradients** doing nothing at the default *Standard* depth: gradients were only part of the immersive tokens. They now live in a dedicated gradient layer (`59-skin-gradients.css`) that applies at every style depth. -- Every predefined style except the RustBox custom style now has gradients behind the switch — app-bar gloss, toolbar tint, sidebar fade, selection sweep, glossy primary/compose buttons and an accent backdrop glow — with signature gradients for Roundcube Classic/Larry, GitHub, Discord, Reddit, Yahoo, Dracula, Tokyo Night, Synthwave, Aqua, Terminal, Retro 95 and others. -- Added retro styles: **Windows XP** (Luna title bar, green Start-style compose button, task-pane sidebar), **Mac OS X Aqua** (pinstripes, gel buttons, Aqua selection), **iOS 6** (glossy navigation bar, linen, glossy buttons), **Windows 7 Aero** (Harmony backdrop, glass app bar), **Frutiger Aero** (sky-and-grass backdrop, bubble buttons) and **BeOS** (yellow tab app bar, bevelled controls). -- Added a **By time of day** theme: light between two configurable times, dark otherwise (`theme_light_from` / `theme_dark_from`, migration `V054`). -- Added an optional **account colour** shown as a stripe on the app bar and a dot next to the address (`account_color`, `V054`); display preferences are per account, so each account can have its own. -- Custom colours now show a live **low-contrast warning** based on WCAG thresholds. -- The message list shows **skeleton rows** while a folder loads, and switching theme or style cross-fades briefly (off with reduced motion). -- Fixed the default RustBox selection colour being dark in the light theme. -- Fixed subjects being squeezed to a few characters by the snippet in message rows. -- Flagged messages are marked with a red side bar instead of a bottom line. -- Reader-toolbar button labels now collapse below 820px instead of making the toolbar scroll. -- Calendar: added an optional **second reminder** per event (for example *1 day before*; choices from *at start* to *1 week before*) and **default reminders for new events** in Settings → Notifications. All-day events are reminded at the start of the working day (default 09:00), so "1 day before" fires the previous morning. Both reminders are exported to ICS. Migration `V054`. -- Fixed clearing an event reminder: choosing *None* in the editor now removes it instead of keeping the old value. -- The interface-style list is grouped into categories (RustBox & classic, Roundcube, mail apps, apps, editor themes, atmospheric, retro) and uses short, consistent names in English and Polish (e.g. *Gmail*, *Windows XP*, *Czysty*, *Papier*). -- `skin_depth` and `skin_gradients` remain in migration `V053`. -- Bumped the application/source release to 0.2.24. +## 0.2.25 -### 0.2.23 +- **Filters – Apply now:** a rule can be run on mail already in a folder, not only on new mail. "Check matches" shows how many messages a rule would affect; each rule in the list has its own "Apply now". Messages that cannot be checked with certainty are left untouched. +- **Filters – more options:** + - exceptions ("except if…"); + - new conditions: To only, Cc only, message size, has attachment, high priority; + - new actions: add a label, move to Trash. +- **Filters – easier to create:** + - ready-made templates; + - an offer to create a missing target folder; + - "Always move messages from this sender…" in the message menu; + - the quick filter can also be applied to messages already in the folder. +- Filter descriptions and confirmation buttons are translated. +- The message right-click menu is much shorter thanks to submenus and always fits the window, so Delete is always reachable. +- Fixed editing a predefined style mixing two styles together. Its layout can now be adjusted while it keeps its look, and only a colour change creates a custom copy (after asking). +- Fixed the "Soft" contrast option removing all borders. +- Fixed subfolders appearing in the wrong place in the folder list; they now appear directly under their parent folder. -- Added **Style depth** to Appearance (`skin_depth`, migration `V053`). *Colors only* applies just a predefined style's palette and keeps every layout control editable; *Standard* (default) is the previous behavior — palette plus the style's fixed layout profile; *Immersive* additionally reshapes the interface with the style's own typography, control/button/avatar shapes, floating or flush panes, app bar and search field, sidebar colors, selection markers, card-style rows, primary/compose buttons and backdrop (gradients, glass blur, bevels). Floating-pane geometry applies only to the desktop three-pane layout; phones and tablets keep the drawer layout. -- Added a separate **Style gradients** switch to Appearance (`skin_gradients`, part of migration `V053`, default off). All gradients used by styles — immersive backdrops, app bars, primary buttons, the Synthwave logo and the Roundcube Classic bevelled toolbars/buttons — are applied only when it is on; otherwise each style uses flat colors. Note: Roundcube Classic is therefore flat by default until the switch is enabled. -- Introduced a common interface-style standard. Every predefined style now ships light and dark palettes, a fixed layout profile and a structural token block (`57-skin-tokens.css`, consumed by `58-skin-immersive.css`). All 23 existing styles were adapted to it — for example GitHub gets its dark header and green primary button, Slack its aubergine sidebar, Gmail its pill navigation and light-blue compose button, Outlook its accent app bar and selection bar, Apple Mail its filled selection. -- Added 14 predefined styles: Dracula, Catppuccin, Solarized, Gruvbox, Tokyo Night, Rosé Pine, Everforest, Synthwave, Paper, Terminal, Aqua Glass, Linear inspired, Material You inspired and Retro 95. -- The interface-style catalog is defined once (`display_preferences::INTERFACE_SKINS`) and shared by the user preference and `default_interface_skin` validation. The release validator now checks that every style is present in the catalog, the settings selector, the layout profiles, both palettes and the token table. -- Consolidated migrations V051 and V052 into the fresh schemas; the compatibility baseline is now V052. Databases below V052 must be opened once with 0.2.22 before upgrading. -- Fixed the fresh MySQL schema: `mail_preferences.message_loading_mode` used a literal `DEFAULT` on a `TEXT` column, which MySQL rejects; it now uses an expression default. -- Bumped the application/source release to 0.2.23. +## 0.2.24 -### 0.2.22 +- The **Style gradients** switch now works at every style depth, and every predefined style has optional gradients. +- New retro styles: Windows XP, Mac OS X Aqua, iOS 6, Windows 7, Frutiger Aero and BeOS. +- New **time-of-day** theme: light during the day, dark in the evening, at times you choose. +- Optional **account colour**, shown as a stripe at the top and a dot next to the address. +- A warning when your custom colours make text hard to read. +- Placeholder rows while a folder loads, and a smooth transition when the theme or style changes. +- Calendar: a **second reminder** per event (for example one day before) and default reminders for new events. All-day events are reminded at the start of the working day. +- The style list is grouped into categories and uses short names in English and Polish. +- Fixes: + - a dark selection colour in the light theme; + - subjects squeezed by previews in the message list; + - the flagged-message marker; + - the reader toolbar on narrow screens; + - clearing an event reminder. -- Added message snooze. A new `snoozed_messages` table (migration `V052`) records a wake time for a message; snoozing it (via the new "Snooze…" context-menu entry or the `b` shortcut while a message is open) hides it client-side from its folder's current view until that time, using `GET /folders/{name}/snoozed-uids` to know which UIDs are currently asleep. `POST /messages/{folder}/{uid}/snooze` and `DELETE /messages/{folder}/{uid}/snooze` set/clear a snooze; `GET /messages/snoozed` lists everything currently sleeping, shown in the new "View snoozed messages…" dialog (`Shift+B`). This is purely a local cache: it has no IMAP side effects and does not change server-side folder pagination/count queries. -- Added an "Undo Send" window. New `undo_send_window_secs` config setting (default `8`, max `120`; `0` restores the previous immediate-send behavior). When enabled, submitting a message stages it in memory instead of sending it right away; the composer shows a toast with an "Undo" action and countdown, and the message is only dispatched to SMTP once the window elapses without being cancelled. `GET /messages/send/{pending_send_id}` polls status (`pending`/`dispatching`/`sent`/`cancelled`/`failed`); `DELETE /messages/send/{pending_send_id}` cancels a still-pending send. Nothing is persisted, so a server restart during the window drops the staged send the same way an in-flight SMTP connection would be lost. -- Bumped the application/source release to 0.2.22. +## 0.2.23 -### 0.2.21 +- **Style depth:** choose whether a predefined style changes only colours, also the layout, or the whole look ("immersive": shapes, fonts, panes, app bar and backdrop). +- A separate **Style gradients** switch. +- All styles were brought to a common standard with light and dark versions. +- 14 new styles: Dracula, Catppuccin, Solarized, Gruvbox, Tokyo Night, Rosé Pine, Everforest, Synthwave, Paper, Terminal, Aqua Glass, Linear, Material You and Retro 95. +- Database upgrade history was consolidated. Very old installations must first be upgraded with version 0.2.22. +- Fixed creating a new MySQL database. -- Added configurable login brute-force protection under `[login_rate_limit]` (env: `LOGIN_RATE_LIMIT__*`): the per-IP token bucket on `POST /api/auth/login` is now driven by `ip_burst`/`ip_replenish_secs` instead of hard-coded values, plus failed-attempt lockouts per mailbox (`account_max_failures`, any IP) and per client IP (`ip_max_failures`, password spraying) with exponential back-off (`lockout_secs` → `max_lockout_secs`). Locked attempts return `429` with `Retry-After` before any IMAP connection is opened. -- The public `GET /api/auth/policy` no longer consumes the per-IP login budget. -- Added `cookie_secure = "auto" | "always" | "never"` (env: `COOKIE_SECURE`). The default `auto` sets the `Secure` cookie attribute only when the browser actually reached RustBox over HTTPS (trusted-proxy `X-Forwarded-Proto`/`Forwarded`/`X-Forwarded-Ssl`, otherwise the `Origin`/`Referer` scheme). Previously `Secure` depended on `environment != "development"`, which broke logins on plain-HTTP production installs and left cookies without `Secure` in development behind HTTPS. -- Added memory guards for messages with attachments. An attachment set that cannot fit `max_message_size_mb` once base64-encoded is now rejected with `413` from the stored sizes before any blob is read when sending, saving an IMAP draft snapshot or attaching a message, and while an upload is still streaming (previously the limit was only checked after the whole MIME message had been built in RAM). New `max_concurrent_message_builds` (default 4) and `message_build_wait_secs` (default 30) limit how many requests hold attachment data in memory at once; extra requests wait and then receive `503`. Environment: `MAX_CONCURRENT_MESSAGE_BUILDS`, `MESSAGE_BUILD_WAIT_SECS`. -- Gmail-style message row opening: outside selection mode, Ctrl/Cmd+click a message opens it in a new browser tab, and Shift+click opens it in a small popup window; both deep-link to the exact message so the new tab/window loads it directly. Plain click and existing selection-mode shift/ctrl range/toggle behavior are unchanged. -- Bumped the application/source release to 0.2.21. +## 0.2.22 -### 0.2.20 +- **Snooze:** hide a message until a chosen time and see all snoozed messages in one list. +- **Undo Send:** a few seconds to cancel a message after pressing Send (the length can be set by the administrator). -- Fixed interrupted initial-backfill recovery so legacy missing-date repair does not run against an invalidated or incomplete folder cache. -- Fixed the `Forward as attachment` route build failure by making `AttachMessageSourceRequest` visible at the same crate scope as the public route handler, so Axum can resolve the handler type from `routes::mod`. -- Fixed messages without a usable RFC 5322 `Date` header so mailbox synchronization falls back to the IMAP `INTERNALDATE`; cached messages with missing/invalid dates are repaired when the folder is opened instead of remaining at the end of the list. -- Fixed `Forward as attachment` so the server fetches the original RFC822 source from IMAP and stores it in the draft as a real `message/rfc822` `.eml` attachment. -- Bumped the application/source release to 0.2.20. +## 0.2.21 -### 0.2.19 +- Stronger protection against password guessing: repeated failures lock the mailbox or address for a growing time, before the mail server is even contacted. +- Login works reliably on installations without HTTPS, while secure cookies are still used whenever HTTPS is available. +- Memory protection when sending large attachments: oversized messages are rejected early and only a limited number are prepared at the same time. +- Ctrl/Cmd-click opens a message in a new tab, Shift-click in a small window. -- 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.20 -### 0.2.18 +- Messages without a date now get the date when they were received instead of landing at the end of the list. +- "Forward as attachment" attaches the original message exactly as received. +- Fixed recovery after an interrupted first synchronisation. -- 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. -- Restored visible signatures/footers for new messages when no identity was explicitly marked default by using the same preferred-identity fallback for signature insertion and sending. -- Added explicit composer undo/redo keyboard handling: Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z and Ctrl+Y. -- Improved adaptive HTML-mail rendering for legacy/marketing templates: preserve safe CSS from ``, retain body presentation metadata, adapt HSL/HSLA and background shorthands with URLs, enforce readable text contrast and remove the hard-coded white iframe canvas. -- Suppress realtime new-mail notifications caused by intentional message moves (including Spam/Junk) by tracking the destination UID while retaining normal notifications for unrelated incoming mail. -- Fixed visible-signature image materialization so cleanup waits for the in-flight CID upload instead of deleting the signature image while the composer is opening. -- Added a top-bar quick account switcher that appears when more than one account is signed in to the current browser session. -- Added browser-level combined notification selection in Account settings; selected accounts keep their own notification folder, sound and DND preferences. -- Added administrator feature gate `multi_account_enabled`; disabling it hides multi-account controls, rejects adding a second account and limits realtime WebSocket monitoring to the active account. -- Made the plain-text and HTML signature editors the same visual height. -- Added a dedicated HTML signature source modal with sanitized source import, visual preview/editing, formatting controls and image insertion. -- Kept the global synchronization spinner active for mailbox page loads, progressive/full-folder loading and individual message/IMAP-draft retrieval. -- Added Shift-click range selection across the currently displayed message list, using the last regular/toggle selection as the anchor. -- Added first-page and last-page controls to mailbox pagination. -- Hide HTML inline/CID resources, including images inserted through signature management, from the reader attachment list while continuing to resolve them inside the message body. -- Treat MIME parts explicitly marked `Content-Disposition: inline` as inline content rather than message attachments, preventing signature-only messages from gaining an attachment indicator after the next IMAP metadata sync. -- Retain signature images as `multipart/related` CID parts for outgoing mail; regular file attachments remain unchanged. -- Kept the application/source release at 0.2.18. +## 0.2.19 -### 0.2.16 +- HTML messages use all the available height in the reader and adapt when the window or layout changes. -- Added opt-in adaptive HTML-mail colors that adjust neutral backgrounds and low-contrast text to the active light/dark theme while preserving layout, images and brand colors. -- Added mailbox pagination, enabled by default at 300 messages per page, with optional smooth progressive loading or full-folder loading. -- Added runtime schema migration V051 for SQLite, MySQL and PostgreSQL. Existing V050 databases gain `display_preferences.adaptive_email_colors` and `mail_preferences.message_loading_mode` before the new preferences are queried. -- Kept V050 as the consolidated compatibility baseline and removed the standalone V050 migration files because those changes are already present in the complete `schema.sql` definitions. -- Bumped the application/source release to 0.2.16. +## 0.2.18 -### 0.2.15 -- Fixed HTML signature editor container semantics so clicks on whitespace/outside the editable area no longer implicitly activate the Bold toolbar button. +- Better choice of the sender identity and signature for new messages. +- Undo and redo shortcuts in the editor. +- Better display of older and marketing HTML e-mails in light and dark themes. +- Moving a message no longer triggers a "new mail" notification. +- Quick account switcher in the top bar when several accounts are signed in, and combined notifications from selected accounts. The administrator can turn multi-account off. +- Signature editor improvements: equal-height fields, a source view and image insertion. +- Shift-click selects a range of messages, and the list has first/last page buttons. +- Images embedded in signatures are no longer shown as attachments. -- Fixed the HTML signature editor so toolbar actions preserve the active text selection and only run on an actual selection; collapsed-caret clicks no longer enable sticky bold/italic/underline typing modes. -- Fixed visible signatures in new messages by creating an editable body entry before the signature instead of placing the signature at the top of an otherwise empty editor. -- Refined the visible-signature separator with more breathing room and a subtler solid rule so it reads as a boundary without dominating the composer. -- Strips the editor-only body-entry marker from outgoing HTML while retaining the visible-signature marker used for de-duplication. -- Consolidated migrations V047-V050 into the V050 compatibility baseline and removed their standalone migration files. Databases older than V050 must be upgraded through a V050-capable release first. -- Bumped the application/source release to 0.2.15. +## 0.2.16 -### 0.2.14 +- Optional adaptive colours for HTML e-mails, so they fit the dark or light theme. +- Paged message list (300 per page by default), with optional smooth loading or loading the whole folder. -- Made pane resizing tolerant of frontend/backend range mismatches: pane widths are normalized server-side, and a rejected width silently disables the in-view resizer for the current session instead of showing a validation error. -- Added a default-on compact message-list layout that consolidates sender, subject/snippet, date and status icons into one row when the message pane is wide enough; it can be disabled in Appearance settings. -- Added per-identity signature insertion mode: `silent` (default) keeps the composer clean and appends the signature during send, while `visible` inserts an editable/removable signature in the composer. -- Visible HTML signatures are structurally marked for de-duplication and keep inline signature images attached through the existing CID flow. -- Added runtime schema migration V050 for SQLite, MySQL and PostgreSQL (`mail_preferences.compact_wide_rows`, `identities.signature_insert_mode`). V051 is the next migration number. -- Bumped the application/source release to 0.2.14. +## 0.2.15 -### 0.2.13 +- Fixed sticky bold/italic formatting in the signature editor. +- New messages with a signature get an empty line to type in above it, and a subtler separator. -- Added durable per-tenant blob deletion outbox for every configured blob backend (`filesystem` including shared NFS/POSIX, and S3-compatible storage). Attachment metadata deletion and cleanup enqueue now commit atomically, so a temporary storage failure cannot silently orphan blobs. -- Blob cleanup runs immediately when possible and is also retried by a background worker with claim leases and exponential backoff; missing objects are treated as successful idempotent deletes and retries do not dead-letter permanently. -- Deleting one attachment, deleting a whole draft, and post-send draft cleanup all use the same durable deletion path. Failed/finalized uploads and failed storage-migration destination objects are queued for cleanup as well. -- Storage health probes reuse a fixed probe key so a failed cleanup cannot accumulate unbounded health-check objects. -- Added runtime schema migration V049 for SQLite, MySQL and PostgreSQL (`blob_delete_outbox`). V050 is the next migration number. -- Bumped the application/source release to 0.2.13. +## 0.2.14 -### 0.2.12 +- Compact single-line message rows when the list is wide (can be turned off). +- Per identity, choose whether the signature is visible while writing or added only when sending. +- Resizing panes no longer shows errors. -- Saving or updating a draft no longer raises a realtime “new message” toast/system notification for the Drafts folder; the folder/list still refreshes normally, and IMAP snapshots remain `\Draft` + `\Seen`. -- Added a composer `Delete draft` action that permanently removes the current IMAP draft and cascades cleanup through the local autosave row, draft attachment rows and uploaded attachment blobs; IMAP deletion is verified before local state is reported as deleted. -- IMAP-only drafts opened from other mail clients can be deleted without requiring a pre-existing RustBox draft database row, while UIDVALIDITY and `\\Draft`/Drafts-folder checks prevent deleting a stale or non-draft message by mistake. -- Explicit `Save draft` now records the newly appended IMAP UID in the live composer even for a brand-new draft, so later Save/Delete/Send operations consistently target the current server snapshot. -- Draft messages in the configured IMAP Drafts folder (or carrying the `\\Draft` flag) now open directly in the normal composer from their server-side RFC822/MIME content; opening does not depend on a RustBox draft database row or `X-RustBox-Draft-ID`, so drafts created by other mail clients can be edited and sent. -- Editing an IMAP-sourced draft keeps its original server UID as the replacement source: explicit Save draft appends the new MIME snapshot first and removes the superseded IMAP draft only after the new UID is resolved; sending also removes the source draft after successful SMTP delivery. -- Existing IMAP draft attachments stay usable directly from their MIME parts while editing, can be removed/re-embedded without first copying the whole message into the draft database, and are carried into Save/Send together with newly uploaded local attachments. -- Inline MIME parts now preserve their original filenames through `Content-Disposition: inline; filename=...` when saving drafts and sending messages, improving Roundcube/Thunderbird interoperability. -- IMAP draft reopening restores To/Cc/Bcc, Subject, plain/HTML body, reply threading headers, priority, MDN state, From identity when it matches a configured identity, inline CIDs and attachment filenames directly from the server message. -- Fixed manual IMAP draft saving for drafts without To/Cc/Bcc recipients by using a build-only envelope while keeping recipient headers empty; Bcc is retained in saved draft snapshots. -- IMAP draft saving now honors the per-account Special folders → Drafts mapping and `default_drafts_folder`, then falls back to the IMAP `\Drafts` attribute and common `Drafts`/`Draft` folder names. -- Added an explicit `Save draft` composer action immediately before `Cancel`; it forces the current message and attachments to be persisted as a real IMAP `\Draft` snapshot while leaving background autosave behavior unchanged. -- Manual IMAP draft saves now report a failure when RFC822 snapshot creation or IMAP APPEND fails instead of showing a misleading success state. -- Fixed draft recovery so attachment metadata is restored from both the browser recovery snapshot and the authoritative server draft. -- Composer attachment chips now show every uploaded file, including inline images/links, instead of hiding embedded items from the attachment list. -- Adding another attachment now merges by unique attachment ID and refreshes the server-side draft attachment list, preventing an existing chip from appearing to be replaced. -- Inline attachment chips expose an attachment-only action that removes the inline occurrence without deleting the uploaded file. -- Stretched `Load more` and `Load all` across the full message-list action area with equal-width controls. -- Added a composer `Hide fields` / `Show fields` toggle that collapses From/To/Cc/Bcc/Subject/options so the editor can use the freed vertical space; invalid recipient input automatically expands the fields again. -- Fixed inline image insertion so adding another image or attachment appends a new element instead of replacing the current selection. -- Replaced deprecated `execCommand("insertHTML")` attachment insertion with DOM Range insertion and safe caret placement. -- Removed filename/size attachment de-duplication so separate files with identical metadata can still be attached independently. -- Moved inline-image resize/alignment controls into a floating contextual panel so selecting an image no longer reduces the composer editing area. -- Added mouse/touch resize handles around the selected inline image while preserving its aspect ratio. -- Inline images can be dragged to another caret position inside the message body. -- Pasting PNG/JPEG/GIF/WebP files with Ctrl/Cmd+V embeds them inline, and dropping files into the editor uploads them at the drop position. -- Attachment chips explicitly identify regular/image attachments, while selected embedded images are labelled `Inline image`. -- Made attachment re-embed actions compact icon buttons to keep the attachment strip readable. -- Collapsed large To/Cc recipient lists in the message reader into a compact summary with on-demand expansion. -- Added a `Load all` action beside `Load more` in the message list. -- Renamed the reader toolbar action from `Message details` to `Details`. -- Images added to HTML messages now embed inline by default; removing an inline image keeps the underlying file available as an attachment that can be embedded again. -- Added `Embed` actions for attachments: images are inserted inline and other files can be inserted as CID links in the message body. -- Added quick inline-image sizing and alignment controls, plus the existing advanced crop/resize editor and an explicit `Keep as attachment` action. -- No schema migration is required; V049 remains the next migration number. +## 0.2.13 -### 0.2.11 +- Deleted attachments are always removed from storage, even after a temporary storage failure; cleanup is retried automatically. -- No schema migration is required; V049 remains the next migration number. -- RoundCube migration script -- Allow to edit all themes. +## 0.2.12 -### 0.2.10 +- **Drafts:** + - drafts from the Drafts folder, including ones created in other mail clients, open directly for editing; + - new "Save draft" and "Delete draft" buttons; + - saving a draft no longer causes a "new mail" notification. +- **Composer:** + - images can be pasted, dragged into place, resized with handles, aligned and cropped; + - added images are embedded in the text by default; + - attachments can be embedded or inserted as links; + - the address fields can be hidden to give the editor more room. +- Long recipient lists are collapsed in the reader. +- A "Load all" button next to "Load more" in the message list. -- Message-body links without an explicit `target` now open in a new browser tab/window instead of navigating inside the sandboxed mail preview iframe. -- Explicit link targets from the message are preserved; links that open a new tab/window receive `rel="noopener noreferrer"`. -- Authentication success toast now appears only after a real login request; restoring an existing session after page refresh stays silent. -- Improved iPhone/Safari mail rendering by explicitly declaring UTF-8 in message preview documents and using broader RFC 2047 header decoding (including ISO-8859-2/Windows-1250) for non-ASCII subjects/sender names. -- Changed PDF attachment preview to use the authenticated inline attachment URL instead of a `blob:` iframe, improving Safari/iPhone compatibility. -- Simplified Composing size-limit information to one compact line with only the attachment and complete-message limits. -- No schema migration is required; V049 remains the next migration number. +## 0.2.11 -### 0.2.9 +- Import of contacts and identities from Roundcube. +- All predefined styles can be edited. -- Made realtime new-mail notifications less aggressive: startup synchronization is silent, notifications are armed only after the application is open, and bulk move/copy/archive/junk/delete destinations suppress self-generated `NewMessages` toasts while the bulk summary toast remains. -- Added a success toast after a completed authenticated startup/login. -- Restored browser/system modifier shortcuts by limiting RustBox single-key shortcuts to unmodified keys; `Ctrl/Cmd+C`, `Ctrl/Cmd+V`, `Ctrl/Cmd+X`, `Ctrl/Cmd+A`, `Ctrl/Cmd+F`, `Ctrl/Cmd+R` and `Ctrl/Cmd+N` are no longer intercepted by mail actions. -- Changed the default messages-per-page setting from 50 to 300 and raised the supported folder-page limit to 300. -- No schema migration is required; V049 remains the next migration number. +## 0.2.10 -### 0.2.8 +- Links in messages open in a new tab. +- Better display of Polish and other non-English characters in subjects and sender names, especially on iPhone. +- PDF preview works on iPhone and in Safari. +- The "logged in" message appears only after a real login. -- Scoped default logging so RustBox can run at DEBUG/INFO without enabling verbose dependency logs; Tantivy merge/GC/file-watcher noise and its internal filesystem/source paths are hidden unless explicitly enabled through a custom `RUST_LOG` target filter. Development and deployment examples now use target-scoped filters. -- Kept administrator diagnostics process-local and cross-platform: removed host CPU/RAM/load-average and Linux `/proc` sampling; runtime diagnostics now rely on portable RustBox in-memory counters plus the process PID. -- Added a disabled-by-default administrator system-diagnostics endpoint with loopback-only CIDR allowlisting, mandatory token or Basic authentication, fail-closed trusted-proxy handling, secret redaction, live SQL/session/storage/search probes, active IMAP IDLE/session/runtime state and bounded security telemetry for failed logins, rate limiting, access denials and server errors. -- Moved the synchronization/index activity indicator to the left of Contacts. -- Corrected production TOML section ordering so intended top-level database/session/mail/security/special-folder settings are no longer accidentally parsed as search settings. -- Refined folder counters so only the unread value is emphasized, hid the sync/index activity icon when idle, and made manual search-index rebuilds drive the same live activity indicator as background indexing. -- Made folder totals/unread counts server-authoritative: IMAP `STATUS MESSAGES/UNSEEN` drives badges, selected-mailbox fallback counts `UID SEARCH UNSEEN`, periodic reconciliation repairs counters even when `HIGHESTMODSEQ` is unchanged, and mutation-time compare-and-set/retry prevents out-of-order STATUS responses from rolling counters backwards. -- Decoupled counter refresh from folder-tree discovery: a forced counter refresh reuses a fresh cached folder tree and batches STATUS requests instead of forcing another IMAP LIST. -- Changed read/unread/flag updates to patch the affected message row and folder badge live; ordinary flag changes no longer rebuild the message list, removing the reader/list jump during mark-read operations. -- Added responsive folder counters (`total / unread` when unread is non-zero, total-only at zero, compact mode in narrow sidebars) and surfaced IMAP header-cache plus search-index backlog in the top-right activity indicator/tooltip. -- Extended the opt-in security-header middleware with a private `/api` profile (`no-store`, `nosniff`, same-origin CORP, `frame-ancestors 'none'` and `X-Frame-Options: DENY`), while keeping Permissions-Policy/COOP and the full document CSP on HTML and HSTS at the public HTTPS edge; disabling `security_headers_enabled` removes the whole RustBox-added security profile. -- Minimized unauthenticated `/api/auth/policy` to `rememberMeEnabled`; optional feature policy moved behind authenticated `/api/auth/session-policy`, and session/remember durations are no longer exposed by policy endpoints. -- Added optimistic read/unread updates in the browser with guarded rollback, followed by authoritative server reconciliation; normal read/unread operations no longer replace the message list. -- Fixed realtime WebSocket deduplication so unique server `eventId` values always win over payload fingerprints; rapid legitimate counter cycles such as `1 → 0 → 1` are no longer dropped as duplicates. Active-folder total text is patched live together with the sidebar counter. -- Hardened IDLE/CONDSTORE reconciliation: counter STATUS no longer advances the flag-sync cursor, non-CONDSTORE servers fall back to a full `UID FLAGS` comparison, partial IMAP fetch streams are discarded instead of being treated as complete, and IDLE header/index writes participate in the same cache-generation transaction. -- Fixed UIDVALIDITY reset writes to stay inside the same database transaction on SQLite, MySQL and PostgreSQL instead of mixing transaction/base-connection writes. -- Tightened the HTML CSP to `style-src 'self'` plus the narrower `style-src-attr 'unsafe-inline'`; inline JavaScript remains disallowed. -- Added regression coverage for authoritative unread refresh, API/document/static header profiles and pre-login policy minimization. -- Added database migration V048 on SQLite, MySQL and PostgreSQL: folder counters now carry a monotonic `counter_generation`, so delayed STATUS responses cannot win an ABA race after newer read/unread activity. V049 is the next migration number. -- Bumped the application/source release to 0.2.8. +## 0.2.9 -### 0.2.7 -- Made opt-in browser security headers content-type-aware instead of attaching document-only policies to every response. -- HTML documents keep the full CSP, clickjacking, permissions, COOP, referrer and MIME-sniffing protections. -- CSS responses now receive only the applicable `Referrer-Policy` and `X-Content-Type-Options`; JavaScript, JSON, SVG, API and other non-document responses retain `X-Content-Type-Options: nosniff` without redundant document policies. -- Added regression coverage for HTML, CSS and API header sets while preserving the disabled-by-default compatibility switch. -- No schema migration is required; runtime schema remains V047 and V048 remains the next migration number. +- Fewer unnecessary notifications: nothing at startup and nothing for your own bulk actions. +- Browser shortcuts such as copy, paste, find and reload are no longer captured by RustBox. +- 300 messages per page by default. -### 0.2.6 -- Fixed iPhone/iOS reader toolbar popovers (`Message details` and `Move / copy`) by rendering the open menu outside the horizontally scrolling toolbar and positioning it against the visual viewport. -- Made folder counters and message-list refreshes more responsive: realtime sync stores the server-reported unread count, folder hierarchy refresh avoids unnecessary IMAP LIST calls, and manual refresh forces the active-folder status check in parallel with counter refresh. -- Improved realtime mail delivery: INBOX remains on IMAP IDLE, the actively viewed folder gets its own IDLE watcher, other folders are reconciled immediately and every 30 seconds, and newly detected headers are cached before realtime events are emitted. -- Added an in-app toast for new mail in any folder and deduplicated browser notifications per account/folder/UID to prevent repeated notifications for the same message. -- Added an animated indexing indicator beside Contacts while deep message indexing is active, and prevented excluded Trash/Junk/Spam folders from starving the indexing batch. -- No schema migration is required; runtime schema remains V047 and V048 remains the next migration number. +## 0.2.8 -### 0.2.5 -- After moving or deleting the currently opened message, RustBox now automatically opens the next message in the current list; if the removed message was last, it falls back to the previous message. -- The behavior applies to Spam, Not spam, Archive, Move and Delete actions and keeps the reader open while advancing. -- No schema migration is required; runtime schema remains V047 and V048 remains the next migration number. +- Folder counters are always accurate, and marking messages as read updates them instantly without reloading the list. +- An activity indicator shows synchronisation and indexing progress. +- An administrator diagnostics endpoint, disabled by default and protected by an address list and password or token. +- Stronger optional security headers, and less information published before login. +- Quieter logs by default. -### 0.2.4 -- Fixed S3-compatible storage over explicitly allowed plain HTTP endpoints by applying `storage.allow_http` to the final `ClientOptions` object instead of setting it before `with_client_options()` replaces those options. This fixes Garage/MinIO/Ceph-style internal HTTP endpoints without weakening the HTTPS default. -- Added an HTTP S3 regression test that verifies a request reaches an explicitly allowed local S3-compatible endpoint. -- Updated the pinned Rust toolchain/MSRV to Rust 1.99.1. -- Updated RustBox Docker build/runtime images from Debian 12 Bookworm to Debian 13 Trixie (`rust:1.99.1-trixie`, `debian:trixie-slim`) and switched the runtime OpenSSL package to Trixie's `libssl3t64`. -- Updated PostgreSQL 17 Compose images from Bookworm to the supported Trixie variant. -- No schema migration is required; runtime schema remains V047 and V048 remains the next migration number. +## 0.2.7 -### 0.2.3 -- Fixed API fallback routing so administratively disabled or otherwise unknown `/api/*` endpoints return HTTP 404 instead of being caught by the SPA fallback and returning the UI with HTTP 200. -- Added administrator feature switches for ManageSieve, diagnostics and About; disabled Sieve/diagnostics routes are not registered, and the matching settings UI is removed while keyboard shortcuts remain available. -- Added a protected “Reset all settings” action that restores display, mailbox, notification, folder, special-folder, calendar and OpenPGP preferences without deleting mail, identities, keys, templates or trusted-sender data. -- Added opt-in browser security response headers (CSP, frame, referrer, permissions and cross-origin opener policies); the feature is disabled by default for compatibility. -- Added a Docker HA example with two RustBox application nodes behind Envoy round-robin load balancing, Valkey-backed sessions, PostgreSQL, OpenSearch and Garage S3 object storage. Envoy adds `X-RustBox-Backend` to responses. -- Added current pinned Garage, Valkey and Envoy images to the HA example and an idempotent Garage bucket/key initialization helper. -- No schema migration is required; runtime schema remains V047 and V048 remains the next migration number. +- Security headers are tailored to each type of response. -### 0.2.2 -- Fixed the SQLite isolation regression test to initialize `AppConfig` with a struct literal plus `..Default::default()`, satisfying `cargo clippy --all-targets -- -D warnings` without suppressing `field_reassign_with_default`. -- Fixed Rust test isolation for SQLite: test-only `db::pool::configure()` still initializes the requested database schema but no longer pins the first temporary SQLite path in the process-wide `OnceLock`, preventing later tests from reusing a deleted `TempDir` and collapsing into HTTP 500/database-open failures. -- Added a regression test that configures SQLite in one temporary directory, removes it, then opens an account database in a second temporary directory. -- Strengthened the offline release contract to assert that `scripts/build-release.sh` requires the full locked test suite, warning-free Clippy, and a locked release build before a release is accepted. -- Fixed the PostgreSQL startup schema compile failure (`implementation of sqlx::Executor is not general enough`) by executing the multi-statement DDL batch through `sqlx::Executor::execute()` on the transaction connection instead of `sqlx::raw_sql(...).execute(&mut PgConnection)`. -- Preserved transactional startup schema initialization and the local `client_min_messages=WARNING` scope used to suppress expected idempotent PostgreSQL NOTICE output. -- Added a static regression gate that forbids the lifetime-problematic `raw_sql(&schema).execute(&mut *tx)` PostgreSQL startup path. -- No schema migration is required; runtime schema remains V047 and V048 remains the next migration number. -- Fixed PostgreSQL header-sync invalidation used by message mutations (including marking/moving messages as spam): the UPSERT now qualifies the existing tenant `user_meta.value`, avoiding `column reference "value" is ambiguous` and HTTP 500 responses. -- Added a regression check for tenant SQL rewriting of the qualified `user_meta.value` reference and a static audit covering all PostgreSQL `ON CONFLICT ... DO UPDATE` target-column reads. -- Confirmed the equivalent SQLite UPSERT is not ambiguous and MySQL uses `ON DUPLICATE KEY UPDATE`, so no matching backend fix is required. -- No schema migration is required; the runtime schema remains V047 and V048 remains the next migration number. -- Centralized SQL schema initialization in `db::pool::configure()` so the database session store no longer executes the global schema a second time. -- Fixed the duplicate startup DDL for all SQL backends: PostgreSQL no longer repeats `relation ... already exists, skipping` notices, while MySQL and SQLite also avoid the redundant second schema pass. -- Scoped PostgreSQL startup schema initialization to a transaction with `client_min_messages=WARNING`, suppressing expected idempotent `CREATE ... IF NOT EXISTS` notices without hiding application/database warnings globally. -- Removed the Docker-specific SQLx PostgreSQL notice filter workaround; normal `RUST_LOG=info` is sufficient. -- No schema migration is required; the runtime schema remains V047 and V048 remains the next migration number. +## 0.2.6 -### 0.2.1 -- Fixed Docker development authentication by documenting and defaulting Compose to `ENVIRONMENT=development`, so session cookies work over direct HTTP while production keeps `Secure` cookies behind HTTPS. -- Exposed `pgp_enabled` through the auth policy and hid OpenPGP settings, identity controls, composer controls and reader UI when OpenPGP is disabled by administrator policy. -- Prevented disabled OpenPGP from producing repeated UI error toasts or unnecessary API requests. -- Added OpenSearch outbox worker backoff for HTTP 429/flood-stage/read-only conditions and stopped the current batch after the first backpressure failure, preventing log storms while preserving durable retries without consuming dead-letter attempts. -- Kept OpenSearch disk safety thresholds intact; Docker documentation explains that named volumes consume the Docker host filesystem and how to diagnose/recover flood-stage blocks. -- Added a stable `rustbox` Compose project name, documented Docker environment variables, and made application port mapping configurable through `PORT`. -- Reduced PostgreSQL startup log noise in the Docker example by suppressing repeated SQLx `NOTICE` messages at the default log filter. +- Faster folder counters and refreshes, and new-mail updates in every folder, not only the Inbox. +- An in-app notice for new mail in any folder, without duplicate notifications. +- Fixed reader menus on iPhone. -### 0.2.0 -- Fixed nested database transactions during message move by reusing the caller-owned transaction for cache removal, header-sync invalidation, folder invalidation, and search outbox writes. -- Kept `search_outbox` in sync between tenant and legacy-import table sets so schema/import parity tests pass. -- Decoupled SQL, blob storage and search: PostgreSQL/MySQL may still use local `data_dir` backends in single-node mode, while cluster mode no longer requires persistent `data_dir` when cluster-safe backends are configured. -- Added `BlobStorage` with filesystem/shared-POSIX and S3-compatible backends, streaming attachment I/O, SHA-256 metadata, S3 retry/timeouts/checksums/SSE options and storage readiness probes. -- Added idempotent attachment migration from legacy filesystem paths to logical object keys (`rustbox storage migrate`). -- Added OpenSearch as the external full-text backend with an async HTTP client, per-tenant filtering/routing, deterministic document IDs, health checks and rebuild support. -- Added a durable SQL search outbox with caller-transaction participation, claim/lease concurrency, retry/backoff, dead-letter state and lag/dead-letter diagnostics. -- Hardened clustered indexing against re-enqueue/lease races, sized worker leases from the OpenSearch timeout/failover budget, and made IMAP IDLE deletions enqueue index removals in the same SQL transaction. -- Added runtime schema migration V047 for attachment checksums and the search outbox across SQLite, MySQL and PostgreSQL. -- Added Docker variants for standalone SQLite, PostgreSQL + SILO (S3-compatible) + OpenSearch, and MySQL + NFS + OpenSearch. -- Added tests for cluster capability validation, filesystem blob streaming, OpenSearch tenant scoping and search-outbox transaction rollback/claim/idempotency behavior. +## 0.2.5 -### 0.1.2 -- Added `log_level` to TOML configuration (`trace`, `debug`, `info`, `warn`, `error`, `off`); `RUST_LOG` remains an explicit runtime override. -- Audited the MySQL and PostgreSQL query catalogs, schema baselines and backend-specific server SQL without adding application tests. -- Fixed a MySQL DDL incompatibility by changing indexed `pgp_keys.email` from `TEXT` to `VARCHAR(320)`; MySQL requires a prefix length when indexing `TEXT`/`BLOB` columns. -- Fixed destructive MySQL cache upserts: folder refresh and message-header refresh now use `INSERT ... ON DUPLICATE KEY UPDATE` instead of `REPLACE INTO`, preserving child rows, message tags, cached bodies and other columns not part of the refresh. -- Documented and enforced cluster storage behavior: mailbox metadata/body cache remains in shared SQL, while draft attachments use either S3-compatible storage or an explicitly shared POSIX filesystem. -- Replaced the pre-0.2 storage/search compatibility switches with the final `[storage]` and `[search]` configuration model; cluster mode validates backend capabilities directly and rejects local Tantivy. -- Added `pgp_enabled`; cluster mode requires OpenPGP disabled because private-key material is filesystem-backed. -- Avoided provisioning per-user Tantivy directories when the search index backend is disabled and made diagnostics/deep indexing honor that setting. -- Added a release audit document with SQL compatibility and distributed-storage findings. +- After moving or deleting the open message, the next one opens automatically. -### 0.1.1 -- Fixed the multi-backend compatibility layer compile blockers reported by the Rust compiler: empty `[]` parameter inference no longer overlaps the generic array implementation, legacy SQLite probing imports `rusqlite::OptionalExtension` without colliding with RustBox's own `OptionalExtension`, and MySQL/PostgreSQL transaction batch execution now uses the typed SQLx transaction helpers instead of the lifetime-problematic `raw_sql` executor path. -- Unified the storage model: `database_backend = sqlite | mysql | pgsql` now selects the database engine for the whole application instead of only the SQL session store; `session_backend = database | redis` either keeps sessions in that same database or moves them to Redis/Redis Sentinel. -- Added complete SQLite, MySQL and PostgreSQL baselines under `migrations/{sqlite,mysql,pgsql}/schema.sql`. Each schema contains global sessions plus the full account-data schema at baseline version 46; the historical SQLite V001-V046 files are squashed out of the release tree and V047 is reserved for the next schema change across all engines. -- Preserved per-account data isolation in a shared physical database with deterministic collision-checked table prefixes, while keeping the session table global. -- Added automatic first-use import of legacy 0.1.0 `data_dir//db.sqlite` databases at `PRAGMA user_version = 46` into the configured backend. The source database is renamed to `db.sqlite.pre-0.1.1.bak` only after a successful import; older schemas are rejected with an explicit upgrade requirement. -- Added a backend-neutral database compatibility layer used by the existing Rust data modules and switched application query selection to the SQLite/MySQL/PostgreSQL catalog matching the configured backend. -- Corrected MySQL/PostgreSQL query dialects and parameter handling, including numbered/reused parameters, upsert syntax, timestamp expressions, JSON address lookup and generated-ID handling. -- Fixed transaction scope for server databases so cache/message synchronization, header invalidation and other grouped writes execute on the actual MySQL/PostgreSQL transaction connection rather than accidentally falling back to the pool. -- Removed separate `session.sql` files: sessions are part of each engine's complete `schema.sql`; Redis remains schema-less. -- Removed stale Python release-contract tests and the unused HTML fixture; retained the runtime JSON fixture used by Rust tests and added native tests for tenant scoping, placeholder adaptation and legacy SQLite import. -- Made Oxi synchronization treat upstream `backend/migrations/` and `backend/tests/` as RustBox-owned review-only areas so schemas and native tests are never overwritten automatically. -- Removed the accidental runtime log `qar.txt` from the source tree and refreshed release metadata for 0.1.1. +## 0.2.4 -### 0.1.0 -- Cleaned the final release gate for Rust 1.99 Clippy with `-D warnings`: refactored persistent session creation into a typed input struct and fixed all reported collapsible conditionals, redundant closures, needless borrows/question marks, test layout and collection checks without disabling Clippy globally. -- Fixed fresh user-database provisioning so only SQLite user migrations `V001`-`V046` are applied; session migration `V047` is now exclusively owned by the configured session-store backend and MySQL SQL can no longer be executed against a user SQLite database. -- Removed the obsolete Refinery migration path/dependency and its `refinery_schema_history` bridge; RustBox now has one canonical user-database migration runner. -- Removed unused test-only session removal helpers that emitted `dead_code` warnings during `cargo test`. -- Strengthened `scripts/build-release.sh` to require formatting, locked tests, Clippy with `-D warnings`, and a locked release build before producing a release binary. -- Removed production-build warning noise from the multi-dialect query catalogs without disabling warnings crate-wide; test-only session/SMTP helpers are now compiled only under `cfg(test)`. -- Fixed cached-message row decoding after the query-catalog refactor: message list queries now project the exact `CachedMessage` column order (`flags`, `size`, `has_attachments`, `snippet`, `reaction`) across SQLite/MySQL/PostgreSQL. -- Fixed Rust `macro_rules!` query hygiene errors by passing dynamic SQL fragments and parameter indexes explicitly into SQLite query macros instead of capturing caller-local variables. -- Fixed the `libsqlite3-sys` Cargo links conflict by using `rusqlite` for SQLite session persistence and limiting `sqlx` to MySQL/PostgreSQL drivers. -- Refactored all executable Rust SQL into a centralized dialect tree at `src/queries/{sqlite,mysql,pgsql}/queries.rs`; application modules no longer embed SQL statements. -- Added persistent sessions that survive process restarts; SQLite under `data_dir/rustbox.sqlite` is the default with no extra configuration. -- Added MySQL and PostgreSQL session-database backends plus independently configurable Redis sessions with direct or Sentinel master discovery. -- Added cluster mode validation and shared-session reads so authenticated traffic can move between nodes when a shared SQL/Redis backend is used. -- Encrypts persisted IMAP/SMTP session credentials with AES-256-GCM; single-node SQLite auto-creates `data_dir/session.key`, while shared/cluster stores require an explicit common key. -- Centralized the complete SQL query catalog by dialect under `src/queries/{sqlite,mysql,pgsql}/queries.rs`; session queries use the same tree. Flattened migrations directly under `migrations/{sqlite,mysql,pgsql}/`; SQLite user migrations remain `V001`-`V046` and sessions use `V047__sessions.sql` across SQL dialects. +- Fixed S3-compatible storage over internal plain-HTTP addresses. +- Updated the Rust toolchain and Docker images (Debian 13). -### 0.0.7 -- Added `allow-downloads` to the sandboxed HTML message reader so user-initiated downloads are no longer blocked by Chromium sandbox policy. -- Added GitHub-, GitLab-, Discord-, Slack-, Reddit- and Notion-inspired predefined interface styles with light/dark palettes. -- Added a release contract that verifies every predefined interface style is visible in the application selector, has a JavaScript profile and CSS palette, and is accepted by backend/config validation. +## 0.2.3 -### 0.0.6 -- Allowed links in rendered HTML messages to open in a new tab from the sandboxed reader while keeping the opened page outside the message sandbox. -- Added eight predefined color presets: Emerald, Lime, Sky, Navy, Plum, Magenta, Copper, and Sand. +- The administrator can turn off filters, diagnostics and the About page. +- A "Reset all settings" option that does not touch mail, identities or keys. +- Optional browser security headers. +- A high-availability Docker example with two RustBox nodes behind a load balancer. -### 0.0.5 -- Added native file logging through `log_dir`; the packaged Linux profile writes JSON logs to `/var/log/rustbox/rustbox.log` and ships 7-day logrotate policy. -- Updated the packaged systemd profile for `/var/lib/rustbox`, `/var/log/rustbox`, hardened service settings, and user-owned configuration. -- Fixed the mobile reader toolbar regression so compact controls keep their original height and Message details / Move-copy menus open next to the triggering control instead of at the bottom of the viewport. -- Full-screen message reading now overrides the normal reader width limit so the message workspace uses the available screen width. -- Fixed mobile reader actions, attachment layout, long subjects, and touch targets. -- Added WebSocket application heartbeat, dead-connection detection, exponential reconnect, reconnect resync, event deduplication, and fallback polling. -- Fixed live message refresh coalescing so folder updates cannot suppress a pending new-message refresh. -- Improved new-mail notifications and made notification clicks open the target message when its UID is available. -- Added message full-screen mode from the reader button and context menu with Escape, browser history, deep links, and scroll restoration. -- Kept attachment upload progress only in the composer progress bar and added duplicate/cancellation safeguards. -- Removed source-code comments and added 0.0.5 regression contracts. +## 0.2.2 -### 0.0.4 -- Changed attachment left-click behavior to prefer in-app preview for supported files while keeping direct download for non-previewable formats. -- Added attachment right-click actions for explicit Preview and Download. -- When an attachment exceeds the preview limit, RustBox now offers to download it instead of stopping at a warning; known oversized files are detected before their contents are fetched for preview. +- Reliability fixes for PostgreSQL, including marking messages as spam, and quieter database startup. -### 0.0.3 -- Fixed reader toolbar dropdowns for Message details and Move/copy by moving popovers out of the horizontally clipped toolbar layer. -- Added Fastmail-, Apple Mail-, Yahoo Mail- and HEY-inspired styles plus Nord, and an optional per-account unlock for UI scaling on predefined styles. -- Added separate mail-size controls: `max_attachment_size_mb = 10` per uploaded file and `max_message_size_mb = 50` for the complete final RFC822/MIME message. The final MIME bytes are checked before SMTP submission, including base64, multipart and OpenPGP overhead; the composer shows an estimated running total. -- Moved the Diagnostics → IMAP reconnect action below capability details so it no longer collides with the status label. -- Added the RustBox repository link to Settings → About. -- Defaulted omitted mail endpoints to localhost:993 (IMAP) and localhost:587 (SMTP); TLS verification is skipped automatically only for loopback endpoints unless `tls_verify` is explicitly configured. -- Swapped the compact composer footer groups so the HTML/Text/Markdown selector is on the left and Cancel/Send on the right. -- Grouped message details and move/copy actions into compact reader-toolbar menus. -- Improved Polish translations for composer, message toolbar and per-message context menu. -- Moved composer format selection to the opposite side of Send/Cancel in the compact composer footer. +## 0.2.1 -- Added server-wide `default_interface_skin` (`rustbox`, `gmail`, `proton`, `zoho`, `outlook`, etc.) for login and accounts using **Global default**. +- Easier Docker setup for development. +- OpenPGP is completely hidden when the administrator disables it. +- OpenSearch handles a full disk gracefully instead of flooding the logs. -- Fixed mobile modal/settings overflow, transparent dialog strips, calendar toolbar wrapping and narrow-layout sizing. -- Fixed visual Sieve rule checkbox alignment and stacked its rule controls safely on small screens. -- Added tap/keyboard expansion for full recipient addresses in the message reader. -- Added persistent IMAP quota progress above the sidebar IMAP/version footer. -- Added administrator-configurable `default_theme = "system" | "dark" | "light"` for the pre-login/login UI. -- Added Gmail-, Proton-, Zoho- and Outlook-inspired interface profiles plus a Minimal profile. -- Expanded Polish translations across settings, dialogs, calendar, contacts, filters and common accessibility/title attributes. -- Bumped the application/source release to `0.0.3`. +## 0.2.0 -### 0.0.2 +- Attachments can be stored on local disk, a shared filesystem or S3-compatible storage, with a migration tool for existing files. +- OpenSearch can be used for search, with reliable background indexing. +- Cluster mode no longer needs local state on each node. +- Docker examples: standalone SQLite, PostgreSQL + SILO (S3-compatible) + OpenSearch, and MySQL + NFS + OpenSearch. -- Split the monolithic browser application JavaScript into functional modules under `src/web/js/`; `build.rs` now assembles the JavaScript manifest into the embedded fingerprinted bundle alongside the existing CSS build. -- Removed the duplicate `ui/` snapshot; `src/web/` is the only browser UI source and Cargo assembles embedded CSS/JS bundles in `OUT_DIR`. -- Removed the top-level Compact/Balanced/Comfortable/Reading appearance preset cards. -- Expanded **Interface style** with RustBox (custom), Roundcube Classic, Roundcube Larry, Roundcube Elastic, Clean, Graphite and High contrast. -- Fixed styles no longer overwrite the user's RustBox custom colors/layout values; switching back to RustBox restores the saved manual configuration. -- Fixed interface styles lock palette/geometry controls while leaving Theme, language, date/time formatting and accessibility/user visibility toggles available. -- Added dedicated light/dark palettes for Larry, Clean, Graphite and High contrast while retaining the existing Classic/Elastic compatibility skins. -- Updated UI/build contract tests for the modular JavaScript build and expanded interface-style validation. -- Fixed the 0.0.2 release build regression by centralizing `AppConfig::default()` and updating stale `AccountSession` test fixtures for the new session metadata fields. -- Updated UI contract tests and visual fixtures after removal of the legacy `ui/` directory. -- Fixed release-test regressions in OpenPGP fingerprint validation, protected system-folder recognition, nested `base_path` UI routing, and missing SMTP endpoint handling. +## 0.1.2 -### 0.0.1 +- Log level can be set in the configuration. +- MySQL fixes, including refreshing folders without losing cached data. +- Clear rules for storage and search in cluster mode. -- Reworked Roundcube appearance compatibility: dedicated Classic and Elastic interface skins now use upstream-aligned light palettes, dedicated dark variants, exact Classic red/gray selection accents, Elastic cyan/dark tokens, and palette locking so generic RustBox colors cannot bleed into the selected skin. -- Replaced the mobile folder-drawer close affordance with a left-facing SVG back arrow in a dedicated mobile drawer header. -- Added persisted `interface_skin` display preferences and two schema migrations for skin selection and saved-response format. -- Added Roundcube-style saved/canned responses with text/HTML formats, settings CRUD UI and composer insertion; HTML is sanitized before insertion. -- Added static theme-contract tests and browser-rendered visual fixtures for Classic/Elastic light/dark and mobile drawer validation. -- Moved Sieve rule creation/editing into a dedicated wide modal with the existing live “what will happen” preview; Settings now shows the rule list and launches the editor instead of squeezing the full builder into the settings pane. -- Moved identity/signature editing into a separate viewport-safe modal and eliminated the horizontal overflow caused by the two-column Settings layout. -- Added a persistent full-screen composer toggle. -- Collapsed the OpenPGP composer controls into a compact summary row with expandable details, while keeping PGP options hidden when no OpenPGP keyring is configured. -- Kept the signature image/CID flow from the previous UI patch in the packaged sources and synchronized standalone `ui/` assets with embedded `src/web/`. -- Moved bulk message actions into a floating overlay so selection mode no longer consumes the message-list grid row or creates horizontal layout spill. -- Hardened narrow reader/message toolbars and composer layout against controls colliding at split-pane widths. -- Reworked composer address/options rows into distinct fields and allowed the formatting toolbar to wrap cleanly. -- Expanded identity signatures/footers with text + HTML editing and image import/paste; imported images are materialized as inline CID draft attachments when composing. -- Completed the OpenPGP roadmap lifecycle with private-key passphrase change. The existing rPGP implementation remains responsible for key parsing, signing, encryption, verification and decryption; GnuPG is used only to re-wrap an existing secret key when changing its passphrase. Passphrases are exchanged over stdin/status-fd and are never stored in the database or command line. -- Added `POST /pgp/keys/{fingerprint}/change-passphrase` and a keyring UI action, including an explicit warning before removing passphrase protection. -- Added optional `RUSTBOX_PGP_GPG_BINARY` override for deployments where the GnuPG binary is not named `gpg`. -- Expanded the visual Sieve builder with dynamically addable conditions and actions. Additional conditions can be chained with nested AND/OR and capability checks now include dynamically added actions. -- Replaced the stale roadmap gap list with a counted functional checklist: 45 functional items remain after this snapshot, with security/integration/acceptance gates tracked separately. -- Fork base: Oxi `7f1507c486b78cf33e54beb701dd460d3bd2bd5d`. -- Flattened the Rust application to the repository root: `Cargo.toml`, `src/`, `migrations/`, `tests/` and `examples/`. -- Removed the separate Oxi frontend source and all TypeScript/React/Next.js/Node/Bun build requirements. -- Added a RustBox-owned embedded HTML/CSS/vanilla-JavaScript interface with dark/light/system themes. -- Added `dev.sh`; ordinary `cargo run`, `cargo test` and `cargo build --release` now work from repository root. -- Added mail reader/list/compose/search UI and About/Credits with the consumed Oxi revision embedded from `UPSTREAM_BASE`. -- Added ManageSieve API/editor with STARTTLS, implicit TLS and explicit plaintext opt-in. -- Added native systemd packaging and Cargo-only CI/release build. -- Added Oxi tracking tools that map upstream `backend/...` changes into the flat RustBox tree without importing Oxi frontend sources. -- Added full-baseline sync review plus selective single-commit backend porting from Oxi. -- Build fix: corrected the embedded SVG raw-string delimiter, imported Figment's `Format` trait for `Toml::file`, and removed an unused `SieveCapabilities` re-export. -- Mail server endpoints are now configuration-owned; the login form only accepts email, password and remember-session preference. -- Fixed Inbox/folder/quota/draft operations to use the connection profile stored in the authenticated session instead of re-reading an optional global IMAP host. -- Added startup validation for `imap_host` to prevent successful Sieve/login state with a non-functional Inbox. -- Rebuilt the interface as a compact three-pane webmail layout with embedded SVG icons, HTML compose formatting, threads, folder actions, Sieve filters and appearance preferences. -- Moved browser sources to `src/web/`; Cargo `build.rs` bundles modular CSS and the executable embeds all UI assets. -- Removed gradients and Unicode pseudo-icons from the application interface. -- Added nested-folder indentation, Reply all, accurate IMAP connection state, and RustBox-branded HttpOnly session/browser cookies. -- Added persistent per-user appearance personalization stored in SQLite. -- Added EN/PL language assets embedded in the Rust binary. -- Added theme palette presets (sage/blue/slate/amber/violet/rose/custom) and custom accent color. -- Added font family/size, message row height, pane widths, border radius, contrast and reduced-motion controls. -- Added toggles for avatars, attachment indicators and flags. -- Added message and folder context menus for common webmail actions. -- Added raw-header viewer and clipboard copy. -- Added folder subscribe/unsubscribe UI and context actions for creating subfolders. -- Added identities/signatures settings UI and composer From identity selection. -- Signatures from the selected identity are inserted into the composer. -- Replaced the internal roadmap with the user-approved 0.0.1 plan. -- Added per-account special IMAP folders for Sent/Drafts/Trash/Junk/Archive and wired them into send/delete/archive/junk flows. -- Added IMAP copy, mark-all-read, empty-folder, multi-select and bulk operations. -- Added drag-and-drop message-to-folder moves and previous/next reader navigation with keyboard shortcuts. -- Added advanced and saved search, read/flag operators, user labels/tags and mailbox quota display. -- Expanded composer to HTML/plain/Markdown modes, inline CID images, attachment reminder and a larger formatting toolbar. -- Expanded identities with Reply-To, automatic Bcc, Organization metadata and text/HTML signature behavior. -- Added contact groups and vCard import/export UI. -- Added a capability-aware visual Sieve rule builder plus vacation/autoresponder generation and server validation. -- Added ICS calendar import UI. -- Connected the browser to the existing IMAP IDLE/WebSocket event stream for realtime refresh and desktop notifications. -- Added notification Do Not Disturb hours and persisted per-folder notification preferences. -- Expanded personalization presets and added custom background, panel and text colors in addition to the accent color. -- Added persisted mailbox preferences for page size, date sort, delayed mark-read, threaded/flat reader mode, HTML/plain preference, quote limiting, snippet visibility and remote-image policy. -- Added remote-image prompting with sandboxed message rendering and no-referrer loading. -- Added IMAP capability diagnostics and a settings diagnostics panel for IMAP, ManageSieve and realtime WebSocket state. -- Added multi-account UI for adding, switching and removing accounts in the current browser session. -- Added user-selectable date and 12/24-hour time formats. -- Added server-side validation for message-list page size/sort and mailbox preference values. -- Added `.eml` import into any selected IMAP folder using APPEND, followed by cache invalidation and realtime folder refresh. -- Added `List-Unsubscribe` handling for safe HTTPS/mailto targets from message headers. -- Added in-app full RFC822 source viewer, Message-ID/sender copy actions and folder-properties inspection. -- Raised extractor body limits only on upload endpoints so the documented attachment/vCard/ICS/.eml limits are actually reachable. -- Expanded contacts with phone, job title, address, website and birthday fields. -- Added contact photo upload/remove/display for PNG/JPEG/WebP with byte-signature validation and a 2 MB limit. -- Extended vCard import/export with TEL, TITLE, ADR, URL and BDAY. -- Changed contact persistence from SQLite `INSERT OR REPLACE` to conflict-update semantics so editing a contact does not destroy group/photo relations through a row replacement. -- Fixed the calendar delete-handler syntax regression found during pre-release static review. -- Added Tantivy index diagnostics and an in-app rebuild from the per-user SQLite cache. -- Added IMAP request-pool reconnect/verification from Diagnostics. -- Added active-session inventory across browsers while the process is running, User-Agent display, individual revoke and revoke-all-others. -- Expanded calendar events with all-day, recurrence RRULE, attendees, organizer, status, reminder, meeting URL and color fields in the UI; added full local ICS export. -- Added direct ICS/vCard import actions for message attachments. -- Expanded contacts to multiple e-mail addresses, phone numbers and postal addresses. -- Added duplicate-contact detection and transactional merge preserving groups, alternate values and contact photos. -- Validated all 28 SQLite migrations from an empty database plus browser JS/JSON/TOML/shell assets. -- Added Month, Week, Day and Agenda calendar views with persistent view selection and event-color markers. -- Hardened contact/calendar/header/source dialogs against viewport overflow and UI scaling. -- Fixed UI scaling so it also changes icon sizing and field/button spacing, and added persistent draggable pane splitters. -- Added reusable action toasts to additional folder, search, calendar and ManageSieve operations. -- Added Spam / Not spam actions in the primary and bulk message toolbars. -- Added safe attachment Open/Download context actions and browser-side Download all as ZIP with UTF-8 filenames. -- Added To/Cc/Bcc autocomplete using contacts and known addresses, including keyboard navigation. -- Added message-list quick actions for read/unread and flag/unflag. -- Hardened message rendering against RustBox-owned icon sprite markup leaking from malformed MIME content. -- Kept the UI free of gradients, TypeScript, React/Next and Node/Bun runtime dependencies. -- Added collapsible hierarchical IMAP folder branches with per-account remembered expansion state and leaf-name rendering. -- Added a keyboard-shortcuts reference dialog and `!` shortcut for Spam / Not spam. -- Added direct sender actions from the reader header: compose, add contact, copy address, search and create filter. -- Expanded message sorting to date, sender, subject and size in ascending/descending order, available directly from the message-list toolbar and persisted per account. -- Added the `Contacts only` remote-image policy, which verifies the current sender against the saved address book before allowing external images. -- Expanded the visual Sieve builder with a second condition, AND/OR combination and per-condition negation while retaining server-side CHECKSCRIPT validation. -- Added automatic identity selection on reply/forward using To/Cc and Delivered-To/Envelope-To headers before signature insertion. -- Added mailbox quota warning/danger states at 80%/95% usage. -- Added collapsible thread navigation with remembered expansion state. -- Added safe Undo for MOVE-backed message actions when the destination UID can be resolved unambiguously, including reverse-order bulk Undo. -- Added “Select all results” for search bulk actions by materializing the full cached match set on explicit request. -- Hardened thread matching so `References` uses complete Message-ID tokens instead of accepting substring collisions. -- Moved safe attachment previews into an in-app modal for images, PDFs and text attachments. -- Added composer upload progress and clipboard image paste as CID inline attachments. -- Added draft recovery from both server-side local drafts and an emergency browser snapshot; SMTP failures force a recoverable draft save. -- Changed autosave so it no longer appends a fresh IMAP Drafts message every 1.2 seconds; IMAP snapshots are reserved for deliberate close/failure points until remote draft UID replacement is implemented. -- Expanded keyboard shortcuts with `C` compose, `V` selection mode and `X` select/unselect current message. -- Replaced runtime browser `confirm()`/`prompt()` flows in the main RustBox UI with reusable application dialogs, including folder/label choices used by message context actions and quick-filter creation. -- Added an explicit message selection mode; row checkboxes are hidden/disabled outside that mode and bulk controls remain available while selecting. -- Fixed compact/comfortable density so it changes control, toolbar and dialog sizing independently from UI scale and the user-defined message-row height. -- Added visible toast auto-dismiss countdowns with pause-on-hover/focus behavior. -- Added Compact, Balanced, Comfortable and Reading interface presets that persist through the existing display-preferences API. -- Reworked message printing to use a sandboxed in-page print frame instead of a popup, while preserving sanitized HTML/plain-text message rendering and message metadata. -- Added persisted composer priority and MDN/read-receipt options; outgoing optional headers are shared by SMTP delivery, draft snapshots and the IMAP Sent copy, with mailbox validation before SMTP. -- Added RFC 8058 one-click unsubscribe through a backend HTTPS POST path with no redirects, DNS/IP public-address checks and direct-address TLS connection to reduce SSRF/DNS-rebinding risk. -- Fixed migration registration so the existing V028 contact migration and new V029 draft-send-options migration are both executed; the migration set is now V001–V029. -- Expanded the visual Sieve builder to three conditions with nested AND/OR grouping, two actions, capability-driven action disabling, managed visual-rule ordering, JSON backup/restore and active-script overwrite confirmation. -- Added optional scheduled vacation start/end dates using Sieve `currentdate` with `date` + `relational` requirements when the server advertises those extensions. -- Added a browser-wide default Spam/Junk destination with a per-account special-folder override; mail actions fall back to IMAP `\Junk`/Spam auto-detection when neither resolves. -- Added persistent mailbox-level “flagged on top” and per-folder inherit/on/off overrides. Flagged-first ordering is performed in SQLite before pagination. -- Added a subtle red lower-edge marker to flagged message rows without changing the existing star action. -- Replaced the quick-filter folder prompt/raw-editor handoff with a dedicated ManageSieve filter wizard that previews the rule, respects advertised extensions, validates with CHECKSCRIPT, saves directly to the selected script and can activate it. -- Kept an Advanced editor handoff that preloads the visual Sieve builder for users who want to refine the generated rule. -- Added SQLite migration V030 and a folder-preferences API for per-folder list behavior. -- Added aggregate unread counts to the browser title and supported application badges. -- Added `/api/ready` alongside liveness `/api/health`; readiness checks that the local RustBox data directory is accessible without coupling process readiness to account-specific IMAP/SMTP availability. -- Made generic application dialogs scroll as a fallback when their content exceeds the browser workspace, while retaining independent scroll regions for structured composer/settings/contact/calendar/source dialogs. -- Fixed inline-image compose preview: uploaded CID images render from the authenticated draft-attachment endpoint while editing, then serialize back to `cid:` references for draft persistence and SMTP/IMAP MIME generation. -- Removing an inline-image attachment now also removes its matching image node from the composer instead of leaving a broken placeholder. -- Replaced the About-page Runtime row with `Author: @linuxiarz.pl`. -- Extended the quick-filter wizard with an optional capability-aware “also mark as read” action and an explicit notice that Sieve rules apply to new incoming mail rather than retroactively changing existing messages. -- Changed the About author label to `@linuxiarz.pl`. -- Fixed action-toast controls so Undo and the close button share one compact control group; clicks no longer propagate between the two actions. -- Preserved the active folder, loaded-page depth, message-list scroll position and active search context across normal refresh/reconnect flows within the browser session. -- Added an explicit offline/reconnecting state and bounded automatic retry for idempotent GET/HEAD requests only; mutating operations are never automatically replayed. -- Added authenticated SMTP connection diagnostics, IMAP custom-keyword capability detection and local synchronization-cache diagnostics with last folder/message sync timestamps. -- Added friendly active-session client names such as `Firefox · Linux` while keeping the full User-Agent available as hover detail. -- Added client-side highlighting of active search terms in sender, subject and snippet without injecting result text as HTML. -- Fixed the Settings modal grid so the header occupies only its natural height and the sidebar/content area fills the remaining workspace instead of being pushed toward the bottom for short panels such as About. -- Made Settings navigation/content independently scrollable and mobile tabs horizontally scrollable. -- Added inline-image selection/editing in the composer: display width, inline/left/center/right placement and in-app crop/cut with a live preview. Cropping creates a replacement CID attachment; GIF cropping stays disabled to preserve animation. -- Dragged image files now insert as inline CID images in HTML mode instead of silently becoming ordinary attachments. -- Added cleanup of CID attachments whose inline image was removed from the editor, including a final cleanup before send/close. -- Added a warning before switching away from HTML when rich formatting may be lost. -- Added persisted Composing settings for default format, draft autosave interval, default priority, default MDN/read-receipt request and attachment-reminder behavior. -- Added SQLite migration V031 for the new composing preferences. -- Added SQLite migrations V032–V036 for advanced appearance preferences, draft compose-format persistence, optional IMAP-keyword synchronization, composer behavior preferences and remote IMAP-draft tracking. -- Expanded Appearance with icon size, toolbar/sidebar density, reader/composer widths, selection color, unread styling, compact headers, border/scrollbar controls, monochrome mode and per-pane background overrides. -- Completed the safe Markdown compose pipeline: Markdown source remains the plain alternative while preview/delivery uses escaped generated HTML rather than raw Markdown HTML. -- Added persisted composer defaults for font, reply-above/below-quote, quote style, forward inline/as-attachment and browser spellcheck. -- Added administrator-configurable outgoing headers with header-name validation, structural/MIME-header denylisting and CR/LF-injection rejection. -- Added opt-in native IMAP keyword synchronization for RustBox labels. Stable RustBox keywords are written only where persistent custom keywords are supported and observed RustBox keywords are imported back into local labels; full remote-removal reconciliation remains partial. -- Reworked deliberate IMAP Drafts snapshots so RustBox resolves the newly appended draft UID from UIDNEXT plus an exact `X-RustBox-Draft-ID` search, stores UID/UIDVALIDITY and removes the prior snapshot only after an unambiguous safe match. -- Added strict draft-ID validation before attachment-path use, closing traversal through draft identifiers and making the draft marker safe for IMAP headers. -- Changed attachment uploads to stream multipart chunks into bounded `.part` files with atomic rename, stale-part cleanup and cleanup on interruption/error instead of buffering the entire upload in memory. -- Fixed the duplicated draft-route `Extension(config)` extractor and kept standalone `ui/` assets synchronized with embedded `src/web/` sources. -- Fixed the Axum 0.8 `E0277 Handler` failure in tag/bulk-tag routes by separating synchronous SQLite work from awaited IMAP work; no rusqlite connection/transaction is retained across network awaits. -- Applied the same DB/network phase separation to single-tag, untag and tag-delete handlers so the compile failure does not simply move to the next route. -- Added authoritative IMAP-label reconciliation for folders that explicitly support persistent custom keywords. Remote RustBox keyword additions and removals can now be reconciled into local labels from Mailbox settings. -- Fixed `Keep me signed in` so the server-side session timeout is extended together with the cookie instead of still expiring at the normal default timeout. -- Active-session diagnostics now expose effective expiry and whether the session uses the remembered timeout. -- Added graceful SIGTERM/Ctrl+C shutdown and explicit cancellation of dedicated IMAP IDLE tasks before process exit. -- Extended synchronization diagnostics with active IMAP IDLE task/subscriber counts to make reconnect storms and leaked subscriptions visible from the UI. +## 0.1.1 + +- The whole application can run on SQLite, MySQL or PostgreSQL; sessions can be kept in the same database or in Redis. +- Existing installations are imported automatically on first start. + +## 0.1.0 + +- Sessions survive server restarts and can be shared between several servers (SQL or Redis); stored credentials are encrypted. +- Cluster mode for running several RustBox nodes. + +## 0.0.7 + +- Downloads from HTML messages work in Chromium-based browsers. +- New GitHub, GitLab, Discord, Slack, Reddit and Notion inspired styles. + +## 0.0.6 + +- Links in HTML messages can open in a new tab. +- Eight new colour presets. + +## 0.0.5 + +- Logging to files with rotation, and a hardened system service. +- Full-screen message reading. +- More reliable live updates with automatic reconnection. +- Mobile reader fixes. + +## 0.0.4 + +- Attachments open in an in-app preview where possible, with a download offer for large files. + +## 0.0.3 + +- Separate limits for a single attachment and for the whole message. +- Default mail server settings for local installations. +- New Fastmail, Apple Mail, Yahoo, HEY, Gmail, Proton, Zoho and Outlook inspired styles, plus Nord and Minimal. +- Server-wide default theme and style. +- Quota bar in the sidebar, mobile layout fixes and better Polish translations. + +## 0.0.2 + +- New interface styles: RustBox, Roundcube Classic, Larry and Elastic, Clean, Graphite and High contrast. Switching styles keeps your own RustBox settings. + +## 0.0.1 + +The first release, created as a fork of the Oxi webmail with a new interface built into the server. + +- **Mail:** + - three-pane layout; + - folders with subfolders; + - special folders; + - threads; + - bulk actions; + - drag and drop; + - undo for moves; + - flagged messages on top; + - quota warnings; + - message source; + - `.eml` import and export; + - one-click unsubscribe. +- **Composer:** + - HTML, plain-text and Markdown editors; + - inline images with cropping; + - attachments with progress; + - draft recovery; + - identities with signatures; + - priority and read receipts; + - canned responses. +- **Search, labels and contacts:** + - search with operators; + - saved searches; + - coloured labels synchronised with IMAP keywords; + - contacts with photos and groups; + - duplicate merging; + - vCard import and export. +- **Filters:** + - visual editor; + - out-of-office replies; + - backup and restore. +- **Calendar:** + - month, week, day and agenda views; + - ICS import and export. +- **Security:** OpenPGP for signing, encryption and key discovery. +- **Live updates:** + - realtime new mail; + - desktop notifications with Do Not Disturb hours; + - several accounts in one browser; + - active session management. +- **Appearance:** + - extensive settings; + - light/dark/system theme; + - English and Polish. +- **Diagnostics** for users and administrators. diff --git a/Cargo.lock b/Cargo.lock index 58554e2..0a7a07c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3506,7 +3506,7 @@ dependencies = [ [[package]] name = "rustbox" -version = "0.2.25" +version = "0.2.26" dependencies = [ "aes-gcm", "async-imap", diff --git a/Cargo.toml b/Cargo.toml index dc8cc1c..16ec569 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,12 @@ [package] name = "rustbox" -version = "0.2.25" +version = "0.2.26" edition = "2024" rust-version = "1.99.1" +license = "AGPL-3.0-only" +authors = ["Mateusz Gruszczyński "] +description = "Fast, self-hosted webmail written in Rust" +repository = "https://git.linuxiarz.pl/gru/RustBox" default-run = "rustbox" [dependencies] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/LICENSING.md b/LICENSING.md new file mode 100644 index 0000000..a57b923 --- /dev/null +++ b/LICENSING.md @@ -0,0 +1,29 @@ +# Licensing + +RustBox is copyright © 2026 Mateusz Gruszczyński ([linuxiarz.pl](https://linuxiarz.pl)). + +RustBox is **dual-licensed**. + +## Open source: GNU AGPL v3 + +RustBox is free software released under the **GNU Affero General Public License, version 3** (`AGPL-3.0-only`). The full text is in [`LICENSE`](LICENSE). + +You may use, study, modify and share RustBox free of charge, privately or in a company, as long as you follow the AGPL. Most importantly: if you run a **modified** RustBox as a network service, you must offer the users of that service the corresponding source code of your modified version under the same license. + +Running an unmodified RustBox — for yourself, your family, your company or your customers — needs nothing more than keeping the license and copyright notices intact. + +## Commercial license + +Organisations that cannot or do not want to meet the AGPL's obligations — for example to keep their own modifications private while offering RustBox as a service, or to ship it as part of a proprietary product — can obtain a **paid commercial license** with terms agreed individually. + +Contact: **[linuxiarz@linuxiarz.pl](mailto:linuxiarz@linuxiarz.pl)** + +Getting in touch is voluntary: a commercial license is never required for use that complies with the AGPL. Companies that use RustBox under the AGPL and want to support its development are equally welcome to write. + +## Contributions + +So that RustBox can stay available under both licenses, contributions are accepted only when the contributor agrees that they may be distributed under the AGPL v3 as well as under commercial licenses issued by the copyright holder. + +## Third-party notices + +The first version of RustBox was created as a fork of [Oxi](https://github.com/c0h1b4/oxi) (MIT License). See [`NOTICE.md`](NOTICE.md) for the attribution and the MIT notice that applies to the parts that originate from Oxi. Third-party Rust crates are used under their own licenses as declared in `Cargo.lock`. diff --git a/NOTICE.md b/NOTICE.md index d4c23ea..a95a79e 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,13 +1,20 @@ -# RustBox attribution / notice +# Notices -RustBox 0.2.25 is derived from the Oxi webmail project: +RustBox +Copyright © 2026 Mateusz Gruszczyński (linuxiarz.pl) -- Upstream: `c0h1b4/oxi` -- Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d` -- RustBox modifications: single-process Rust runtime, RustBox-owned embedded interface, native service packaging, configuration changes and ManageSieve integration. +RustBox is distributed under the GNU Affero General Public License v3 (see `LICENSE`) or, alternatively, under a commercial license (see `LICENSING.md`). -RustBox does not ship the Oxi TypeScript frontend in this release. Existing Oxi Rust backend code remains the implementation base and should retain all attribution/license obligations applicable to the upstream project. +## Origin -The user-supplied Oxi snapshot did **not** contain a `LICENSE` or `NOTICE` file. Upstream documentation identified Oxi as MIT-licensed, but this source release deliberately does not reconstruct an authoritative license text. Before public redistribution, verify the license at the exact upstream revision and include all required notices. +The first version of RustBox was created as a fork of **Oxi** — https://github.com/c0h1b4/oxi — a webmail project published under the MIT License. RustBox has since been developed independently and is no longer synchronised with Oxi. A few internal identifiers (for example cookie and database names) still date from that origin. -Some internal compatibility identifiers originating from Oxi remain (for example cookie/database naming). They are implementation details retained to reduce unnecessary migration and merge breakage. +Portions of RustBox that originate from Oxi remain available under the MIT License: + +> Copyright (c) the Oxi authors +> +> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 9427aae..42130d8 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,58 @@ -# RustBox 0.2.25 +# RustBox 0.2.26 -RustBox is a webmail derived from `c0h1b4/oxi`. The consumed Oxi revision is recorded in `UPSTREAM_BASE`. +**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. -The repository is one Cargo application. The RustBox browser interface lives under `src/web/`, uses HTML/CSS/vanilla JavaScript, and is embedded into the Rust executable at build time. There is no TypeScript, React, Next.js, Node.js or Bun build step. +![RustBox](docs/screenshots/mail-light.png) -## Layout +## Highlights -```text -rustbox/ -├── Cargo.toml -├── build.rs -├── src/ -│ ├── main.rs -│ ├── ui.rs -│ └── web/ -│ ├── index.html -│ ├── js/ -│ │ ├── manifest.txt -│ │ └── *.js -│ ├── icons.svg -│ └── css/ -├── migrations/ -├── tests/ -├── examples/ -├── docs/ -├── packaging/ -├── scripts/ -├── config.dev.toml -├── config.example.toml -├── UPSTREAM_BASE -└── dev.sh +- Three-pane mail client with folder tree, threads, bulk actions, drag and drop, snooze, undo send and undo move. +- HTML, plain-text and Markdown composer with inline images, drafts, identities, signatures and OpenPGP. +- Server-side filters (Sieve) with a visual editor, exceptions, templates and **Apply now** for mail that is already in a folder. +- Calendar with reminders and ICS import/export, contacts with groups and vCard. +- Realtime new-mail updates and desktop notifications. +- 43 interface styles, from Gmail- and Outlook-inspired to Dracula and Windows XP, plus a fully adjustable custom style, light/dark/time-of-day themes and a mobile layout. +- SQLite, MySQL or PostgreSQL; local disk, shared filesystem or S3 storage; built-in search or OpenSearch; optional cluster mode. +- English and Polish interface. + +The complete list, with screenshots, is in **[docs/FEATURES.md](docs/FEATURES.md)**. + +| | | +|---|---| +| ![Filters](docs/screenshots/filters-editor.png) | ![Dracula style](docs/screenshots/style-dracula.png) | +| ![Calendar](docs/screenshots/calendar.png) | ![Windows XP style](docs/screenshots/style-winxp.png) | + +## Quick start + +```bash +# development server with config.dev.toml +./dev.sh +# then open http://127.0.0.1:3001 ``` -`build.rs` combines the CSS modules from `src/web/css/manifest.txt` and JavaScript modules from `src/web/js/manifest.txt`. Cargo embeds the generated CSS/JS bundles, HTML and SVG sprite in the RustBox executable. There is no standalone source `src/web/app.js`; `src/web/` is the single source of truth and Cargo generates the embedded bundles in `OUT_DIR`. +Production build and systemd installation: -## Storage and search in 0.2.16 +```bash +./scripts/build-release.sh +sudo ./scripts/install-systemd.sh +sudo systemctl enable --now rustbox +``` -RustBox separates three concerns: SQL metadata, blob storage and search. The SQL engine does **not** decide whether `data_dir` may be used. A single-node PostgreSQL/MySQL deployment may still use filesystem blobs and local Tantivy. In cluster mode, local node state is rejected unless the configured backend is explicitly shared. +Docker Compose examples (SQLite; PostgreSQL + S3 + OpenSearch; MySQL + NFS + OpenSearch; HA with Garage) are in [`docker/`](docker/README.md). -Blob backends: +## Documentation -- `storage.backend="filesystem"` — local disk in single-node mode; set `storage.shared=true` only for a real shared POSIX mount such as NFS, CephFS or GlusterFS. -- `storage.backend="s3"` — AWS S3 or compatible APIs such as MinIO/Ceph RGW. - -Physical blob deletion is durable across all blob backends. RustBox records deletion work in the per-account SQL `blob_delete_outbox` in the same transaction that removes attachment metadata, attempts the delete immediately, and retries temporary failures in the background with leases/backoff. This covers local disk, shared POSIX/NFS and S3-compatible storage without permanently dead-lettering failed cleanup. - -Search backends: - -- `search.backend="local_tantivy"` — single-node. -- `search.backend="opensearch"` — cluster-capable. Index mutations are written first to a durable per-tenant SQL outbox and processed with retry/backoff/dead-letter handling. -- `search.backend="disabled"` — no full-text index. - -In a cluster using shared SQL + S3/shared POSIX + OpenSearch/disabled search + `pgp_enabled=false`, `data_dir` is not required as persistent node state. See `docker/README.md` for ready Compose variants. - -Administrator-only deep runtime diagnostics can be enabled separately under `[diagnostics_endpoint]`. It is disabled and loopback-only by default, supports a network allowlist plus token or Basic Auth, redacts secrets, and probes SQL/Redis/storage/search/session/runtime/security state. See [`docs/SYSTEM_DIAGNOSTICS.md`](docs/SYSTEM_DIAGNOSTICS.md). +| Document | Contents | +|---|---| +| [Features](docs/FEATURES.md) | Everything RustBox does, with screenshots | +| [Storage and search](docs/STORAGE_SEARCH_OPERATIONS.md) | Backends, shared storage, migrations, failure modes | +| [System diagnostics](docs/SYSTEM_DIAGNOSTICS.md) | Administrator diagnostics endpoint | +| [IMAP connection pool](docs/imap-pool.md) | How IMAP sessions are reused | +| [Docker](docker/README.md) | Compose deployments | +| [Releasing](docs/RELEASING.md) · [Validation](docs/VALIDATION.md) | Release process and checks | +| [Implementation status](docs/IMPLEMENTATION_STATUS.md) | Detailed technical status | +| [Changelog](CHANGELOG.md) | Changes per version | +| [Database migrations](migrations/README.md) | Schema baseline and upgrades | ## Configure mail servers @@ -117,6 +116,27 @@ Mail-size limits are intentionally separate. `max_attachment_size_mb` applies to Attachments are streamed to blob storage on upload, but sending a message or saving an IMAP draft snapshot has to hold them in memory while the MIME message is built. RustBox therefore (1) rejects an attachment set whose base64-encoded size cannot fit `max_message_size_mb` with `413` before any file is read into memory, and also while an upload is still streaming, and (2) allows only `max_concurrent_message_builds` such operations at the same time; further requests wait up to `message_build_wait_secs` and then receive `503`. Peak RAM for these operations is roughly `max_concurrent_message_builds` x 4 x `max_message_size_mb`. Fully streamed MIME assembly is not implemented. +## Storage and search + +RustBox separates three concerns: SQL metadata, blob storage and search. The SQL engine does **not** decide whether `data_dir` may be used. A single-node PostgreSQL/MySQL deployment may still use filesystem blobs and local Tantivy. In cluster mode, local node state is rejected unless the configured backend is explicitly shared. + +Blob backends: + +- `storage.backend="filesystem"` — local disk in single-node mode; set `storage.shared=true` only for a real shared POSIX mount such as NFS, CephFS or GlusterFS. +- `storage.backend="s3"` — AWS S3 or compatible APIs such as MinIO/Ceph RGW. + +Physical blob deletion is durable across all blob backends. RustBox records deletion work in the per-account SQL `blob_delete_outbox` in the same transaction that removes attachment metadata, attempts the delete immediately, and retries temporary failures in the background with leases/backoff. This covers local disk, shared POSIX/NFS and S3-compatible storage without permanently dead-lettering failed cleanup. + +Search backends: + +- `search.backend="local_tantivy"` — single-node. +- `search.backend="opensearch"` — cluster-capable. Index mutations are written first to a durable per-tenant SQL outbox and processed with retry/backoff/dead-letter handling. +- `search.backend="disabled"` — no full-text index. + +In a cluster using shared SQL + S3/shared POSIX + OpenSearch/disabled search + `pgp_enabled=false`, `data_dir` is not required as persistent node state. See `docker/README.md` for ready Compose variants. + +Administrator-only deep runtime diagnostics can be enabled separately under `[diagnostics_endpoint]`. It is disabled and loopback-only by default, supports a network allowlist plus token or Basic Auth, redacts secrets, and probes SQL/Redis/storage/search/session/runtime/security state. See [`docs/SYSTEM_DIAGNOSTICS.md`](docs/SYSTEM_DIAGNOSTICS.md). + ## Database, storage, search and cluster traffic `database_backend = "sqlite" | "mysql" | "pgsql"` selects only the authoritative SQL database. It does not force or forbid local blob/search storage. In particular, PostgreSQL/MySQL are valid in a single-node installation together with `storage.backend="filesystem"` and `search.backend="local_tantivy"`. @@ -183,8 +203,28 @@ rustbox search reindex Ready Docker examples are in `docker/`: standalone SQLite, PostgreSQL + SILO (S3-compatible) + OpenSearch, and MySQL + NFS + OpenSearch. -All executable application SQL is centralized by dialect under `src/queries/{sqlite,mysql,pgsql}/queries.rs`. Migrations V047-V052 are consolidated into the V052 compatibility baseline; databases older than V052 must first be upgraded with a release that can reach that baseline (0.2.22). RustBox 0.2.23 adds V053 for the appearance `skin_depth` and `skin_gradients` settings and 0.2.24 adds V054 for the time-of-day theme and per-account colour; fresh schemas already contain the columns, while existing tenants migrate automatically. V055 is the next migration number. See `migrations/README.md`. +Database schema upgrades run automatically at startup. Databases older than the consolidated baseline (V052) must first be opened once with RustBox 0.2.22. See `migrations/README.md` for the schema history; V055 is the next migration number. +## ManageSieve + +Supported modes: + +- `starttls` — recommended; +- `implicit` — TLS immediately after connect; +- `none` — plaintext transport; reusable credential authentication remains blocked unless `sieve_allow_insecure_auth = true` is explicitly enabled. + +When `sieve_host` is not configured, RustBox uses the authenticated session's IMAP host. Sieve uses the same session credentials; passwords are not exposed to the browser after login. + +## Native installation + +```bash +./scripts/build-release.sh +sudo ./scripts/install-systemd.sh +sudo systemctl enable --now rustbox +sudo journalctl -u rustbox -f +``` + +For public access put an HTTPS reverse proxy in front of RustBox. The production example binds to `127.0.0.1:3001`. ## Development @@ -214,72 +254,27 @@ cargo test cargo build --release ``` -## UI +## Project layout -Multi-account browser sessions are enabled by default and can be disabled by the administrator with `multi_account_enabled = false`. When enabled, signed-in accounts can be switched from the top bar and selected together for new-mail notifications. - -RustBox uses a compact three-pane mail layout: folders, message list and reader. The current internal baseline includes configurable special IMAP folders, collapsible folder hierarchy, folder CRUD/subscriptions, move/copy/bulk actions, drag-and-drop message moves, collapsible threads, raw headers/source download, labels, advanced/saved search, quota warnings, contacts/groups/vCard, calendar/ICS import, identities/signatures with automatic reply identity selection, HTML/plain/Markdown compose modes, CID inline images, and ManageSieve raw + visual rule editing with negation, dynamically addable conditions/actions and nested AND/OR chaining. OpenPGP includes per-user key management, identity binding, PGP/MIME + inline sign/encrypt/decrypt/verify, WKD/HKP discovery and private-key passphrase management. - -IMAP IDLE events are delivered to the browser over the embedded WebSocket endpoint for realtime refresh and optional desktop notifications. Notification preferences include sound, folder selection and Do Not Disturb hours. - -Appearance preferences are persisted per account and include light/dark/system theme, density, preview position, UI scale, multiple color presets, custom accent/background/panel/text colors, font, row height, pane widths, radius, contrast and visibility toggles. Administrators can set the global light/dark mode with `default_theme = "system" | "dark" | "light"` and the global interface style with `default_interface_skin = "rustbox" | "gmail" | "proton" | "zoho" | "outlook" | ...` in TOML. The global interface style is used on the login screen and by accounts set to **Global default**. Built-in interface styles include RustBox, Roundcube variants, Clean, Graphite, High contrast, Gmail/Proton/Zoho/Outlook/Fastmail/Apple Mail/Yahoo/HEY/GitHub/GitLab/Discord/Slack/Reddit/Notion-inspired profiles, Nord and Minimal. Predefined styles can optionally unlock the per-account UI scale control. The mailbox toolbar also exposes persisted date/sender/subject/size sorting, and remote images can be restricted to saved contacts. - -The native RustBox interface intentionally uses flat surfaces, small radii and one SVG icon set. Compatibility skins may reproduce upstream visual treatments such as gradients, while toolbar icons remain SVG rather than Unicode glyphs. - -## ManageSieve - -Supported modes: - -- `starttls` — recommended; -- `implicit` — TLS immediately after connect; -- `none` — plaintext transport; reusable credential authentication remains blocked unless `sieve_allow_insecure_auth = true` is explicitly enabled. - -When `sieve_host` is not configured, RustBox uses the authenticated session's IMAP host. Sieve uses the same session credentials; passwords are not exposed to the browser after login. - -## Native installation - -```bash -./scripts/build-release.sh -sudo ./scripts/install-systemd.sh -sudo systemctl enable --now rustbox -sudo journalctl -u rustbox -f +```text +src/ Rust application (Axum server, IMAP/SMTP, Sieve, storage, search) +src/web/ Browser interface: index.html, css/, js/, i18n/, icons.svg +migrations/ SQL schemas for SQLite, MySQL and PostgreSQL +docker/ Compose examples +packaging/ systemd unit, logrotate and production config +scripts/ Build, install, release and Roundcube import helpers +docs/ Documentation and screenshots ``` -For public access put an HTTPS reverse proxy in front of RustBox. The production example binds to `127.0.0.1:3001`. +`build.rs` bundles the CSS and JavaScript modules listed in `src/web/css/manifest.txt` and `src/web/js/manifest.txt`; the bundles, HTML, translations and icons are embedded in the executable. -## Following Oxi +## License -Initialize Git after extracting the release archive: +Copyright © 2026 Mateusz Gruszczyński ([linuxiarz.pl](https://linuxiarz.pl)). -```bash -git init -git add . -git commit -m "RustBox 0.2.16 release" -./scripts/setup-upstream.sh -``` +RustBox is dual-licensed: -Inspect newer Oxi changes: +- **GNU Affero General Public License v3** ([`LICENSE`](LICENSE)) — free to use, modify and share, also in companies. If you run a modified version as a network service, offer its source code to its users. +- **Commercial license** — for organisations that prefer different terms, for example to keep their changes private. Paid; terms by agreement. Contact: [linuxiarz@linuxiarz.pl](mailto:linuxiarz@linuxiarz.pl). Contacting the author is voluntary and never required for use that follows the AGPL. -```bash -./scripts/oxi-status.sh -``` - -Import the mapped Oxi backend delta: - -```bash -./scripts/sync-upstream.sh -``` - -Oxi `backend/...` paths are mapped into RustBox's flat Cargo tree. Oxi frontend sources are never copied. Relevant upstream frontend behavior is reviewed and reimplemented in `src/web/`. `UPSTREAM_BASE` advances only after `./scripts/finish-upstream.sh` completes review and the build gate. - -To port one backend commit without advancing the baseline: - -```bash -./scripts/oxi-port-commit.sh -``` - -See `docs/UPSTREAM.md` for details. - -## Attribution - -See `NOTICE.md`. The supplied Oxi snapshot did not contain an authoritative `LICENSE` file; verify the exact upstream licensing terms before public redistribution. +Details: [`LICENSING.md`](LICENSING.md). The first version of RustBox was created as a fork of [Oxi](https://github.com/c0h1b4/oxi) (MIT License); see [`NOTICE.md`](NOTICE.md). diff --git a/RELEASE-MANIFEST.txt b/RELEASE-MANIFEST.txt index 2092a84..92fd3c8 100644 --- a/RELEASE-MANIFEST.txt +++ b/RELEASE-MANIFEST.txt @@ -1,4 +1,11 @@ -RustBox 0.2.25 release manifest +RustBox 0.2.26 release manifest + +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` +- removes the Oxi synchronisation tooling (`scripts/*upstream*.sh`, `scripts/oxi-*.sh`, `UPSTREAM_BASE`, `docs/UPSTREAM.md`); RustBox is developed independently +- redesigned Settings → About (author, repository, license, origin) +- documentation: `docs/FEATURES.md` with screenshots in `docs/screenshots/`, rewritten README and user-facing CHANGELOG +- Polish settings notes are rendered after translations load; verbose code comments trimmed 0.2.25 release changes: - filters: "Apply now" runs a visual Sieve rule on messages already in a folder; `POST /api/filters/match` finds matches with IMAP `UID SEARCH` (exact lower bound, superset upper bound, the gap decided from the local cache; undecidable messages are reported and skipped), and the client applies flags/labels, copies and a final move; per-rule "Apply now" in the rule list and an "apply to existing messages" option in the quick filter dialog @@ -111,14 +118,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.25 configuration +- README.md documents 0.2.26 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.25 +- Cargo.toml version = 0.2.26 - 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 29323f2..9e3814d 100644 --- a/RELEASE-VALIDATION.txt +++ b/RELEASE-VALIDATION.txt @@ -1,4 +1,7 @@ -RustBox 0.2.25 - final source validation +RustBox 0.2.26 - 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.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" @@ -64,7 +67,7 @@ RustBox 0.2.25 - 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.25 +- version metadata identifies 0.2.26 Completed in this environment: - offline release validator: scripts/validate-release-static.py (455/455 checks passed) @@ -72,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.25 +- release metadata consistently identifies 0.2.26 - 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/UPSTREAM_BASE b/UPSTREAM_BASE deleted file mode 100644 index 72b6656..0000000 --- a/UPSTREAM_BASE +++ /dev/null @@ -1 +0,0 @@ -7f1507c486b78cf33e54beb701dd460d3bd2bd5d diff --git a/docs/FEATURES.md b/docs/FEATURES.md new file mode 100644 index 0000000..399a92e --- /dev/null +++ b/docs/FEATURES.md @@ -0,0 +1,164 @@ +# RustBox features + +RustBox is a self-hosted webmail for any standards-compliant IMAP/SMTP server. It ships as one Rust executable with the whole interface built in. The interface is available in English and Polish. + +![Mail view](screenshots/mail-light.png) + +## Contents + +- [Mail](#mail) +- [Composing](#composing) +- [Search, labels and contacts](#search-labels-and-contacts) +- [Filters (Sieve)](#filters-sieve) +- [Calendar](#calendar) +- [Notifications and realtime](#notifications-and-realtime) +- [Appearance](#appearance) +- [Mobile](#mobile) +- [Security and privacy](#security-and-privacy) +- [Administration and deployment](#administration-and-deployment) +- [Known limitations](#known-limitations) + +## Mail + +- Three-pane layout (folders, message list, reader) with resizable panes. The preview can sit on the right or at the bottom, or be turned off. +- Folder tree with subfolders shown under their parent and collapsible branches. You can create, rename, delete and subscribe to folders, mark all as read and empty a folder. +- Special folders (Sent, Drafts, Trash, Junk, Archive) are configured per account. They follow renames, and administrators can set server-wide defaults. +- Accurate total and unread counters, kept in sync with the mail server. Changes appear immediately and are reconciled in the background. +- Threaded or flat reading, previous/next navigation, and automatic opening of the next message after a move or delete. +- Bulk actions on many messages at once, including the whole search result: read/unread, flag, archive, move, copy, label, spam and delete. +- Drag and drop a message onto a folder to move it. Moves and deletes can be undone. +- Snooze: hide a message until a chosen time, then see all snoozed messages in one list. +- Pagination (up to 300 per page), progressive loading or loading the whole folder. Sort by date, sender, subject or size, and optionally keep flagged messages on top per folder. +- A compact single-line row layout when the message list is wide. +- Safe HTML preview in an isolated frame. Remote images are blocked until you allow them, or loaded only for contacts or trusted senders. +- Attachment preview for images, PDF, text, HTML and video. You can download one attachment or all of them as a ZIP. +- Raw headers, message source, `.eml` download and `.eml` import into any folder. +- One-click unsubscribe from mailing lists when the sender supports it. +- A short context menu with submenus (forward, labels, snooze, filters, sender, more), designed to always fit the window. +- Mailbox quota with warnings at 80% and 95%. +- Several accounts signed in at the same time in one browser, with quick switching. + +![Context menu](screenshots/context-menu.png) + +## Composing + +- HTML, plain-text and Markdown editors. The HTML toolbar covers fonts, sizes, colours, lists, quotes, alignment, links, undo and redo. +- Inline images: paste or drag and drop them, then resize, align and crop in place. +- Attachments by drag and drop, with upload progress and a reminder when the text mentions an attachment that isn't there. +- Drafts are saved automatically and can be recovered. Drafts from the Drafts folder, including ones written in other mail clients, open directly for editing. +- Undo Send: a short window to cancel a message after pressing Send. +- Multiple identities, each with Reply-To, automatic Bcc, organisation and text/HTML signatures. Replies pick the matching identity automatically. +- Priority, read-receipt requests and recipient autocomplete from contacts. +- Forward as attachment. +- OpenPGP: key management, signing and encryption (PGP/MIME and inline), decryption and verification, and key discovery. + +![Composer](screenshots/compose.png) + +## Search, labels and contacts + +- Full-text search with operators (`from:`, `to:`, `subject:`, `folder:`, `has:attachment`, `is:unread`, `is:flagged`, dates). Matching terms are highlighted, searches can be saved and there is an advanced search dialog. +- Coloured labels with bulk assignment. They can optionally be kept in sync with IMAP keywords, so they are visible in other clients. +- Contacts with photos, several e-mail addresses, phone numbers and postal addresses, plus groups, duplicate detection and merging. +- vCard import and export, including importing a vCard straight from a message. + +![Contacts](screenshots/contacts.png) + +## Filters (Sieve) + +RustBox manages server-side filters over ManageSieve, the standard used by Dovecot Pigeonhole and others. + +- **Visual rule editor:** conditions on sender, domain, recipients (To/Cc, To only, Cc only), subject, mailing list, any header, message size, attachments and priority. Conditions can be joined with AND/OR and negated, and a rule can have **exceptions** ("except if…"). +- **Actions:** move, copy, move to Trash, add a label, mark as read, flag, redirect (optionally keeping a copy) and discard. A rule can run several actions and stop processing further rules. +- **Templates** for common rules: messages from a sender to a folder, subject words, flagging mail from a VIP, newsletters, mail sent to an alias, large attachments, forwarding a copy. +- **Apply now:** a rule normally affects only new mail. "Check matches" shows how many messages already in a folder match it, and "Apply now" runs its actions on them. Each rule in the list also has its own "Apply now". +- **Always move messages from this sender…** straight from the message context menu, and quick filters created from a message's sender, domain or subject. +- Offers to create a target folder that does not exist yet. +- Every rule is summarised in plain language ("When … Then …") and shown with a live preview of the generated script. +- Out-of-office replies with optional start and end dates, plus a raw script editor with server-side validation, backup and restore. + +![Filter editor](screenshots/filters-editor.png) + +![Apply a filter to existing mail](screenshots/filters-apply-now.png) + +## Calendar + +- Month, week, day and agenda views. +- Events with all-day mode, location, attendees, status, colour and a meeting link. +- Two reminders per event (for example 15 minutes and one day before), with default reminders for new events. Reminders for all-day events fire at the start of the working day. +- ICS import and export, including importing an invitation attachment. + +![Calendar](screenshots/calendar.png) + +## Notifications and realtime + +- New mail shows up immediately thanks to IMAP IDLE and a live connection to the browser. +- Desktop notifications with an optional sound, a choice of folders to watch and Do Not Disturb hours. +- The unread count appears in the browser tab and, where supported, on the app icon. + +## Appearance + +- Light, dark, system or **time-of-day** theme. +- **43 interface styles**: + - RustBox and classic styles; + - Roundcube Classic, Larry and Elastic; + - styles inspired by popular mail clients and apps; + - editor themes such as Nord, Dracula, Catppuccin, Solarized, Gruvbox and Tokyo Night; + - atmospheric styles; + - retro styles: Windows XP, Mac OS X Aqua, iOS 6, Windows 7, Frutiger Aero and BeOS. +- **Style depth:** + - *colours only*; + - *standard*, which adds the style's layout; + - *immersive*, which also adds the style's shapes, typography, app bar and backdrop. +- An optional **Style gradients** switch. +- A predefined style's layout and typography can be adjusted while it keeps its look. Changing a colour creates a custom copy. +- The RustBox custom style is fully adjustable: colours, fonts, sizes, density, pane widths, corner radius, contrast, selection colour, unread styling, icon size and more. A live warning appears when your custom colours are hard to read. +- An optional colour per account, so it is always clear which account is active. +- Keyboard shortcuts with an in-app reference. + +![Appearance settings](screenshots/settings-appearance.png) + +| Gmail | Outlook (immersive) | +|---|---| +| ![Gmail style](screenshots/style-gmail.png) | ![Outlook style](screenshots/style-outlook.png) | +| **Dracula (immersive, dark)** | **Windows XP (immersive, gradients)** | +| ![Dracula style](screenshots/style-dracula.png) | ![Windows XP style](screenshots/style-winxp.png) | + +More: [dark theme](screenshots/mail-dark.png) · [Roundcube Elastic](screenshots/style-roundcube-elastic.png) + +## Mobile + +The interface adapts to phones and tablets: a folder drawer, a full-screen reader and touch-friendly menus. + +| Message list | Reader (dark) | +|---|---| +| Mobile list | Mobile reader | + +## Security and privacy + +- Mail server addresses are set by the administrator; users only enter their e-mail address and password. +- Protection against password guessing: per-mailbox and per-address lockouts with growing delays, applied before the mail server is contacted. +- Encrypted, revocable sessions with a list of active sessions and devices. +- Optional strict browser security headers. +- HTML mail is sanitised and shown in an isolated frame. Links open outside the preview, and remote content is blocked by default. +- Secrets are never shown in diagnostics. + +## Administration and deployment + +- A single executable with the interface built in, a systemd service and Docker Compose examples. +- Databases: SQLite, MySQL/MariaDB or PostgreSQL. Sessions can also be kept in Redis. +- Attachment storage on local disk, a shared filesystem (NFS, CephFS, GlusterFS) or S3-compatible storage. +- Search with a built-in index or OpenSearch. A cluster mode runs several nodes behind a load balancer. +- Health and readiness endpoints, a diagnostics panel for users and an optional, protected diagnostics endpoint for administrators. +- Features can be switched on and off: filters, OpenPGP, multi-account, diagnostics, About page. +- A helper script imports contacts and identities from Roundcube. + +See the [README](../README.md) for configuration, [Storage and search](STORAGE_SEARCH_OPERATIONS.md) and [System diagnostics](SYSTEM_DIAGNOSTICS.md). + +## Known limitations + +- No CalDAV or CardDAV synchronisation yet. Calendar invitations cannot be answered from RustBox, and recurring events are stored but not expanded. +- No password-change integration and no IMAP shared-folder (ACL) management. +- Notifications work while RustBox is open in a browser tab. +- The current status of every area is tracked in [Implementation status](IMPLEMENTATION_STATUS.md). + +![About](screenshots/about.png) diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 111e642..0c0d5bd 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,7 +1,13 @@ -# RustBox 0.2.25 - implementation status +# RustBox 0.2.26 - 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.25` 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.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. + +## 0.2.26 licensing and documentation + +- Licensed under AGPL-3.0-only with an optional commercial license (`LICENSE`, `LICENSING.md`, `NOTICE.md`). The Oxi synchronisation scripts and `UPSTREAM_BASE` were removed; the About page no longer embeds an Oxi revision. +- `docs/FEATURES.md` lists all features with screenshots (`docs/screenshots/`, generated against a mocked API). +- Translations are applied to dynamic settings notes after they load. ## 0.2.25 filters @@ -113,11 +119,10 @@ This file tracks the `0.2.25` source snapshot. Storage/search scope and outstand ## Implemented / wired in this working snapshot -### Runtime and upstream +### Runtime - Single Rust runtime with embedded HTML/CSS/vanilla-JavaScript UI. - No TypeScript, React, Next.js, Node.js or Bun product build/runtime. -- Oxi upstream tracking and mapped backend-port scripts retained. - EN/PL language assets embedded in the binary. ### Mail and IMAP workflow diff --git a/docs/RELEASING.md b/docs/RELEASING.md index a100df5..22ba153 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,7 +1,7 @@ # Releasing RustBox 1. Update the version in `Cargo.toml` and the RustBox package entry in `Cargo.lock`. -2. Confirm `UPSTREAM_BASE` is the Oxi revision actually consumed by the release. +2. Update `CHANGELOG.md` and the release notes. 3. Ensure no TypeScript sources or separate frontend build dependencies are present. 4. Run the release gate and package only after it passes: diff --git a/docs/UPSTREAM.md b/docs/UPSTREAM.md deleted file mode 100644 index 9a1f567..0000000 --- a/docs/UPSTREAM.md +++ /dev/null @@ -1,144 +0,0 @@ -# Oxi upstream policy - -RustBox is derived from `c0h1b4/oxi`. `UPSTREAM_BASE` contains the exact Oxi commit whose backend changes have been consumed by RustBox. - -RustBox uses a flat Cargo layout, while Oxi uses a `backend/` subtree. The sync tooling therefore treats Oxi as a source upstream and maps its backend paths into RustBox rather than reintroducing the Oxi directory layout. - -## Path mapping - -Automatically mapped examples: - -```text -Oxi backend/Cargo.toml -> RustBox Cargo.toml -Oxi backend/Cargo.lock -> RustBox Cargo.lock -Oxi backend/rust-toolchain.toml -> RustBox rust-toolchain.toml -Oxi backend/src/... -> RustBox src/... -Oxi backend/examples/... -> RustBox examples/... -Oxi backend/docs/... -> RustBox docs/... -``` - -Review-only boundaries, never mapped automatically: - -```text -Oxi backend/migrations/... -> review against RustBox migrations/... -Oxi backend/tests/... -> review/port as native Rust tests when useful -``` - -RustBox-owned `src/web/` has no automatic mapping from the Oxi frontend. - -## Configure the upstream - -A release archive does not contain `.git`. Initialize the RustBox repository once: - -```bash -git init -git add . -git commit -m "RustBox 0.1.2 baseline" -./scripts/setup-upstream.sh -``` - -The script adds: - -```text -upstream-oxi -> https://github.com/c0h1b4/oxi.git -``` - -and fetches Oxi history/tags. - -## Inspect a newer Oxi state - -```bash -./scripts/oxi-status.sh -``` - -or inspect a specific Oxi ref: - -```bash -./scripts/oxi-status.sh upstream-oxi/main -./scripts/oxi-status.sh -``` - -The report separates: - -1. Oxi backend changes that can be mapped automatically; -2. Oxi backend migrations/tests that require manual review; -3. Oxi frontend changes that require a manual UI port; -4. other Oxi root-level changes that require manual review; -5. upstream commits since `UPSTREAM_BASE`. - -## Advance the consumed Oxi backend state - -```bash -./scripts/sync-upstream.sh -``` - -The operation: - -1. requires a clean RustBox Git worktree; -2. fetches `upstream-oxi`; -3. verifies that the target descends from the recorded `UPSTREAM_BASE`; -4. creates a `sync/oxi-*` review branch; -5. generates the Oxi backend delta from `UPSTREAM_BASE` to the target, excluding `backend/migrations/` and `backend/tests/`; -6. rewrites only patch metadata so `backend/` becomes ``; -7. applies the mapped delta with Git three-way merge support; -8. does not import Oxi frontend sources, migrations, or tests; -9. records the target as a pending sync inside Git metadata; -10. deliberately leaves `UPSTREAM_BASE` unchanged until the maintainer finishes review. - -Conflicts are expected when Oxi and RustBox changed the same functionality. Resolve them in the flat RustBox paths, keeping RustBox-specific behavior where required. - -After resolving backend conflicts and porting any relevant UI behavior, finish the sync: - -```bash -./scripts/finish-upstream.sh -``` - -If Oxi changed its frontend, root-level files, or the RustBox-owned upstream migration/test areas, the finish step refuses to advance the baseline until those reviews are explicitly acknowledged: - -```bash -./scripts/finish-upstream.sh --ui-reviewed --root-reviewed --rustbox-owned-reviewed -``` - -`--rustbox-owned-reviewed` confirms that changes under Oxi `backend/migrations/` and `backend/tests/` were inspected and any relevant behavior was manually ported. Those files are never copied into RustBox automatically. - -The finish step runs the RustBox release build gate and only then stages the new `UPSTREAM_BASE`. Review `git status` and commit the sync. `--skip-build` exists for exceptional local workflows, but CI should still run the full gate before merge. - -If the sync is rejected, discard the review branch; the main branch and its `UPSTREAM_BASE` remain unchanged. - -## Port one Oxi feature - -For a useful Oxi commit that should be brought in before a full baseline update: - -```bash -./scripts/oxi-port-commit.sh -``` - -This creates `port/oxi-` and maps only the backend portion of that single commit. It intentionally does not change `UPSTREAM_BASE`, because RustBox has not consumed the complete Oxi history up to that revision. - -When a later full sync includes a previously ported feature, Git may report that some hunks are already present; review those conflicts instead of duplicating the code. - -## UI policy - -Oxi frontend source is never copied into RustBox. For a new Oxi feature that includes frontend work: - -- consume or port the backend/API changes; -- inspect the corresponding Oxi frontend diff to understand behavior; -- implement that behavior in RustBox `src/web/`; -- keep the same-origin RustBox API/UI model; -- do not add `.ts` or `.tsx` sources. - -## RustBox-owned boundaries - -These areas require explicit review and should not be replaced blindly by upstream changes: - -- `migrations/` (RustBox owns the squashed baseline and future migration numbering); -- `tests/` (RustBox keeps native Rust/runtime fixtures rather than importing upstream Python contracts); -- `src/web/`; -- `src/ui.rs`; -- RustBox UI routing and same-origin behavior; -- RustBox ManageSieve integration; -- `dev.sh`; -- native packaging/release scripts; -- RustBox branding and documentation. - -The goal is to keep following Oxi protocol/backend improvements without restoring its split frontend/backend repository structure. diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index 1be06e2..0901ac8 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -11,12 +11,10 @@ Release source checks: - mail endpoints are configuration-owned; the login request contains only email/password/session preference; - authenticated mail routes reuse the resolved session connection profile; - CSS is built by Cargo `build.rs` from `src/web/css/manifest.txt`; -- no CSS gradients and no Unicode pseudo-icons in the browser UI; +- gradients appear only when the "Style gradients" switch is on; no Unicode pseudo-icons in the browser UI; - all UI action icons are served from the embedded SVG sprite; - `dev.sh` runs the root Cargo package; - native release build outputs `target/release/rustbox`; -- `UPSTREAM_BASE` records the consumed Oxi backend revision; -- upstream tooling maps Oxi `backend/` deltas into RustBox's flat paths and never imports Oxi frontend source. Full release gate on a host with Rust/Cargo and dependency access: diff --git a/docs/screenshots/about.png b/docs/screenshots/about.png new file mode 100644 index 0000000..0a046f3 Binary files /dev/null and b/docs/screenshots/about.png differ diff --git a/docs/screenshots/calendar.png b/docs/screenshots/calendar.png new file mode 100644 index 0000000..4ac2160 Binary files /dev/null and b/docs/screenshots/calendar.png differ diff --git a/docs/screenshots/compose.png b/docs/screenshots/compose.png new file mode 100644 index 0000000..666807a Binary files /dev/null and b/docs/screenshots/compose.png differ diff --git a/docs/screenshots/contacts.png b/docs/screenshots/contacts.png new file mode 100644 index 0000000..e2ef8a4 Binary files /dev/null and b/docs/screenshots/contacts.png differ diff --git a/docs/screenshots/context-menu.png b/docs/screenshots/context-menu.png new file mode 100644 index 0000000..3cb3d9a Binary files /dev/null and b/docs/screenshots/context-menu.png differ diff --git a/docs/screenshots/filters-apply-now.png b/docs/screenshots/filters-apply-now.png new file mode 100644 index 0000000..0cebae8 Binary files /dev/null and b/docs/screenshots/filters-apply-now.png differ diff --git a/docs/screenshots/filters-editor.png b/docs/screenshots/filters-editor.png new file mode 100644 index 0000000..dbe8a33 Binary files /dev/null and b/docs/screenshots/filters-editor.png differ diff --git a/docs/screenshots/filters-list.png b/docs/screenshots/filters-list.png new file mode 100644 index 0000000..d300617 Binary files /dev/null and b/docs/screenshots/filters-list.png differ diff --git a/docs/screenshots/mail-dark.png b/docs/screenshots/mail-dark.png new file mode 100644 index 0000000..e2eff33 Binary files /dev/null and b/docs/screenshots/mail-dark.png differ diff --git a/docs/screenshots/mail-light.png b/docs/screenshots/mail-light.png new file mode 100644 index 0000000..6858cba Binary files /dev/null and b/docs/screenshots/mail-light.png differ diff --git a/docs/screenshots/mobile-list.png b/docs/screenshots/mobile-list.png new file mode 100644 index 0000000..1533726 Binary files /dev/null and b/docs/screenshots/mobile-list.png differ diff --git a/docs/screenshots/mobile-reader.png b/docs/screenshots/mobile-reader.png new file mode 100644 index 0000000..24ca070 Binary files /dev/null and b/docs/screenshots/mobile-reader.png differ diff --git a/docs/screenshots/settings-appearance.png b/docs/screenshots/settings-appearance.png new file mode 100644 index 0000000..c035523 Binary files /dev/null and b/docs/screenshots/settings-appearance.png differ diff --git a/docs/screenshots/style-dracula.png b/docs/screenshots/style-dracula.png new file mode 100644 index 0000000..33b6fe2 Binary files /dev/null and b/docs/screenshots/style-dracula.png differ diff --git a/docs/screenshots/style-gmail.png b/docs/screenshots/style-gmail.png new file mode 100644 index 0000000..762f314 Binary files /dev/null and b/docs/screenshots/style-gmail.png differ diff --git a/docs/screenshots/style-outlook.png b/docs/screenshots/style-outlook.png new file mode 100644 index 0000000..59ad39a Binary files /dev/null and b/docs/screenshots/style-outlook.png differ diff --git a/docs/screenshots/style-roundcube-elastic.png b/docs/screenshots/style-roundcube-elastic.png new file mode 100644 index 0000000..b3d3590 Binary files /dev/null and b/docs/screenshots/style-roundcube-elastic.png differ diff --git a/docs/screenshots/style-winxp.png b/docs/screenshots/style-winxp.png new file mode 100644 index 0000000..a8404e2 Binary files /dev/null and b/docs/screenshots/style-winxp.png differ diff --git a/scripts/finish-upstream.sh b/scripts/finish-upstream.sh deleted file mode 100755 index eb92237..0000000 --- a/scripts/finish-upstream.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT" - -UI_REVIEWED=0 -ROOT_REVIEWED=0 -OWNED_REVIEWED=0 -SKIP_BUILD=0 -for arg in "$@"; do - case "$arg" in - --ui-reviewed) UI_REVIEWED=1 ;; - --root-reviewed) ROOT_REVIEWED=1 ;; - --rustbox-owned-reviewed) OWNED_REVIEWED=1 ;; - --skip-build) SKIP_BUILD=1 ;; - *) echo "usage: $0 [--ui-reviewed] [--root-reviewed] [--rustbox-owned-reviewed] [--skip-build]" >&2; exit 2 ;; - esac -done - -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "error: not a Git checkout" >&2 - exit 1 -fi -PENDING_FILE="$(git rev-parse --git-path rustbox-upstream-pending)" -if [ ! -f "$PENDING_FILE" ]; then - echo "error: no pending Oxi sync; run ./scripts/sync-upstream.sh first" >&2 - exit 1 -fi - -BASE="$(sed -n 's/^base=//p' "$PENDING_FILE")" -TARGET_SHA="$(sed -n 's/^target=//p' "$PENDING_FILE")" -BRANCH="$(sed -n 's/^branch=//p' "$PENDING_FILE")" -CURRENT_BRANCH="$(git branch --show-current)" - -if [ -z "$BASE" ] || [ -z "$TARGET_SHA" ] || [ -z "$BRANCH" ]; then - echo "error: invalid pending sync metadata" >&2 - exit 1 -fi -if [ "$CURRENT_BRANCH" != "$BRANCH" ]; then - echo "error: pending Oxi sync belongs to branch '$BRANCH'; current branch is '$CURRENT_BRANCH'" >&2 - exit 1 -fi -if git diff --name-only --diff-filter=U | grep -q .; then - echo "error: unresolved merge conflicts remain" >&2 - git diff --name-only --diff-filter=U >&2 - exit 1 -fi -if find . -path ./.git -prune -o -type f \( -name '*.ts' -o -name '*.tsx' \) -print -quit | grep -q .; then - echo "error: TypeScript source exists in RustBox" >&2 - exit 1 -fi - -if ! git diff --quiet "$BASE..$TARGET_SHA" -- backend/migrations/ backend/tests/; then - if [ "$OWNED_REVIEWED" -ne 1 ]; then - echo "error: Oxi changed backend migrations/tests that RustBox owns independently." >&2 - echo "Review/port only relevant behavior, then rerun with --rustbox-owned-reviewed." >&2 - git diff --name-status "$BASE..$TARGET_SHA" -- backend/migrations/ backend/tests/ >&2 || true - exit 1 - fi -fi - -if ! git diff --quiet "$BASE..$TARGET_SHA" -- frontend/; then - if [ "$UI_REVIEWED" -ne 1 ]; then - echo "error: Oxi frontend changed between $BASE and $TARGET_SHA." >&2 - echo "Review/port the relevant behavior, then rerun with --ui-reviewed." >&2 - git diff --name-status "$BASE..$TARGET_SHA" -- frontend/ >&2 || true - exit 1 - fi -fi - -if ! git diff --quiet "$BASE..$TARGET_SHA" -- . ':(exclude)backend/**' ':(exclude)frontend/**'; then - if [ "$ROOT_REVIEWED" -ne 1 ]; then - echo "error: Oxi root-level files changed between $BASE and $TARGET_SHA." >&2 - echo "Review relevant build/config/docs changes, then rerun with --root-reviewed." >&2 - git diff --name-status "$BASE..$TARGET_SHA" -- . ':(exclude)backend/**' ':(exclude)frontend/**' >&2 || true - exit 1 - fi -fi - -if [ "$SKIP_BUILD" -ne 1 ]; then - ./scripts/build-release.sh -else - echo "warning: build gate skipped by explicit --skip-build" >&2 -fi - -printf '%s\n' "$TARGET_SHA" > UPSTREAM_BASE -git add UPSTREAM_BASE -rm -f "$PENDING_FILE" - -printf 'Oxi sync review completed: %s -> %s\n' "$BASE" "$TARGET_SHA" -printf 'UPSTREAM_BASE is staged. Review git status and commit the sync.\n' diff --git a/scripts/oxi-port-commit.sh b/scripts/oxi-port-commit.sh deleted file mode 100755 index e4fa729..0000000 --- a/scripts/oxi-port-commit.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT" -REMOTE="upstream-oxi" -COMMIT="${1:-}" - -if [ -z "$COMMIT" ]; then - echo "usage: $0 " >&2 - exit 2 -fi -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "error: not a Git checkout" >&2 - exit 1 -fi -if ! git diff --quiet || ! git diff --cached --quiet; then - echo "error: working tree must be clean before porting an Oxi commit" >&2 - exit 1 -fi -if ! git remote get-url "$REMOTE" >/dev/null 2>&1; then - echo "error: Oxi remote is not configured; run ./scripts/setup-upstream.sh" >&2 - exit 1 -fi - -git fetch "$REMOTE" --tags >/dev/null -SHA="$(git rev-parse "$COMMIT^{commit}")" -PARENT="$(git rev-parse "$SHA^1")" -branch="port/oxi-${SHA:0:12}" -git switch -c "$branch" - -tmp_raw="$(mktemp)" -tmp_mapped="$(mktemp)" -cleanup() { rm -f "$tmp_raw" "$tmp_mapped"; } -trap cleanup EXIT - -git diff --binary --find-renames "$PARENT..$SHA" -- \ - backend/ \ - ':(exclude)backend/migrations/**' \ - ':(exclude)backend/tests/**' > "$tmp_raw" -sed \ - -e 's#^diff --git a/backend/#diff --git a/#' \ - -e 's# b/backend/# b/#' \ - -e 's#^--- a/backend/#--- a/#' \ - -e 's#^+++ b/backend/#+++ b/#' \ - -e 's#^rename from backend/#rename from #' \ - -e 's#^rename to backend/#rename to #' \ - -e 's#^copy from backend/#copy from #' \ - -e 's#^copy to backend/#copy to #' \ - -e 's#^Binary files a/backend/#Binary files a/#' \ - -e 's# and b/backend/# and b/#' \ - "$tmp_raw" > "$tmp_mapped" - -if [ ! -s "$tmp_mapped" ]; then - echo "Oxi commit $SHA has no backend/ changes to map." -else - git apply --3way --index "$tmp_mapped" -fi - -printf 'Ported backend portion of Oxi commit %s onto branch %s.\n' "$SHA" "$branch" -printf 'UPSTREAM_BASE was intentionally NOT changed.\n' -printf 'Review excluded upstream DB/tests separately: git show %s -- backend/migrations/ backend/tests/\n' "$SHA" -printf 'Review the original commit, especially frontend changes: git show %s -- frontend/\n' "$SHA" diff --git a/scripts/oxi-status.sh b/scripts/oxi-status.sh deleted file mode 100755 index 6f6e7fa..0000000 --- a/scripts/oxi-status.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT" -REMOTE="upstream-oxi" -TARGET="${1:-upstream-oxi/main}" - -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "error: not a Git checkout" >&2 - exit 1 -fi -if ! git remote get-url "$REMOTE" >/dev/null 2>&1; then - echo "error: Oxi remote is not configured; run ./scripts/setup-upstream.sh" >&2 - exit 1 -fi - -git fetch "$REMOTE" --tags >/dev/null -BASE="$(tr -d '[:space:]' < UPSTREAM_BASE)" -TARGET_SHA="$(git rev-parse "$TARGET^{commit}")" - -if ! git merge-base --is-ancestor "$BASE" "$TARGET_SHA"; then - echo "error: recorded Oxi base is not an ancestor of $TARGET ($TARGET_SHA)" >&2 - exit 1 -fi - -printf 'RustBox Oxi status\n' -printf ' consumed: %s\n' "$BASE" -printf ' target: %s\n' "$TARGET_SHA" -printf ' commits: %s\n' "$(git rev-list --count "$BASE..$TARGET_SHA")" - -printf '\nOxi backend changes that can be mapped automatically into RustBox root:\n' -git diff --name-status "$BASE..$TARGET_SHA" -- backend/ ':(exclude)backend/migrations/**' ':(exclude)backend/tests/**' || true - -printf '\nOxi backend DB migrations/tests requiring manual review (never auto-imported):\n' -git diff --name-status "$BASE..$TARGET_SHA" -- backend/migrations/ backend/tests/ || true - -printf '\nOxi frontend changes requiring manual RustBox UI port:\n' -git diff --name-status "$BASE..$TARGET_SHA" -- frontend/ || true - -printf '\nOther Oxi repository changes to review manually:\n' -git diff --name-status "$BASE..$TARGET_SHA" -- . ':(exclude)backend/**' ':(exclude)frontend/**' || true - -printf '\nUpstream commits:\n' -git log --oneline --decorate --no-merges "$BASE..$TARGET_SHA" || true diff --git a/scripts/setup-upstream.sh b/scripts/setup-upstream.sh deleted file mode 100755 index eb73b27..0000000 --- a/scripts/setup-upstream.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT" - -REMOTE="upstream-oxi" -URL="https://github.com/c0h1b4/oxi.git" - -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "error: RustBox must be a Git checkout before configuring Oxi upstream." >&2 - echo "Initialize it first:" >&2 - echo " git init" >&2 - echo " git add ." >&2 - echo " git commit -m 'RustBox 0.1.2 baseline'" >&2 - exit 1 -fi - -if git remote get-url "$REMOTE" >/dev/null 2>&1; then - current="$(git remote get-url "$REMOTE")" - if [ "$current" != "$URL" ]; then - echo "error: remote '$REMOTE' already points to: $current" >&2 - echo "expected: $URL" >&2 - exit 1 - fi -else - git remote add "$REMOTE" "$URL" -fi - -git fetch "$REMOTE" --tags - -BASE="$(tr -d '[:space:]' < UPSTREAM_BASE)" -if ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then - echo "error: recorded Oxi base commit is not available after fetch: $BASE" >&2 - exit 1 -fi - -printf 'Oxi upstream configured.\n' -printf 'Remote: %s -> %s\n' "$REMOTE" "$URL" -printf 'Consumed Oxi baseline: %s\n' "$BASE" -printf 'Latest upstream-oxi/main: %s\n' "$(git rev-parse upstream-oxi/main)" diff --git a/scripts/sync-upstream.sh b/scripts/sync-upstream.sh deleted file mode 100755 index 9bb2ee8..0000000 --- a/scripts/sync-upstream.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT" -REMOTE="upstream-oxi" -TARGET="${1:-upstream-oxi/main}" - -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "error: run this inside a RustBox Git checkout" >&2 - exit 1 -fi -if ! git rev-parse --verify HEAD >/dev/null 2>&1; then - echo "error: create the initial RustBox commit before syncing Oxi" >&2 - exit 1 -fi -if ! git diff --quiet || ! git diff --cached --quiet; then - echo "error: working tree must be clean before Oxi sync" >&2 - exit 1 -fi -if ! git remote get-url "$REMOTE" >/dev/null 2>&1; then - echo "error: Oxi remote is not configured; run ./scripts/setup-upstream.sh" >&2 - exit 1 -fi - -git fetch "$REMOTE" --tags -BASE="$(tr -d '[:space:]' < UPSTREAM_BASE)" -TARGET_SHA="$(git rev-parse "$TARGET^{commit}")" - -if [ "$BASE" = "$TARGET_SHA" ]; then - echo "RustBox already consumes Oxi $TARGET_SHA" - exit 0 -fi -if ! git merge-base --is-ancestor "$BASE" "$TARGET_SHA"; then - echo "error: $TARGET_SHA does not descend from recorded Oxi base $BASE" >&2 - echo "Refuse automatic mapping; review the upstream history manually." >&2 - exit 1 -fi - -branch="sync/oxi-$(date -u +%Y%m%d-%H%M%S)" -git switch -c "$branch" - -tmp_raw="$(mktemp)" -tmp_mapped="$(mktemp)" -cleanup() { rm -f "$tmp_raw" "$tmp_mapped"; } -trap cleanup EXIT - -git diff --binary --find-renames "$BASE..$TARGET_SHA" -- \ - backend/ \ - ':(exclude)backend/migrations/**' \ - ':(exclude)backend/tests/**' > "$tmp_raw" -sed \ - -e 's#^diff --git a/backend/#diff --git a/#' \ - -e 's# b/backend/# b/#' \ - -e 's#^--- a/backend/#--- a/#' \ - -e 's#^+++ b/backend/#+++ b/#' \ - -e 's#^rename from backend/#rename from #' \ - -e 's#^rename to backend/#rename to #' \ - -e 's#^copy from backend/#copy from #' \ - -e 's#^copy to backend/#copy to #' \ - -e 's#^Binary files a/backend/#Binary files a/#' \ - -e 's# and b/backend/# and b/#' \ - "$tmp_raw" > "$tmp_mapped" - -PENDING_FILE="$(git rev-parse --git-path rustbox-upstream-pending)" -printf 'base=%s\ntarget=%s\nbranch=%s\n' "$BASE" "$TARGET_SHA" "$branch" > "$PENDING_FILE" - -if [ -s "$tmp_mapped" ]; then - if ! git apply --3way --index "$tmp_mapped"; then - echo >&2 - echo "Oxi backend changes produced conflicts on branch $branch." >&2 - echo "Resolve them in the flat RustBox paths, then run ./scripts/finish-upstream.sh." >&2 - echo "UPSTREAM_BASE has NOT been advanced." >&2 - exit 1 - fi -fi - -if find . -path ./.git -prune -o -type f \( -name '*.ts' -o -name '*.tsx' \) -print -quit | grep -q .; then - echo "error: TypeScript appeared in the RustBox source tree; remove/port it before continuing" >&2 - exit 1 -fi - -printf '\nMapped Oxi backend %s -> %s into RustBox on branch %s.\n' "$BASE" "$TARGET_SHA" "$branch" -printf 'UPSTREAM_BASE remains at %s until the review is explicitly finished.\n' "$BASE" -printf '\nRustBox-owned upstream backend areas were NOT imported. Review them with:\n' -printf " git diff --name-status %s..%s -- backend/migrations/ backend/tests/\n" "$BASE" "$TARGET_SHA" -printf '\nFrontend changes were NOT imported. Review them with:\n' -printf ' git diff --name-status %s..%s -- frontend/\n' "$BASE" "$TARGET_SHA" -printf ' git diff %s..%s -- frontend/\n' "$BASE" "$TARGET_SHA" -printf '\nOther Oxi root changes were NOT imported. Review them with:\n' -printf " git diff %s..%s -- . ':(exclude)backend/**' ':(exclude)frontend/**'\n" "$BASE" "$TARGET_SHA" -printf '\nAfter resolving/porting and testing, run ./scripts/finish-upstream.sh.\n' diff --git a/src/AGENTS.md b/src/AGENTS.md index 11f096f..dc72ddd 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -1,7 +1,7 @@ # BACKEND KNOWLEDGE BASE ## OVERVIEW -Rust Axum application core for RustBox, forked from Oxi. It manages IMAP/SMTP connections, SQLite caching, and Tantivy search. +Rust Axum application core for RustBox (the first version was forked from Oxi; it is no longer synchronised). It manages IMAP/SMTP connections, SQLite caching, and Tantivy search. ## STRUCTURE ``` diff --git a/src/auth/login_guard.rs b/src/auth/login_guard.rs index 5938c7a..4e06a66 100644 --- a/src/auth/login_guard.rs +++ b/src/auth/login_guard.rs @@ -1,18 +1,5 @@ -//! Login brute-force protection. -//! -//! The per-IP token bucket (`tower_governor`, configured in `routes::mod`) caps the -//! raw request rate on `POST /api/auth/login`. This module adds failure-based -//! lockouts on top of it: -//! -//! * per account (case-insensitive e-mail, regardless of client IP) — stops slow, -//! distributed password guessing against one mailbox; -//! * per client IP (any account) — stops password spraying from one source. -//! -//! Consecutive lockouts of the same key double the lockout time up to -//! `max_lockout_secs`. A locked request is rejected before RustBox opens an IMAP -//! connection, so brute force does not reach (or trigger lockouts on) the mail -//! server. State is in memory and per node; see `[login_rate_limit]` in the -//! configuration for all knobs. +//! Login brute-force protection: per-account and per-IP failure lockouts with +//! exponential back-off, checked before any IMAP connection is opened. use std::collections::HashMap; use std::hash::Hash; diff --git a/src/config.rs b/src/config.rs index 163857f..4a722d7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -332,10 +332,7 @@ pub struct AppConfig { #[serde(default = "default_max_message_size_mb")] pub max_message_size_mb: usize, - /// Maximum number of requests that may hold attachment data in memory at - /// the same time while a message is sent, an IMAP draft snapshot is saved - /// or a message is attached. Peak RAM for these operations is roughly - /// `max_concurrent_message_builds` x a few times `max_message_size_mb`. + /// Maximum concurrent requests holding attachment data in memory. #[serde(default = "default_max_concurrent_message_builds")] pub max_concurrent_message_builds: usize, @@ -343,12 +340,7 @@ pub struct AppConfig { #[serde(default = "default_message_build_wait_secs")] pub message_build_wait_secs: u64, - /// How long a sent message is held back before actually reaching SMTP, so - /// the sender can cancel it ("Undo Send"). `0` disables the feature and - /// sends immediately, exactly as before this option existed. The built - /// message (including attachments) stays in memory for the whole window, - /// bounded by the same `max_concurrent_message_builds` slot it already - /// holds for an immediate send. + /// Seconds a sent message is held for "Undo Send" (0 = send immediately). #[serde(default = "default_undo_send_window_secs")] pub undo_send_window_secs: u64, diff --git a/src/db/display_preferences.rs b/src/db/display_preferences.rs index fe968dc..60faf4f 100644 --- a/src/db/display_preferences.rs +++ b/src/db/display_preferences.rs @@ -1,10 +1,7 @@ use crate::db::pool::{Connection, DbValue, ToDbValue}; use serde::{Deserialize, Deserializer, Serialize}; -/// Every predefined interface style. `default` (follow the server-wide -/// `default_interface_skin`) is accepted as a user preference in addition to -/// these, but is not itself a style. Shared with configuration validation so -/// the server default and the per-user preference can never drift apart. +/// Predefined interface styles, shared with `default_interface_skin` validation. pub const INTERFACE_SKINS: &[&str] = &[ "rustbox", "clean", diff --git a/src/db/snooze.rs b/src/db/snooze.rs index e2e3f0d..78badd1 100644 --- a/src/db/snooze.rs +++ b/src/db/snooze.rs @@ -1,8 +1,4 @@ -//! Snoozing a message hides it from its folder listing until a chosen wake -//! time. This is purely local cache state: nothing is moved on the real IMAP -//! server, so snoozing never risks the actual mailbox. The client is -//! responsible for excluding a still-sleeping message from the folder it -//! renders and for showing the "Snoozed" view built from [`list_snoozed`]. +//! Snooze: hide a message from its folder until a wake time (local cache only). use std::time::{SystemTime, UNIX_EPOCH}; diff --git a/src/message_budget.rs b/src/message_budget.rs index 469cc45..b008876 100644 --- a/src/message_budget.rs +++ b/src/message_budget.rs @@ -1,17 +1,5 @@ -//! Memory guards for assembling messages that carry attachments. -//! -//! Attachments are streamed to blob storage on upload, but sending a message or -//! saving an IMAP draft snapshot has to load every attachment into memory, and -//! the MIME builder then keeps several copies (raw bytes, base64 body, final -//! RFC 5322 text, the copy appended to the Sent folder). Two guards keep that -//! bounded: -//! -//! * [`ensure_attachments_fit`] rejects an attachment set that cannot fit -//! `max_message_size_mb` *before* any blob is read, using the sizes already -//! stored in the database (and while an upload is still streaming). -//! * [`MessageBuildLimiter`] caps how many requests may hold attachment data in -//! memory at the same time (`max_concurrent_message_builds`). Excess requests -//! wait up to `message_build_wait_secs` and then get `503`. +//! Memory guards for building messages with attachments: size check before +//! loading blobs and a limit on concurrent builds. use std::sync::Arc; use std::time::Duration; diff --git a/src/routes/auth.rs b/src/routes/auth.rs index f14bae7..009b531 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -169,12 +169,7 @@ fn peer_ip(extensions: &axum::http::Extensions) -> IpAddr { .unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)) } -/// Whether issued/cleared session cookies carry the `Secure` attribute. -/// -/// `cookie_secure = "auto"` (default) marks cookies `Secure` only when the -/// browser actually reached RustBox over HTTPS, so plain-HTTP installations -/// keep working: a `Secure` cookie sent over HTTP is discarded by browsers and -/// the user could never stay logged in. +/// Whether session cookies get `Secure` (`auto`: only when reached over HTTPS). pub(crate) fn cookie_secure( config: &AppConfig, extensions: &axum::http::Extensions, @@ -187,11 +182,7 @@ pub(crate) fn cookie_secure( } } -/// RustBox itself serves plain HTTP, so HTTPS can only be terminated in front -/// of it. A scheme reported by a trusted reverse proxy (`X-Forwarded-Proto`, -/// `Forwarded: proto=`, `X-Forwarded-Ssl`) wins; otherwise the scheme of the -/// browser-supplied `Origin`/`Referer` is used. A forged header can only change -/// the cookies issued to the client that forged it. +/// Request scheme: trusted proxy headers first, then the browser `Origin`/`Referer`. fn request_is_https( config: &AppConfig, extensions: &axum::http::Extensions, diff --git a/src/routes/drafts.rs b/src/routes/drafts.rs index ee4c51a..b74352e 100644 --- a/src/routes/drafts.rs +++ b/src/routes/drafts.rs @@ -846,10 +846,7 @@ fn build_draft_rfc822( } } - // Lettre requires at least one envelope destination even when merely serializing - // an RFC822 message. Drafts, however, are valid before the user chooses any - // recipient. Force a private build-only envelope in that case; it is not part - // of the formatted RFC822 headers and therefore does not alter the saved draft. + // Lettre needs an envelope recipient even to serialise; drafts may have none. if valid_recipients == 0 { let envelope = Envelope::new(Some(from_address.clone()), vec![from_address]) .map_err(|e| e.to_string())?; diff --git a/src/routes/filters.rs b/src/routes/filters.rs index bd3d15f..c28fc9c 100644 --- a/src/routes/filters.rs +++ b/src/routes/filters.rs @@ -1,22 +1,8 @@ -//! "Apply now" for mail filters: find the messages already in a folder that a -//! visual filter rule matches, so the client can apply the rule's actions to -//! existing mail (Sieve itself only ever runs on newly delivered messages). +//! "Apply now" for filters: find existing messages a rule matches. //! -//! Matching runs on the IMAP server with `UID SEARCH`, so it covers the whole -//! folder regardless of what RustBox has cached. IMAP SEARCH only does -//! case-insensitive substring matching, though, while Sieve rules can also use -//! `:is` / `:matches`, sizes, attachments and non-ASCII values. Every rule is -//! therefore translated into two searches: -//! -//! * an *upper bound* — a superset of the real matches, and -//! * a *lower bound* — a subset that is certainly correct. -//! -//! Exact leaves contribute the same criteria to both bounds; inexact leaves -//! widen the upper bound to `ALL` (or a substring) and shrink the lower bound -//! to `NOT ALL`; `NOT` swaps the bounds. Messages in the lower bound match. -//! Messages only in the upper bound are decided exactly from the local message -//! cache when it holds what the rule needs; anything still undecided is -//! reported as `uncertain` and left alone. +//! The rule becomes two IMAP `UID SEARCH` criteria: an upper bound (superset) +//! and a lower bound (certain matches). Candidates in between are decided from +//! the local cache; undecidable ones are reported as `uncertain`. use std::collections::{HashMap, HashSet}; use std::sync::Arc; @@ -260,11 +246,8 @@ fn bounds_for_test(test: &RuleTest) -> Bounds { let Some(literal) = search_literal(test) else { return inexact(ALL.to_string()); }; - // IMAP SEARCH does case-insensitive substring matching on the whole - // header text. That is exact for Sieve `header :contains` tests; - // `:is` / `:matches` and Sieve `address` tests (which ignore the - // display name) are only a superset and are narrowed afterwards - // from the cache. + // IMAP SEARCH is a case-insensitive substring match: exact for `:contains` + // header tests, a superset for `:is`, `:matches` and address tests. let substring = matches!(test.match_kind, MatchKind::Contains); let q = quote(&literal); match test.kind.as_str() { diff --git a/src/routes/messages/mod.rs b/src/routes/messages/mod.rs index 63f3d4a..e0a155b 100644 --- a/src/routes/messages/mod.rs +++ b/src/routes/messages/mod.rs @@ -1830,11 +1830,8 @@ pub async fn delete_message_handler( Ok(Json(serde_json::json!({ "status": "ok" })).into_response()) } -/// Snoozing is local cache state only: nothing moves on the real IMAP server. -/// `wake_at` and `snooze_for_secs` are mutually exclusive ways to say when -/// the message should reappear; the client is responsible for hiding a -/// still-sleeping message from the folder it renders (see -/// [`list_sleeping_uids`]) and for showing it again once `wake_at` passes. +/// Snooze a message (local cache only). `wake_at` and `snooze_for_secs` are +/// alternatives. pub async fn snooze_message_handler( Extension(session): Extension, Extension(config): Extension>, @@ -1893,10 +1890,7 @@ pub async fn list_snoozed_handler( Ok(Json(serde_json::json!({ "messages": snoozed })).into_response()) } -/// The UIDs in `folder` that are still snoozed as of now, so the client can -/// hide those rows from the folder it is rendering. No IMAP or background -/// work is involved; a message simply stops being returned here once its -/// wake time passes. +/// UIDs in `folder` that are still snoozed. pub async fn list_sleeping_uids( Extension(session): Extension, Extension(config): Extension>, diff --git a/src/routes/send.rs b/src/routes/send.rs index 335316b..edaaa80 100644 --- a/src/routes/send.rs +++ b/src/routes/send.rs @@ -255,10 +255,7 @@ fn validate_mailbox(label: &str, value: &str) -> Result<(), AppError> { pub struct SendResponse { pub status: String, pub message_id: String, - /// Set only when the message was staged under "Undo Send" instead of - /// being dispatched immediately. Cancel it with - /// `DELETE /messages/send/{pending_send_id}` or poll - /// `GET /messages/send/{pending_send_id}` for the final result. + /// Set when the message is held for "Undo Send". #[serde(skip_serializing_if = "Option::is_none")] pub pending_send_id: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -597,12 +594,7 @@ pub async fn send_message_handler( .into_response()); } - // Undo Send: stage the already-built message and let the client cancel - // it for `undo_window` seconds before anything is actually dispatched. - // The MessageBuildLimiter permit (if any) is moved into the delayed task - // below and held for the whole window, exactly as it is already held - // through an immediate send; this is what bounds how many undo-pending - // sends can hold attachment data in memory at once. + // Undo Send: hold the built message; the build permit is kept for the window. let pending_id = undo_registry.stage(); let draft_id = req.draft_id.clone(); let dispatch_config = config.clone(); diff --git a/src/routes/tests.rs b/src/routes/tests.rs index 2f934d1..a38e095 100644 --- a/src/routes/tests.rs +++ b/src/routes/tests.rs @@ -86,11 +86,7 @@ fn test_config_with_smtp(_static_dir: &str, data_dir: &str) -> Arc { sieve_timeout_secs: 10, trusted_proxies: None, outgoing_headers: Vec::new(), - // Existing send tests assert on the immediate, synchronous send - // outcome (status/body), not the "Undo Send" staged-dispatch flow; - // keep that behavior disabled here so they keep testing what they - // were written to test. undo_send.rs/routes/send.rs have their own - // dedicated tests for the staged-dispatch path. + // Undo Send has its own tests; keep sends immediate here. undo_send_window_secs: 0, ..AppConfig::default() }) diff --git a/src/ui.rs b/src/ui.rs index 430030a..f95e510 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -13,7 +13,6 @@ const THEME_BOOTSTRAP_JS: &[u8] = include_bytes!("web/theme-bootstrap.js"); const ICONS: &[u8] = include_bytes!("web/icons.svg"); const I18N_EN: &[u8] = include_bytes!("web/i18n/en.json"); const I18N_PL: &[u8] = include_bytes!("web/i18n/pl.json"); -const OXI_BASE: &str = include_str!("../UPSTREAM_BASE"); const ASSET_ID: &str = env!("RUSTBOX_ASSET_ID"); const FAVICON: &str = r##""##; @@ -126,8 +125,7 @@ pub async fn serve_ui(uri: Uri, Extension(config): Extension>) -> .replace( "__RUSTBOX_MAX_MESSAGE_MB__", &config.max_message_size_mb.to_string(), - ) - .replace("__RUSTBOX_OXI_BASE__", OXI_BASE.trim()); + ); ( StatusCode::OK, [ diff --git a/src/undo_send.rs b/src/undo_send.rs index 08c5639..221eef1 100644 --- a/src/undo_send.rs +++ b/src/undo_send.rs @@ -1,18 +1,5 @@ -//! In-memory staging for "Undo Send". -//! -//! When `undo_send_window_secs > 0`, a fully built outgoing message is held -//! in memory for that many seconds before it is actually dispatched to SMTP, -//! so the sender can cancel it. Nothing here is persisted: a server restart -//! during the window loses any message still staged, the same way a message -//! already queued by any mail server can be lost if the process is killed -//! mid-flight. The window is meant to be short (seconds, not minutes) for -//! exactly this reason. -//! -//! State transitions are `Pending -> Cancelled` (a user clicks "Undo") or -//! `Pending -> Dispatching -> Sent | Failed` (the window elapses). Both the -//! cancel handler and the delayed dispatch task race to leave `Pending`, but -//! only one of them can win because the transition happens under a single -//! mutex: whichever reaches it first decides the outcome. +//! In-memory staging for "Undo Send" (not persisted). A send is either +//! cancelled or dispatched; the transition happens under one mutex. use std::collections::HashMap; use std::sync::Mutex; @@ -94,10 +81,7 @@ impl UndoSendRegistry { } } - /// Called by the delayed dispatch task once its wait is over, right - /// before it would contact SMTP. Returns `true` if this call is the one - /// that claimed the send (i.e. it was not cancelled in the meantime); - /// the caller must then actually dispatch and record the result. + /// Claims the send for dispatch; `false` if it was cancelled. pub fn begin_dispatch(&self, id: &str) -> bool { let mut entries = self.entries.lock().expect("undo-send registry lock"); match entries.get_mut(id) { diff --git a/src/web/css/00-tokens.css b/src/web/css/00-tokens.css index 47a94aa..d599981 100644 --- a/src/web/css/00-tokens.css +++ b/src/web/css/00-tokens.css @@ -127,8 +127,7 @@ html[data-font-family="mono"] { --font-sans: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; } -/* Not derived from var(--line): a custom property referring to itself is a - cycle, which made --line empty (no borders) wherever no palette overrode it. */ +/* Not var(--line) itself: a self-reference is invalid CSS. */ html[data-contrast="soft"] { --line: color-mix(in srgb, var(--text) 11%, var(--panel)); --line-strong: color-mix(in srgb, var(--text) 20%, var(--panel)); diff --git a/src/web/css/30-mail.css b/src/web/css/30-mail.css index cb8da34..e4be221 100644 --- a/src/web/css/30-mail.css +++ b/src/web/css/30-mail.css @@ -1416,8 +1416,7 @@ body.reader-fullscreen-open { } } -/* Optional wide-pane single-line message rows. The preference is on by default, - but the layout only activates once the message pane has enough horizontal room. */ +/* 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); diff --git a/src/web/css/50-settings.css b/src/web/css/50-settings.css index e995857..bc81777 100644 --- a/src/web/css/50-settings.css +++ b/src/web/css/50-settings.css @@ -192,9 +192,74 @@ color: var(--muted); } -.about-list code { - font: 10px var(--font-mono); - overflow-wrap: anywhere; +.about-list a, +.about-card a { + color: var(--accent); +} + +.about-hero { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 16px; +} + +.about-hero h2 { + margin: 0 0 2px; +} + +.about-hero p { + margin: 0; +} + +.about-logo { + flex: none; + width: 44px; + height: 44px; + padding: 9px; + border-radius: 12px; + background: color-mix(in srgb, var(--accent) 16%, var(--panel)); + color: var(--accent); + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} + +.about-version { + margin-left: 6px; + padding: 2px 8px; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--muted); + font-size: 11px; + font-weight: 600; + vertical-align: middle; +} + +.about-card { + display: grid; + gap: 6px; + margin-top: 16px; + padding: 12px 14px; + border: 1px solid var(--line); + border-radius: var(--radius-panel); + background: var(--panel-2); +} + +.about-card p { + margin: 0; + line-height: 1.5; +} + +.about-origin { + margin: 14px 0 0; +} + +.about-copyright { + margin: 4px 0 0; + font-size: 11px; } .toast-stack { @@ -2635,8 +2700,7 @@ kbd { } -/* 0.2.24: theme schedule, account colour, contrast hint, list skeleton and - a short cross-fade when the theme or interface style changes. */ +/* Theme schedule, account colour, contrast hint, list skeleton, theme cross-fade. */ .theme-schedule-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -2684,8 +2748,7 @@ kbd { grid-column: 1 / -1; margin: 0; padding: 6px 9px; - /* Fixed colours on purpose: the warning must stay readable precisely when - the user's own text/panel colours are not. */ + /* Fixed colours: must stay readable with any custom palette. */ border: 1px solid #d9a93a; border-radius: var(--radius-control); background: #fff4d6; diff --git a/src/web/css/56-interface-styles.css b/src/web/css/56-interface-styles.css index 73413ac..55700db 100644 --- a/src/web/css/56-interface-styles.css +++ b/src/web/css/56-interface-styles.css @@ -1096,7 +1096,7 @@ html[data-interface-skin="notion"][data-theme="dark"] { } -/* 0.2.23 predefined styles: light and dark palettes. */ +/* Editor and atmospheric styles: light and dark palettes. */ html[data-interface-skin="dracula"][data-theme="light"] { color-scheme: light; --bg: #f4f1e6; @@ -1855,7 +1855,7 @@ html[data-interface-skin="retro"][data-theme="dark"] { -/* 0.2.24 predefined styles: light and dark palettes. */ +/* Retro styles: light and dark palettes. */ html[data-interface-skin="winxp"][data-theme="light"] { color-scheme: light; --bg: #ece9d8; diff --git a/src/web/css/57-skin-tokens.css b/src/web/css/57-skin-tokens.css index 7dec710..e59e26e 100644 --- a/src/web/css/57-skin-tokens.css +++ b/src/web/css/57-skin-tokens.css @@ -1,17 +1,5 @@ -/* - * RustBox interface-style standard (0.2.23). - * - * Every predefined style ships three layers: - * 1. light and dark palettes (56-interface-styles.css / 55-roundcube-skins.css); - * 2. a fixed layout profile (interfaceStyleProfiles in 20-preferences-auth.js); - * 3. the structural tokens below, consumed only when the user selects - * "Immersive" style depth (58-skin-immersive.css). - * - * Tokens here are always flat. Gradients live in 59-skin-gradients.css and - * only apply with the separate Appearance switch (skin_gradients). - * - * Generated from one table; keep every style listed here. - */ +/* Structural tokens per style, used at "Immersive" depth (58-skin-immersive.css). + Palettes: 55/56; layout profiles: 20-preferences-auth.js; gradients: 59. */ :root { --skin-font: var(--font-sans); diff --git a/src/web/css/58-skin-immersive.css b/src/web/css/58-skin-immersive.css index 67a8001..ba2d7ce 100644 --- a/src/web/css/58-skin-immersive.css +++ b/src/web/css/58-skin-immersive.css @@ -1,12 +1,4 @@ -/* - * "Immersive" style depth: applies the structural tokens from 57-skin-tokens.css. - * - * Every rule is scoped to :root[data-skin-depth="immersive"][data-theme] so it - * outranks the per-style chrome rules (html[data-interface-skin=…][data-theme=…] …) - * without !important, and has no effect at the "colors" or "standard" depth. - * Inline custom properties written by applyPreferences() live on , so the - * shared radius/font variables are re-pointed on where they inherit down. - */ +/* "Immersive" depth: applies the tokens from 57-skin-tokens.css. */ :root[data-skin-depth="immersive"][data-theme] body { --font-sans: var(--skin-font); @@ -100,8 +92,7 @@ backdrop-filter: var(--skin-glass); } -/* Floating panes only where the three-pane desktop layout is shown; the - phone/tablet drawer layout in 60-responsive.css stays untouched. */ +/* Floating panes only in the desktop three-pane layout. */ @media (min-width: 821px) { :root[data-skin-depth="immersive"][data-theme] .mail-shell { gap: var(--skin-shell-gap); diff --git a/src/web/css/59-skin-gradients.css b/src/web/css/59-skin-gradients.css index 0999195..e2517aa 100644 --- a/src/web/css/59-skin-gradients.css +++ b/src/web/css/59-skin-gradients.css @@ -1,11 +1,4 @@ -/* - * Style gradients (0.2.24): generated from one table. - * - * Active only with Appearance -> "Style gradients" on, at every style depth, - * for every predefined style except the RustBox custom style. Gradients are - * background-image layers over the colours chosen by the palette and depth; - * the defaults are translucent overlays, signature styles override them. - */ +/* Style gradients: only with the "Style gradients" switch on; not for the RustBox style. */ :root[data-skin-gradients="true"] { --grad-backdrop: radial-gradient(1100px 560px at 92% -12%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 62%); diff --git a/src/web/i18n/pl.json b/src/web/i18n/pl.json index eb5014d..305bd2f 100644 --- a/src/web/i18n/pl.json +++ b/src/web/i18n/pl.json @@ -350,11 +350,6 @@ "Active sessions": "Aktywne sesje", "Live RustBox sessions for this account, including other browsers while the server is running.": "Aktywne sesje RustBox dla tego konta, także w innych przeglądarkach, gdy serwer działa.", "Revoke other sessions": "Unieważnij pozostałe sesje", - "Webmail based on the Oxi codebase, with a RustBox-owned interface compiled into the Rust executable.": "Webmail oparty na kodzie Oxi, z interfejsem RustBox kompilowanym bezpośrednio do programu Rust.", - "Upstream": "Projekt bazowy", - "Oxi base": "Baza Oxi", - "Frontend": "Frontend", - "HTML, CSS and vanilla JavaScript embedded at Cargo build time": "HTML, CSS i czysty JavaScript osadzane podczas kompilacji Cargo", "Author": "Autor", "Keyboard shortcuts": "Skróty klawiaturowe", "Identity & signature": "Tożsamość i podpis", @@ -788,7 +783,15 @@ "because the copied colours belong to it.": "bo skopiowane kolory należą do niego.", "Shapes, fonts, app bar and gradients specific to the style are not part of the copy.": "Kształty, czcionki, pasek aplikacji i gradienty charakterystyczne dla stylu nie wchodzą do kopii.", "Create a custom copy?": "Utworzyć własną kopię?", - "Create custom copy": "Utwórz własną kopię" + "Create custom copy": "Utwórz własną kopię", + "Fast, self-hosted webmail written in Rust.": "Szybki webmail do samodzielnego hostowania, napisany w Rust.", + "License": "Licencja", + "Technology": "Technologia", + "GNU AGPL v3 or commercial license": "GNU AGPL v3 lub licencja komercyjna", + "Rust server with an embedded HTML, CSS and JavaScript interface": "Serwer w Rust z wbudowanym interfejsem HTML, CSS i JavaScript", + "RustBox is free software under the GNU Affero General Public License v3: you may use, modify and share it, also in a company. If you run a modified version as a network service, you must offer its source code to its users.": "RustBox jest wolnym oprogramowaniem na licencji GNU Affero General Public License v3: możesz go używać, zmieniać i udostępniać, także w firmie. Jeśli uruchamiasz zmodyfikowaną wersję jako usługę sieciową, musisz udostępnić jej kod źródłowy jej użytkownikom.", + "Companies that prefer different terms — for example to keep their changes private — can obtain a paid commercial license. Contacting the author is voluntary: it is never required for use that follows the AGPL.": "Firmy, które wolą inne warunki — na przykład zachować swoje zmiany dla siebie — mogą uzyskać płatną licencję komercyjną. Kontakt z autorem jest dobrowolny: nie jest wymagany przy korzystaniu zgodnym z AGPL.", + "The first version of RustBox was created as a fork of the Oxi project (MIT License).": "Pierwsza wersja RustBox powstała jako fork projektu Oxi (licencja MIT)." }, "fixed_interface_style_note": "gotowy styl interfejsu. Włącz edycję poniżej, aby dopasować jego układ i typografię.", "custom_interface_style_note": "RustBox można w pełni dostosować: kolory, odstępy, panele, typografię i zaawansowane ustawienia interfejsu.", diff --git a/src/web/index.html b/src/web/index.html index 4a984e6..e5b65c0 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -1307,21 +1307,40 @@ diff --git a/src/web/js/20-preferences-auth.js b/src/web/js/20-preferences-auth.js index 62861c4..5f5cbe4 100644 --- a/src/web/js/20-preferences-auth.js +++ b/src/web/js/20-preferences-auth.js @@ -231,9 +231,7 @@ function resolvedSkinDepth(prefs = state.prefs) { return skinDepths.includes(prefs.skinDepth) ? prefs.skinDepth : "standard"; } -// "colors" keeps only a predefined style's palette; "standard" and "immersive" -// also apply its fixed layout profile ("immersive" adds the structural tokens -// from 57-skin-tokens.css on top, purely in CSS). +// Style depth: "colors" = palette only; "standard" adds the layout profile; "immersive" adds the structural tokens. function layoutInterfaceStyle(prefs = state.prefs) { return resolvedSkinDepth(prefs) === "colors" ? null : activeInterfaceStyle(prefs); } @@ -310,9 +308,7 @@ function applyPreferences() { const fixedStyle = layoutInterfaceStyle(p); const skinDepth = resolvedSkinDepth(p); const previousLook = `${root.dataset.theme}|${root.dataset.interfaceSkin}|${root.dataset.skinDepth}|${root.dataset.skinGradients}`; - // A predefined style fixes the layout unless editing is allowed; then the - // user's own values (seeded from the style when editing was enabled) win, - // while the style's palette, tokens and gradients stay in place. + // The style fixes the layout unless editing is allowed. const visual = fixedStyle && !p.presetScaleUnlocked ? { ...p, ...fixedStyle.preferences } : p; @@ -487,9 +483,7 @@ function contrastRatio(first, second) { return (a + 0.05) / (b + 0.05); } -// Warns while the user's own colours make text hard to read (WCAG AA, 4.5:1 -// for text, 3:1 for the accent against panels). Predefined palettes are -// checked at release time, so the hint is only shown for custom colours. +// Live WCAG contrast hint for custom colours. function updateContrastWarning(usingPredefinedPalette) { const warning = $("#pref-contrast-warning"); if (!warning) return; @@ -535,6 +529,7 @@ async function loadPreferences() { catch (error) { console.warn("Could not load display preferences", error); } applyPreferences(); await loadTranslations(state.prefs.language); + applyPreferences(); } async function loadMailPreferences() { @@ -724,9 +719,7 @@ function schedulePromotedAppearanceSave() { const presetLayoutPreferenceKeys = [...presetEditablePreferenceKeys].filter((key) => !presetColorPreferenceKeys.has(key)); -// "Allow editing predefined style": seed the user's own layout values from the -// style, so editing starts from exactly what is on screen and never mixes in -// values saved earlier for another style. +// Seeds the user's layout from the style so editing starts from what is on screen. async function setPresetEditing(enabled) { const layoutStyle = layoutInterfaceStyle(state.prefs); const payload = { preset_scale_unlocked: enabled }; @@ -746,10 +739,7 @@ async function setPresetEditing(enabled) { } } -// Colours of a predefined style come from its light and dark palettes and -// cannot be overridden one by one without mixing two palettes. Changing one -// therefore turns the style into a RustBox custom copy of the palette that is -// on screen now, pinned to the current light/dark mode so it stays coherent. +// A colour change on a predefined style creates a RustBox custom copy of the current palette. async function promotePresetToCustom(key, value) { const paletteStyle = activeInterfaceStyle(state.prefs); if (!paletteStyle || !presetColorPreferenceKeys.has(key) || !state.prefs.presetScaleUnlocked) return false; @@ -815,7 +805,7 @@ function savePreference(key, value) { } if (["accentColor", "backgroundColor", "panelColor", "textColor"].includes(key)) state.prefs.colorPreset = "custom"; applyPreferences(); - if (key === "language") loadTranslations(value); + if (key === "language") loadTranslations(value).then(applyPreferences); const previousTimer = preferenceTimers.get(key); if (previousTimer) clearTimeout(previousTimer); const timer = setTimeout(async () => { diff --git a/src/web/js/30-folders-tags.js b/src/web/js/30-folders-tags.js index ebb8b04..8794359 100644 --- a/src/web/js/30-folders-tags.js +++ b/src/web/js/30-folders-tags.js @@ -11,12 +11,31 @@ function folderIconName(folder) { return "folder"; } +function folderPathParts(folder) { + const delimiter = folder.delimiter || "/"; + return delimiter ? folder.name.split(delimiter) : [folder.name]; +} + +// Tree order: children under their parent, INBOX first, siblings alphabetical. +function compareFolderPaths(a, b) { + const left = folderPathParts(a); + const right = folderPathParts(b); + for (let index = 0; index < Math.max(left.length, right.length); index += 1) { + if (left[index] === undefined) return -1; + if (right[index] === undefined) return 1; + if (left[index] === right[index]) continue; + if (index === 0) { + const leftInbox = left[0].toUpperCase() === "INBOX"; + const rightInbox = right[0].toUpperCase() === "INBOX"; + if (leftInbox !== rightInbox) return leftInbox ? -1 : 1; + } + return left[index].localeCompare(right[index]) || (left[index] < right[index] ? -1 : 1); + } + return 0; +} + function orderedFolders() { - return [...state.folders].sort((a, b) => { - if (a.name.toUpperCase() === "INBOX") return -1; - if (b.name.toUpperCase() === "INBOX") return 1; - return a.name.localeCompare(b.name); - }); + return [...state.folders].sort(compareFolderPaths); } function folderDepth(folder) { diff --git a/src/web/js/40-messages-reader.js b/src/web/js/40-messages-reader.js index e6b9aef..f3988cc 100644 --- a/src/web/js/40-messages-reader.js +++ b/src/web/js/40-messages-reader.js @@ -312,11 +312,7 @@ async function quickUpdateMessageFlag(message, flag, add, successMessage) { } } -// Hides rows for messages that are currently snoozed in this folder. This is -// a purely visual, best-effort filter on top of the already-rendered list: -// it never changes state.messages/state.total, so pagination/counts/caching -// above are completely unaffected. A message simply stops being hidden once -// its wake time passes and the folder is reloaded or reopened. +// Hides rows of snoozed messages (visual only; counts and paging unchanged). async function applySnoozeHiding() { if (state.searchContext) return; const folder = state.folder; @@ -508,9 +504,7 @@ function readerFullscreenUrl(detail = state.currentDetail) { return new URL(location.href); } -// Gmail-style modifier clicks on a message row, active only outside selection -// mode (inside selection mode the same modifiers keep doing range/toggle -// selection, see the row click handler below). +// Ctrl/Cmd/Shift-click on a row outside selection mode. function openMessageInNewTab(folder, uid) { window.open(messageDeepLinkUrl(folder, uid), "_blank", "noopener"); } diff --git a/src/web/js/50-compose.js b/src/web/js/50-compose.js index 1e60359..c51d788 100644 --- a/src/web/js/50-compose.js +++ b/src/web/js/50-compose.js @@ -1788,10 +1788,7 @@ function buildReferences(detail) { return values.length ? values.join(" ") : null; } -// "Undo Send": the server held the just-sent message back for a few seconds -// instead of dispatching it immediately. Show a toast with an Undo action -// for the whole window, then poll once for the final result so a send that -// actually failed in the background is still reported. +// Undo Send: offer Undo during the hold window, then report the final result. function notifyUndoSendStaged(pendingId, windowSecs) { const seconds = Math.max(0, windowSecs); toast("Sending\u2026", "info", seconds * 1000 || 4000, { diff --git a/src/web/js/60-mail-actions-search.js b/src/web/js/60-mail-actions-search.js index 948485f..da6916f 100644 --- a/src/web/js/60-mail-actions-search.js +++ b/src/web/js/60-mail-actions-search.js @@ -181,9 +181,7 @@ async function copyCurrentTo(targetFolder) { } catch (error) { toast(error.message, "error"); } } -// Snoozing hides a message from its folder listing until a chosen wake time. -// It is purely local client/server cache state: nothing moves on the real -// IMAP server, so a snooze never risks the actual mailbox. +// Snooze hides a message until its wake time (local only, nothing moves on IMAP). function snoozePresetOptions() { const now = new Date(); const laterToday = new Date(now.getTime() + 3 * 60 * 60 * 1000); diff --git a/src/web/js/80-settings-realtime.js b/src/web/js/80-settings-realtime.js index 08f0f93..d827116 100644 --- a/src/web/js/80-settings-realtime.js +++ b/src/web/js/80-settings-realtime.js @@ -409,9 +409,7 @@ function calendarReminderStorageKey(event, minutes) { return `rustbox.calendarReminder.${state.accountId || "account"}.${event.id}.${event.start_time}.${minutes}`; } -// Timed events: start minus the reminder. All-day events: the reminder counts -// from the start of the working day (calendar settings, default 09:00) of the -// event's date, so "1 day before" fires the previous morning, not at midnight. +// All-day events are reminded relative to the start of the working day. function calendarReminderTrigger(event, minutes) { const offset = Math.max(0, Number(minutes) || 0) * 60000; if (event.all_day) { @@ -476,10 +474,7 @@ function isDuplicateRealtimeEvent(payload) { const now = Date.now(); for (const [key, seenAt] of state.realtimeSeenEvents) if (now - seenAt > 10000) state.realtimeSeenEvents.delete(key); - // Server websocket envelopes carry a unique eventId. When present it is the - // only safe deduplication key: state can legitimately cycle A -> B -> A in - // under a second (for example unread 1 -> 0 -> 1). Fingerprint-based - // deduplication would incorrectly drop the second A and leave stale UI. + // Deduplicate by eventId when present: the same state can legitimately repeat. if (payload.eventId) { const idKey = `id:${payload.eventId}`; const duplicate = state.realtimeSeenEvents.has(idKey); diff --git a/src/web/js/91-sieve-apply.js b/src/web/js/91-sieve-apply.js index 416b0ae..f974c41 100644 --- a/src/web/js/91-sieve-apply.js +++ b/src/web/js/91-sieve-apply.js @@ -1,6 +1,4 @@ -// Filter conveniences on top of the visual Sieve editor: templates, creating -// missing target folders, and "Apply now" — running a rule's actions on mail -// that is already in a folder (Sieve itself only sees newly delivered mail). +// Filter templates, missing-folder creation and "Apply now" for existing mail. function fmtText(template, values = {}) { return trText(template).replace(/\{(\w+)\}/g, (_, key) => String(values[key] ?? "")); diff --git a/src/web/js/95-bindings-boot.js b/src/web/js/95-bindings-boot.js index 92fce4b..4eaa39b 100644 --- a/src/web/js/95-bindings-boot.js +++ b/src/web/js/95-bindings-boot.js @@ -507,9 +507,7 @@ function bindEvents() { $$("[data-signature-command]").forEach((button) => button.addEventListener("click", () => { const range = selectedSignatureRange(); if (!range) { - // execCommand on a collapsed caret enables a sticky typing style (for - // example every subsequently typed character becomes bold). Signature - // formatting is intentionally selection-only. + // Formatting applies to the selection only (a collapsed caret would make it sticky). signatureEditor.focus(); return; }