Compare commits

..
48 Commits
Author SHA1 Message Date
Mateusz Gruszczyński 2932578424 v0.2.31
RustBox CI / rustbox (push) Waiting to run
2026-11-08 16:42:41 +01:00
Mateusz Gruszczyński a3ac2a68fe v0.2.30
RustBox CI / rustbox (push) Canceled after 0s
2026-11-08 07:32:12 +01:00
Mateusz Gruszczyński 1b12899ff6 0.2.29
RustBox CI / rustbox (push) Canceled after 0s
2026-11-07 10:27:26 +01:00
Mateusz Gruszczyński 065fc87306 v0.2.28_fix
RustBox CI / rustbox (push) Canceled after 0s
2026-11-06 13:08:12 +01:00
Mateusz Gruszczyński c160f25e4c v0.2.28
RustBox CI / rustbox (push) Canceled after 0s
2026-11-06 12:30:57 +01:00
Mateusz Gruszczyński 20d78ee7be v0.2.27
RustBox CI / rustbox (push) Canceled after 0s
2026-11-06 11:15:43 +01:00
Mateusz Gruszczyński 53f4812adc v0.2.26
RustBox CI / rustbox (push) Canceled after 0s
2026-11-05 14:03:43 +01:00
Mateusz Gruszczyński 99ee3385ac v0.2.25
RustBox CI / rustbox (push) Canceled after 0s
2026-11-05 08:33:13 +01:00
Mateusz Gruszczyński 648556f762 v0.2.25
RustBox CI / rustbox (push) Canceled after 0s
2026-11-04 23:59:38 +01:00
Mateusz Gruszczyński de4d70970c 0.2.24
RustBox CI / rustbox (push) Canceled after 0s
2026-11-04 23:34:09 +01:00
Mateusz Gruszczyński beae94e932 v0.2.23
RustBox CI / rustbox (push) Canceled after 0s
2026-11-04 20:44:31 +01:00
Mateusz Gruszczyński 3b9110c8c8 v0.2.22
RustBox CI / rustbox (push) Canceled after 0s
2026-11-04 14:59:03 +01:00
Mateusz Gruszczyński 6e74b33f03 v0.2.20_hf1
RustBox CI / rustbox (push) Canceled after 0s
2026-11-03 22:58:56 +01:00
Mateusz Gruszczyński 2486856799 v0.2.20
RustBox CI / rustbox (push) Canceled after 0s
2026-11-03 16:07:58 +01:00
Mateusz Gruszczyński 9003a50eb4 v0.2.18
RustBox CI / rustbox (push) Canceled after 0s
2026-11-03 13:02:34 +01:00
Mateusz Gruszczyński 1309358b68 v0.2.18
RustBox CI / rustbox (push) Canceled after 0s
2026-11-03 12:11:13 +01:00
Mateusz Gruszczyński 1bb398acd1 v0.2.17
RustBox CI / rustbox (push) Canceled after 0s
2026-11-03 10:35:05 +01:00
Mateusz Gruszczyński 2ac1566eca v0.2.16
RustBox CI / rustbox (push) Canceled after 0s
2026-11-02 23:46:16 +01:00
Mateusz Gruszczyński dbe7103687 v0.2.16
RustBox CI / rustbox (push) Canceled after 0s
2026-11-02 23:32:23 +01:00
Mateusz Gruszczyński d91212d24c v0.2.15
RustBox CI / rustbox (push) Canceled after 0s
2026-11-02 23:13:21 +01:00
Mateusz Gruszczyński fb9eab87eb v0.2.15
RustBox CI / rustbox (push) Canceled after 0s
2026-11-02 16:22:13 +01:00
Mateusz Gruszczyński edda5fc9e4 v0.2.14
RustBox CI / rustbox (push) Canceled after 0s
2026-11-02 15:34:49 +01:00
Mateusz Gruszczyński 12e0c99850 v0.2.13
RustBox CI / rustbox (push) Canceled after 0s
2026-11-02 14:23:13 +01:00
Mateusz Gruszczyński 83440f20e2 v0.2.12
RustBox CI / rustbox (push) Canceled after 0s
2026-11-02 13:54:18 +01:00
Mateusz Gruszczyński 5dc9db0b1c v0.2.11
RustBox CI / rustbox (push) Canceled after 0s
2026-11-02 10:39:00 +01:00
Mateusz Gruszczyński 7f643c8509 v0.2.11
RustBox CI / rustbox (push) Canceled after 0s
2026-11-02 09:49:12 +01:00
Mateusz Gruszczyński db0d230d3b v0.2.10
RustBox CI / rustbox (push) Canceled after 0s
2026-11-02 09:37:35 +01:00
Mateusz Gruszczyński 02d0b13ec1 v0.2.10
RustBox CI / rustbox (push) Canceled after 0s
2026-11-01 23:52:18 +01:00
Mateusz Gruszczyński a116890e20 v0.2.9
RustBox CI / rustbox (push) Canceled after 0s
2026-11-01 21:36:04 +01:00
Mateusz Gruszczyński dacdf9c27d v0.2.8
RustBox CI / rustbox (push) Canceled after 0s
2026-11-01 20:40:40 +01:00
Mateusz Gruszczyński cfc74d9db6 v0.2.6
RustBox CI / rustbox (push) Canceled after 0s
2026-11-01 15:46:10 +01:00
Mateusz Gruszczyński 47c3cd7f65 v0.2.5
RustBox CI / rustbox (push) Canceled after 0s
2026-10-31 23:53:40 +01:00
Mateusz Gruszczyński 27a53c3c6a v0.2.5
RustBox CI / rustbox (push) Canceled after 0s
2026-10-31 23:48:30 +01:00
Mateusz Gruszczyński e4aa787e06 0.2.4
RustBox CI / rustbox (push) Canceled after 0s
2026-10-31 22:36:28 +01:00
Mateusz Gruszczyński 2c98b8f925 v0.2.3
RustBox CI / rustbox (push) Canceled after 0s
2026-10-31 19:40:19 +01:00
Mateusz Gruszczyński 5b22add1f3 v0.2.2
RustBox CI / rustbox (push) Canceled after 0s
2026-10-31 16:27:41 +01:00
Mateusz Gruszczyński 95c0587c1e v0.2.1
RustBox CI / rustbox (push) Canceled after 0s
2026-10-31 15:55:47 +01:00
Mateusz Gruszczyński 0d9f3ff447 v0.2.0_docker
RustBox CI / rustbox (push) Canceled after 0s
2026-10-31 15:43:13 +01:00
Mateusz Gruszczyński 9ba6571fa6 v0.2.0_docker
RustBox CI / rustbox (push) Canceled after 0s
2026-10-31 15:33:58 +01:00
Mateusz Gruszczyński 6888cb7950 v0.2.0
RustBox CI / rustbox (push) Canceled after 0s
2026-10-31 14:42:50 +01:00
Mateusz Gruszczyński dc57aa7089 v0.2.0 2026-10-31 14:36:07 +01:00
Mateusz Gruszczyński fc81ecafd6 todo
RustBox CI / rustbox (push) Canceled after 0s
2026-10-31 09:26:44 +01:00
Mateusz Gruszczyński e3e844ef4f v0.1.2
RustBox CI / rustbox (push) Canceled after 0s
2026-10-31 09:22:42 +01:00
Mateusz Gruszczyński 698e3c4a1b v0.1.1
RustBox CI / rustbox (push) Canceled after 0s
2026-10-31 00:37:16 +01:00
gru ecc201ef50 Update config.dev.toml
RustBox CI / rustbox (push) Canceled after 0s
2026-10-30 19:17:23 +01:00
Mateusz Gruszczyński 425ea7449b v0.1.0
RustBox CI / rustbox (push) Canceled after 0s
2026-10-30 17:22:17 +01:00
Mateusz Gruszczyński 0421994fe6 v0.0.6
RustBox CI / rustbox (push) Canceled after 0s
2026-10-30 14:42:01 +01:00
Mateusz Gruszczyński 1234aae646 v0.0.6 2026-10-30 14:41:15 +01:00
264 changed files with 46865 additions and 5858 deletions

No files matched your search

+12 -4
View File
@@ -5,10 +5,10 @@ ENVIRONMENT=development
DATA_DIR=.dev/data
SESSION_TIMEOUT_HOURS=24
# Optional fixed mail hosts. Leave empty to provide them at login.
IMAP_HOST=
# Optional fixed mail hosts. Omit IMAP_HOST to use localhost; SMTP falls back to IMAP.
# IMAP_HOST=localhost
IMAP_PORT=993
SMTP_HOST=
# SMTP_HOST=localhost
SMTP_PORT=587
TLS_ENABLED=true
@@ -20,5 +20,13 @@ SIEVE_ALLOW_INSECURE_AUTH=false
SIEVE_TIMEOUT_SECS=10
TRUSTED_PROXIES=127.0.0.1,::1
RUST_LOG=info
RUST_LOG=warn,rustbox=debug,tower_http=info
# RUSTBOX_CONFIG=/etc/rustbox/rustbox.toml
# RustBox 0.2 storage/search nested overrides use double underscores.
# STORAGE__BACKEND=s3
# STORAGE__BUCKET=rustbox
# STORAGE__REGION=us-east-1
# STORAGE__ENDPOINT=https://s3.example.com
# SEARCH__BACKEND=opensearch
# For arrays such as search.urls, TOML config is recommended.
+7 -1
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@1.88.0
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
@@ -17,6 +17,12 @@ jobs:
workspaces: .
- name: Reject TypeScript sources
run: '! find . -type f \( -name "*.ts" -o -name "*.tsx" \) -print -quit | grep -q .'
- name: Install static-validator dependency
run: python3 -m pip install --disable-pip-version-check 'PyYAML>=6,<7'
- name: Static release validation
run: python3 scripts/validate-release-static.py
- name: Generate dependency lockfile
run: cargo generate-lockfile
- name: Format
run: cargo fmt --all -- --check
- name: Test
+1
View File
@@ -31,3 +31,4 @@ Thumbs.db
debugging/
*.zip
__pycache__
+311 -217
View File
@@ -1,227 +1,321 @@
# RustBox changelog
### 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.
- Bumped the application/source release to `0.0.5`.
What changed in each RustBox release, written for the people who use and run it. Technical details are in the [documentation](docs/).
### 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.
- Bumped the application/source release to `0.0.4`.
## 0.2.31
### 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.
- Phones and tablets: press and hold a message in the list to open a bottom sheet with its actions (read, flag, archive, delete as large buttons, then reply, forward, labels, move, filters and the rest of the desktop menu). The hover buttons no longer cover the subject and date on touch screens.
- Mark read/unread and flag from the message context menu no longer open the message first.
- Added server-wide `default_interface_skin` (`rustbox`, `gmail`, `proton`, `zoho`, `outlook`, etc.) for login and accounts using **Global default**.
## 0.2.30
- 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`.
- Optional CalDAV and CardDAV server. With `[dav] enabled = true` every account gets a calendar and an address book at `/dav/` that phones, Thunderbird, DAVx⁵, Apple Calendar/Contacts or an ActiveSync gateway such as ActivePush can sync. It uses the same events and contacts as the webmail, so a change made in either place appears in the other. Sign in with your mail address and password.
### 0.0.2
## 0.2.29
- 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.
- An open message thread now updates automatically when a new message for that thread arrives via realtime events; reopening the thread is no longer required.
### 0.0.1
## 0.2.28
- 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.
- Replying to a message you sent no longer addresses the reply back to yourself; RustBox chooses the other thread participant(s), including when aliases/identities are used.
- The mailbox toolbar now switches the message list between threaded and flat views. The compact icon-only control matches the message-selection button and changes its icon to reflect the active view. Threaded mode groups related messages by Message-ID/In-Reply-To/References, shows the thread size, and lets each thread expand or collapse; the reader follows the same persisted mailbox preference.
- Polish text decoding is more tolerant of legacy mail: RFC 2047 ISO-8859-2 subjects and common UTF-8/Windows-1252 or ISO-8859-2/Western-Latin mojibake are repaired in headers, bodies and cached message previews.
## 0.2.27
- Monochrome mode now creates a custom copy when editing a predefined interface style, just like direct colour changes.
- Wide message rows give more room to the subject and show the complete subject in a tooltip.
- Read/unread, flag and open actions no longer reserve hidden horizontal space; they appear to the left on hover.
- Shift-click message popups now open as a reader-only embedded view instead of briefly showing the full mailbox.
- Opening a message in a new window or tab now uses the same reader-only embedded view as the popup instead of the full mailbox/fullscreen layout.
- Adaptive HTML mail colours now derive light/dark mode from the actual reader background, so custom palettes stay readable even when their saved theme mode does not match their visual brightness.
- Mailbox storage is now clickable: folder properties can show IMAP-reported size in MB, and a storage dialog visualizes per-folder usage with progress bars.
## 0.2.26
- RustBox now has a license: the GNU AGPL v3, plus an optional paid commercial license for companies that prefer different terms. The About page explains both.
- 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.
## 0.2.25
- **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.
## 0.2.24
- 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.
## 0.2.23
- **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.
## 0.2.22
- **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).
## 0.2.21
- 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.
## 0.2.20
- 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.
## 0.2.19
- HTML messages use all the available height in the reader and adapt when the window or layout changes.
## 0.2.18
- 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.
## 0.2.16
- 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.
## 0.2.15
- 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.
## 0.2.14
- 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.
## 0.2.13
- Deleted attachments are always removed from storage, even after a temporary storage failure; cleanup is retried automatically.
## 0.2.12
- **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.
## 0.2.11
- Import of contacts and identities from Roundcube.
- All predefined styles can be edited.
## 0.2.10
- 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.
## 0.2.9
- 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.8
- 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.7
- Security headers are tailored to each type of response.
## 0.2.6
- 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.5
- After moving or deleting the open message, the next one opens automatically.
## 0.2.4
- Fixed S3-compatible storage over internal plain-HTTP addresses.
- Updated the Rust toolchain and Docker images (Debian 13).
## 0.2.3
- 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.2.2
- Reliability fixes for PostgreSQL, including marking messages as spam, and quieter database startup.
## 0.2.1
- 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.
## 0.2.0
- 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.
## 0.1.2
- 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.
## 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.
Generated
+1624 -730
View File
File diff suppressed because it is too large. Load diff
+21 -3
View File
@@ -1,8 +1,12 @@
[package]
name = "rustbox"
version = "0.0.5"
version = "0.2.31"
edition = "2024"
rust-version = "1.88"
rust-version = "1.99.1"
license = "AGPL-3.0-only"
authors = ["Mateusz Gruszczyński <linuxiarz@linuxiarz.pl>"]
description = "Fast, self-hosted webmail written in Rust"
repository = "https://git.linuxiarz.pl/gru/RustBox"
default-run = "rustbox"
[dependencies]
@@ -25,10 +29,10 @@ rand = "0.9"
base64 = "0.22"
sha2 = "0.10"
rusqlite = { version = "0.34", features = ["bundled"] }
refinery = { version = "0.9", features = ["rusqlite"] }
axum-extra = { version = "0.10", features = ["cookie"] }
async-trait = "0.1.92"
mail-parser = "0.11.2"
encoding_rs = "0.8"
futures = "0.3.32"
lettre = { version = "0.11", default-features = false, features = ["tokio1-native-tls", "smtp-transport", "builder"] }
uuid = { version = "1", features = ["v4"] }
@@ -41,6 +45,20 @@ rand08 = { package = "rand", version = "0.8.6" }
smallvec = "1"
sha1 = "0.10"
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "mysql", "postgres", "macros"] }
redis = { version = "0.32", default-features = false, features = ["tokio-comp"] }
aes-gcm = "0.10"
object_store = { version = "0.14.2", features = ["aws"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
bytes = "1"
# CalDAV / CardDAV server ([dav] section)
quick-xml = "0.41"
percent-encoding = "2"
chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }
chrono-tz = "0.10"
[dev-dependencies]
http-body-util = "0.1"
tempfile = "3"
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 <https://www.gnu.org/licenses/>.
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
<https://www.gnu.org/licenses/>.
+29
View File
@@ -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`.
+15 -8
View File
@@ -1,13 +1,20 @@
# RustBox attribution / notice
# Notices
RustBox 0.0.5 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.
+202 -91
View File
@@ -1,49 +1,76 @@
# RustBox 0.0.5
# RustBox 0.2.31
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
├── Cargo.lock
├── 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; optional CalDAV/CardDAV server to sync both with phones and desktop clients.
- 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:
```bash
./scripts/build-release.sh
sudo ./scripts/install-systemd.sh
sudo systemctl enable --now rustbox
```
Docker Compose examples (SQLite; PostgreSQL + S3 + OpenSearch; MySQL + NFS + OpenSearch; HA with Garage) are in [`docker/`](docker/README.md).
## Documentation
| 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
Mail endpoints are configured by the administrator, not on the login screen. Users enter only email and password.
Login brute-force protection is on by default and configured in the `[login_rate_limit]` section (or `LOGIN_RATE_LIMIT__*` environment variables): a per-IP token bucket on the login endpoint plus failed-attempt lockouts per mailbox and per client IP with exponential back-off. Locked attempts are answered with `429` and `Retry-After` without contacting the IMAP server. Counters are in memory per node. See `config.example.toml` for every option.
Session cookies get the `Secure` attribute according to `cookie_secure` (default `auto`): only when the browser reached RustBox over HTTPS, detected from a trusted proxy's `X-Forwarded-Proto`/`Forwarded`/`X-Forwarded-Ssl` header or the browser's `Origin`/`Referer`. Plain-HTTP installations keep working; set `cookie_secure = "always"` behind an HTTPS proxy that does not forward the scheme.
Edit `config.dev.toml` before the first run:
```toml
default_theme = "system" # system | dark | light
default_interface_skin = "rustbox" # rustbox | gmail | proton | zoho | outlook | fastmail | apple-mail | yahoo | hey | nord | ...
default_interface_skin = "rustbox" # rustbox | gmail | outlook | apple-mail | github | slack | dracula | catppuccin | tokyo-night | paper | terminal | aqua | material | retro | winxp | osx | ios6 | win7 | frutiger-aero | beos | ... (full list in config.example.toml)
log_level = "info" # RustBox app level; dependencies default to WARN. RUST_LOG can override the full filter.
# Recommended dev filter: warn,rustbox=debug,tower_http=info
# Avoid a global RUST_LOG=debug/info unless diagnosing a dependency: third-party
# crates may emit internal filesystem/source paths in verbose records.
# Mail hosts may be omitted: IMAP defaults to localhost:993 and SMTP to localhost:587.
# imap_host = "localhost"
# smtp_host = "localhost"
@@ -53,8 +80,24 @@ tls_enabled = true
max_attachment_size_mb = 10
# Hard limit for the complete rendered RFC822/MIME message submitted to the MTA.
max_message_size_mb = 50
# Memory guard for messages with attachments. Sending a message or saving an IMAP
# draft snapshot loads every attachment into RAM and the MIME builder keeps several
# copies (about 3-4x the message size). Attachment sets that cannot fit
# max_message_size_mb are rejected with 413 before any file is read, and at most
# max_concurrent_message_builds requests may hold attachments in memory at once;
# the others wait up to message_build_wait_secs and then get 503.
# Rough peak RAM = max_concurrent_message_builds x 4 x max_message_size_mb.
max_concurrent_message_builds = 4
message_build_wait_secs = 30
# Optional; defaults to imap_host.
# Optional features exposed by the server/UI.
sieve_enabled = true
diagnostics_enabled = true
about_enabled = true
# Adds content-type-aware browser security headers when enabled: full document policy on HTML, private no-store/CORP/anti-framing policy on /api, Referrer-Policy on HTML/CSS, and nosniff broadly.
security_headers_enabled = false
# Optional; defaults to imap_host. Ignored when sieve_enabled=false.
# sieve_host = "mail.example.com"
sieve_port = 4190
sieve_tls_mode = "starttls"
@@ -71,6 +114,117 @@ If the mail host settings are omitted, RustBox uses `localhost` with IMAP `993`
Mail-size limits are intentionally separate. `max_attachment_size_mb` applies to each raw uploaded file, while `max_message_size_mb` is checked against the final RFC822/MIME byte stream immediately before SMTP submission. The latter therefore includes message bodies, headers, MIME boundaries, base64 expansion, inline CID parts and OpenPGP transformations. The composer displays only an estimate; the server-side final-MIME check is authoritative.
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"`.
`session_backend = "database"` stores sessions in the selected SQL database. `session_backend = "redis"` moves only session persistence to Redis/Redis Sentinel. Mailbox metadata and cached message bodies remain in SQL.
`data_dir` is persistent node state only when a selected local feature actually needs it: default SQLite, implicit filesystem storage, implicit local Tantivy, or local OpenPGP keys. In cluster mode it is not required when those features are replaced with cluster-safe backends. Cluster validation is capability-based:
- SQL: shared MySQL/PostgreSQL; SQLite is rejected;
- blobs: S3-compatible storage, or `filesystem` with `storage.shared=true` on a real NFS/CephFS/GlusterFS mount;
- search: OpenSearch or `disabled`; local Tantivy is rejected;
- OpenPGP: remains disabled until a cluster-safe key backend exists;
- all nodes use the same `session_encryption_key`.
Example stateless cluster:
```toml
database_backend = "pgsql"
database_url = "postgres://rustbox:secret@db/rustbox"
session_backend = "database"
session_encryption_key = "<shared-key>"
cluster_mode = true
pgp_enabled = false
[storage]
backend = "s3"
bucket = "rustbox"
region = "eu-central-1"
# endpoint = "https://s3.example.com" # MinIO/Ceph RGW/etc.
[sync]
interval_secs = 30
deep_index_batch_size = 10
[search]
backend = "opensearch"
urls = ["https://opensearch.example.com:9200"]
required = true
# Production-safe default: do not proactively download every full message body.
deep_index = false
```
Shared POSIX is also supported without S3:
```toml
[storage]
backend = "filesystem"
path = "/mnt/rustbox-shared"
shared = true
```
Existing attachment files can be migrated to the configured storage backend without deleting the source files:
```bash
rustbox storage migrate --from filesystem:/old/data/root --to configured --dry-run
rustbox storage migrate --from filesystem:/old/data/root --to configured --verify
```
The SQL row is switched to the logical object key only after upload and size/checksum verification. Search state is rebuildable from SQL:
```bash
rustbox search reindex
```
Ready Docker examples are in `docker/`: standalone SQLite, PostgreSQL + SILO (S3-compatible) + OpenSearch, and MySQL + NFS + OpenSearch.
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
@@ -100,70 +254,27 @@ cargo test
cargo build --release
```
## UI
## Project layout
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-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.0.5 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 <oxi-commit>
```
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).
+148 -70
View File
@@ -1,75 +1,153 @@
RustBox 0.0.5 source release
RustBox 0.2.31 release manifest
Runtime/source model:
- one root Cargo package
- src/ Rust application code
- src/web/ RustBox-owned embedded browser UI
- migrations/ SQLite migrations
- no TypeScript/React/Next.js/Node.js/Bun build
- ManageSieve TLS/STARTTLS/explicit insecure transport option
- native systemd packaging
0.2.31 release changes:
- touch screens: long press on a message row opens a bottom action sheet (read/unread, flag, archive, delete quick actions plus the full desktop context menu with in-place submenus); it follows the interface style colours, shape, scale and density, closes via backdrop, drag-down, Escape and Android back, and is centred on tablets; the hover quick-action overlay is hidden on touch screens instead of covering the subject and date
- message context menu: mark read/unread and flag no longer open the message first
Oxi tracking:
- UPSTREAM_BASE stores consumed Oxi revision
- scripts/setup-upstream.sh configures upstream-oxi
- scripts/oxi-status.sh reports upstream changes
- scripts/sync-upstream.sh maps the cumulative Oxi backend delta into flat RustBox paths
- scripts/finish-upstream.sh advances UPSTREAM_BASE only after explicit upstream review/build validation
- scripts/oxi-port-commit.sh maps a selected Oxi backend commit without advancing the baseline
- Oxi frontend changes are reviewed and manually ported to src/web/
- docs/UI.md documents the browser UI/build conventions
0.2.30 release changes:
- optional built-in CalDAV / CardDAV server (`[dav] enabled = true`): one calendar and one address book per account at `<base_path>/dav/`, backed by the webmail calendar and contacts tables; PROPFIND, REPORT (calendar-query, calendar-multiget, addressbook-query, addressbook-multiget), GET/HEAD, PUT, DELETE, ETag/If-Match/If-None-Match, CTag (`getctag`), RFC 6764 `/.well-known/caldav|carddav`; HTTP Basic auth against IMAP with a short-lived login cache and the webmail brute-force guard; uploaded iCalendar/vCard objects are stored verbatim and webmail edits are patched into them property by property (migration V055); see `docs/CALDAV_CARDDAV.md`
Current 0.0.5 release additions:
- attachment left-click defaults to safe in-app preview for supported types
- attachment right-click exposes explicit Preview and Download actions
- oversized preview attempts offer direct download instead of ending with a warning
- Polish composer, reader toolbar and per-message context-menu translation pass
- grouped reader message details menu for headers/source and transfer menu for move/copy
- compact composer format selector moved to the lower-right opposite Send/Cancel
- mobile layout reliability pass for settings, source/header/calendar dialogs and narrow toolbars
- fixed Sieve visual-editor checkbox alignment and mobile single-column rule controls
- tap/keyboard expansion for full sender/recipient address details in the reader
- persistent IMAP quota progress above the sidebar IMAP/version footer
- global TOML `default_theme` for light/dark mode and `default_interface_skin` for login + account-wide default interface styling
- Gmail-, Proton-, Zoho- and Outlook-inspired interface profiles plus Minimal
- expanded Polish localization, including scoped dialog/settings text and common attributes
- JavaScript source split into functional `src/web/js/` modules and assembled by Cargo build.rs
- Interface style selector expanded to RustBox custom, Roundcube Classic/Larry/Elastic, Clean, Graphite, High contrast, Gmail-, Proton-, Zoho-, Outlook-inspired and Minimal
- legacy Compact/Balanced/Comfortable/Reading appearance cards removed
- persisted special folders and mailbox behavior preferences
- bulk IMAP actions, copy/move, folder mark-all-read/empty, message-to-folder drag/drop
- HTML/plain/Markdown composer modes, CID inline images and expanded identity/signature rules
- advanced/saved search, tags, contact groups, visual Sieve/vacation builder
- IMAP IDLE/WebSocket refresh, desktop notifications and DND
- IMAP capabilities diagnostics and multi-account browser session UI
- RFC822 .eml download/import, List-Unsubscribe handling and RFC 8058 one-click HTTPS unsubscribe
- expanded contacts with multiple e-mails/phones/addresses, vCard profile fields, duplicate merge and validated PNG/JPEG/WebP photos
- calendar Month/Week/Day/Agenda views, richer event metadata and ICS import/export
- search-index status/rebuild, IMAP reconnect diagnostics and live cross-browser session revoke UI
- 45 contiguous SQLite migrations wired sequentially; V042 adds per-user trusted senders, V043 saved responses, V044 interface skins and V045 saved-response format
- safe Undo for resolvable IMAP MOVE actions and select-all across complete cached search results
- exact-token thread matching and expanded keyboard selection/navigation shortcuts
- in-app attachment preview, clipboard CID image paste and upload progress
- separate 10 MiB per-file attachment limit and 50 MiB final RFC822/MIME message limit with pre-SMTP exact byte validation and composer size estimate
- recoverable drafts with browser emergency snapshots and throttled IMAP Drafts snapshots
- outgoing priority and MDN/read-receipt draft/send options with pre-SMTP mailbox validation
- expanded Sieve visual rules: nested three-condition grouping, two actions, managed reordering, backup/restore, active overwrite guard and optional date-scheduled vacation
- spam destination default plus per-account override, pre-pagination flagged-first sorting with per-folder overrides, and subtle flagged-row markers
- direct ManageSieve quick-filter wizard with validation/save/activation and advanced-editor handoff
- unread browser/app badge updates and a public local-storage readiness endpoint
- inline image editing with display sizing, alignment, crop/cut preview and CID cleanup
- dedicated Composing settings for format, autosave interval, default priority, MDN and attachment reminder
- corrected Settings header/content grid for short panels and scroll-safe navigation
0.2.29 release changes:
- open message thread refreshes automatically after realtime NewMessages events, without requiring the user to reopen it
- replies to own sent messages target the other participant(s), with account aliases/identities treated as own addresses
- mailbox toolbar toggles between grouped threads and individual messages using the persisted mailbox `thread_mode` preference; thread rows expand/collapse and use Message-ID/In-Reply-To/References metadata
- legacy Polish text repair covers common UTF-8/Windows-1252 and ISO-8859-2/Western-Latin mojibake in headers, bodies and cached summaries/details; exact ISO-8859-2 folded-subject coverage added
Validation note:
- JavaScript, JSON, TOML, SVG, shell syntax, UI selector/icon integrity and SQLite migrations were validated in the release environment.
- A full cargo build/test was not executed in this environment because cargo/rustc are not installed; run ./dev.sh or cargo test on a Rust toolchain host before deployment.
0.2.27 release changes:
- predefined styles promote to a custom copy when monochrome mode is changed
- wide message rows allocate more space to subjects and expose the complete subject as a tooltip
- message quick actions overlay to the left on hover instead of reserving hidden width
- Shift-click message popups use a reader-only embedded layout
- adaptive HTML mail colors derive light/dark behavior from the actual reader background for custom palettes
- mailbox storage opens a graphical per-folder IMAP size view; folder properties show measured size in MB when available
0.0.5 additions:
- mobile reader action and attachment fixes
- WebSocket heartbeat, reconnect backoff, watchdog, resync, dedupe and fallback polling
- reliable live new-message refresh and unread reconciliation
- notification click-through to the newest message when UID is available
- full-screen message reader with context-menu entry, Escape, history and deep links
- composer-only upload progress with duplicate and cancellation safeguards
- source-code comment removal and regression contracts
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
- filters: exceptions ("except if"), new conditions (To only, Cc only, size larger/smaller, has attachment, high priority), new actions (add RustBox label, move to Trash), rule templates, creating a missing target folder, "Always move messages from this sender…" in the message context menu, and translated rule summaries
- the message context menu groups forwarding, labels, snooze, filters, sender actions and technical tools into submenus and never exceeds the window height (scrolls instead), so Delete stays reachable
- fixes editing a predefined style mixing two styles: "Allow editing predefined style" seeds the layout/typography from the style and keeps the style; only a colour change creates a confirmed RustBox custom copy taken from the screen, with the theme pinned to the current light/dark mode; switching style locks editing again
- fixes "Soft" contrast removing all borders (self-referencing line colour)
0.2.24 release changes:
- fixes the "Style gradients" switch having no visible effect at the default "standard" depth: gradients moved out of the immersive-only tokens into 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: translucent defaults (app-bar gloss, toolbar tint, sidebar fade, selection sweep, glossy primary/compose buttons, accent backdrop glow) plus signature gradients for styles such as Roundcube Classic/Larry, GitHub, Discord, Reddit, Yahoo, Dracula, Tokyo Night, Synthwave, Aqua, Terminal and Retro 95
- adds 6 retro predefined styles: Windows XP inspired (Luna title bar, green Start-style compose button, task-pane sidebar), Mac OS X Aqua inspired (pinstripes, gel buttons, Aqua selection), iOS 6 inspired (glossy navigation bar, linen, glossy buttons), Windows 7 Aero inspired (Harmony backdrop, glass app bar), Frutiger Aero and BeOS inspired (yellow tab app bar)
- adds a time-of-day theme (`theme = schedule`, light/dark switch times) and an optional per-account colour (app-bar stripe and address dot), persisted by migration V054
- adds an optional second calendar reminder per event (e.g. 1 day before; choices up to 1 week) plus default reminders for new events in Settings → Notifications; all-day events are reminded at the start of the working day; clearing a reminder in the event editor now really clears it (V054)
- groups the interface-style selector into categories (RustBox & classic, Roundcube, mail apps, apps, editor themes, atmospheric, retro) with short, consistent names in English and Polish
- adds a live low-contrast warning for custom colours (WCAG thresholds), skeleton rows while a folder loads and a short cross-fade when the theme or style changes
- fixes the RustBox default selection colour being dark in the light theme, subjects being squeezed by snippets in message rows, and the flagged-message marker (now a side bar instead of a bottom line); reader-toolbar labels collapse below 820px instead of overflowing
0.2.23 release changes:
- adds a "Style depth" appearance setting (`skin_depth`, migration V053): `colors` applies only a predefined style's palette, `standard` (default, previous behavior) adds its fixed layout profile, `immersive` additionally applies its structural design tokens — typography, control/button/avatar shapes, floating or flush panes, app-bar and search treatment, selection markers, row cards and backdrop
- adds a separate "Style gradients" switch (`skin_gradients`, default off, same V053 migration): every gradient used by a style — backdrops, app bars, primary buttons, Roundcube Classic bevels — is applied only when it is on; otherwise each style uses its flat fallback colors
- introduces the interface-style standard: every predefined style now ships light + 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
- adds 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 in Rust (`INTERFACE_SKINS`) and shared by preference and `default_interface_skin` validation; the release validator enforces catalog/selector/profile/palette/token parity
- consolidates V051 and V052 into the fresh schemas; the compatibility baseline is now V052
- fixes the fresh MySQL schema: `mail_preferences.message_loading_mode` now uses an expression default (MySQL rejects literal defaults on TEXT)
0.2.22 release changes:
- adds message snooze: snoozing a message hides it client-side from its folder's current view until the chosen wake time, without touching server-side folder pagination/count SQL; `snoozed_messages` is a local-only cache table with no IMAP side effects, so a snoozed message is unaffected on any other client
- adds an "Undo Send" window: when `undo_send_window_secs > 0` (default 8s, max 120s), a submitted message is held in memory and only dispatched to SMTP after the window elapses unless cancelled; nothing is persisted, so a server restart during the window loses the staged send the same way an in-flight SMTP connection would be lost
- adds `GET/DELETE /messages/send/{pending_send_id}` to poll or cancel a staged send, and `POST/DELETE /messages/{folder}/{uid}/snooze`, `GET /messages/snoozed`, `GET /folders/{name}/snoozed-uids` for snooze
0.2.21 release changes and retained feature set:
- adds configurable login brute-force protection (`[login_rate_limit]`): per-IP token bucket plus per-mailbox and per-IP failed-attempt lockouts with exponential back-off; locked attempts return 429 + Retry-After before any IMAP connection
- the public `/api/auth/policy` GET no longer consumes the per-IP login budget
- adds `cookie_secure = auto | always | never`; `auto` sets the cookie `Secure` attribute only for requests that really arrived over HTTPS, so plain-HTTP installs keep working
- adds attachment memory guards: attachment sets that cannot fit `max_message_size_mb` once MIME-encoded are rejected with 413 from stored sizes before any file is loaded (send, IMAP draft snapshot, attach-message, streaming upload), and `max_concurrent_message_builds` / `message_build_wait_secs` bound how many requests hold attachments in RAM at once
- adds gmail-style message row opening: Ctrl/Cmd+click opens a message in a new tab, Shift+click opens it in a popup window, both deep-linked (outside selection mode; existing shift/ctrl selection behavior while selecting is unchanged)
- fixes the Axum route visibility mismatch for `AttachMessageSourceRequest`, allowing the forward-as-attachment handler to compile from `routes::mod`
- falls back to IMAP `INTERNALDATE` for messages with a missing/invalid message `Date` and repairs cached missing dates when opening a folder
- forwards a message as a real server-created `message/rfc822` `.eml` draft attachment
- expands sandboxed HTML message previews to the real remaining reader height and keeps that height synchronized across layout/fullscreen changes
- adds equal-height text/HTML signature fields and a dedicated raw-HTML + visual signature editor with image insertion
- keeps synchronization activity visible during page/progressive/all mailbox loads and individual message retrieval
- adds Shift-click contiguous message selection and direct first/last pagination controls
- fixes HTML signature formatting so toolbar actions preserve the selected text and reliably toggle formatting
- gives new messages with visible signatures a real editable body entry above the signature and softens/spaces the composer-only separator
- strips the editor-only body-entry marker from serialized outgoing HTML while retaining signature de-duplication metadata
- defaults wide message panes to compact single-line rows, with an Appearance setting to disable the behavior
- adds per-identity signature insertion mode: silent append-on-send (default) or visible/editable in the composer
- adds a durable SQL blob-deletion outbox used by filesystem/shared POSIX/NFS and S3 backends; attachment/draft cleanup is transactionally queued and retried until successful
- failed finalized uploads and failed destination objects from storage migration are also queued for cleanup; storage health probes reuse a fixed key to avoid unbounded probe-object buildup
- suppresses realtime “new message” toast/system notifications for Drafts saves while preserving normal folder/list refresh and `\Draft` + `\Seen` IMAP flags
- adds `Delete draft` in the composer; IMAP deletion is UID/UIDVALIDITY-checked and local draft attachment rows/blobs are cascaded after the server draft is removed
- adds an explicit `Save draft` action before `Cancel`; it persists the current message and attachments as a real IMAP Drafts snapshot without changing background autosave behavior
- explicit IMAP draft saves surface snapshot/APPEND failures instead of reporting a false success
- draft recovery restores attachment metadata from browser snapshots plus the authoritative server draft
- attachment chips show all uploaded files, including inline images/links, and new uploads merge by unique attachment ID instead of replacing visible entries
- fixes multi-attachment/inline-image insertion so new items append instead of replacing the selected image/content
- uses DOM Range insertion with stable caret placement for embedded images and CID file links
- floats image sizing/alignment actions over the editor instead of consuming composer height
- supports Ctrl/Cmd+V image embedding, file drop at the caret, dragging embedded images within the body, and four resize handles
- labels embedded images separately from regular/image attachment chips
- keeps attachment re-embed actions compact and allows separate same-name/same-size files
- compact recipient address summaries with expandable full recipient lists
- adds default pagination at 300 messages per page, plus optional smooth progressive loading and full-folder loading
- adds a composer `Hide fields` / `Show fields` control so From/To/Cc/Bcc/Subject/options can collapse and the editor can use the freed vertical space
- inline-by-default image attachments, re-embedding, CID file links, and inline image layout controls
- opens message-body links without an explicit target in a new tab/window instead of navigating inside the sandboxed mail-preview iframe; explicit targets remain intact
- shows the login success toast only after a real POST login; session restoration/page refresh is silent
- improves non-ASCII/Polish mail display with explicit UTF-8 preview documents and broader RFC 2047 header decoding (including ISO-8859-2/Windows-1250)
- previews PDFs from the authenticated inline attachment endpoint instead of a blob iframe for Safari/iPhone compatibility
- presents compose attachment/message size limits as compact raw values without implementation-detail descriptions
- retains the 0.2.9 realtime notification, shortcut and 300-message mailbox behavior
- adds opt-in adaptive HTML-mail colors in Appearance plus mailbox loading-mode selection, persisted by runtime migration V051
Architecture:
- database_backend = sqlite | mysql | pgsql selects authoritative SQL only
- blob storage and search are independent from the SQL engine
- PostgreSQL/MySQL single-node deployments may continue to use data_dir-backed filesystem/Tantivy
- cluster mode clears an implicit/default data_dir; explicit data_dir is not forbidden, but local-only capabilities are rejected
Blob storage:
- filesystem: local single-node or shared POSIX with storage.shared=true
- shared POSIX examples: NFS, CephFS, GlusterFS
- s3: AWS S3 and S3-compatible endpoints such as MinIO/Ceph RGW
- attachment rows use logical storage keys and SHA-256 metadata
- storage migration command: rustbox storage migrate --from filesystem:/path --to configured [--dry-run] [--verify]
Search:
- local_tantivy: single-node only
- opensearch: cluster-capable external index
- disabled: no full-text index
- OpenSearch operations are tenant-scoped by user_hash
- durable SQL outbox provides claim/lease, retry/backoff and dead-letter handling
- rebuild command: rustbox search reindex
Database/schema:
- migrations/{sqlite,mysql,pgsql}/schema.sql contain the current fresh schema
- compatibility baseline is V052; former V047-V052 changes are consolidated into `schema.sql` and their standalone migrations are removed
- databases older than V052 must be upgraded with a V052-capable RustBox release (0.2.22) before using this build
- V053 adds `display_preferences.skin_depth` (default `standard`) and `display_preferences.skin_gradients` (default off) to existing V052 tenants
- V054 adds `theme_light_from`, `theme_dark_from`, `account_color`, `calendar_events.reminder_extra_minutes` and the calendar default reminders to existing V053 tenants
- V055 adds `dav_name` / `dav_data` to `calendar_events` and `contacts` (CalDAV/CardDAV resource name and stored object) and makes `idx_contacts_email` non-unique (an address book may hold several cards sharing one address)
- next migration number: V056
Docker examples:
- docker/compose-single-sqlite.yml
- docker/compose-ha-garage.yml (2x RustBox + Envoy + Valkey + PostgreSQL + OpenSearch + Garage S3)
- docker/compose-postgres-s3-opensearch.yml (PostgreSQL + SILO/S3 + OpenSearch)
- docker/compose-mysql-nfs-opensearch.yml (MySQL + real NFS volume + OpenSearch)
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.31 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.31
- 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
+100
View File
@@ -0,0 +1,100 @@
RustBox 0.2.31 - final source validation
0.2.31 scope:
- touch long-press action sheet on the message list, exercised in Playwright against the built server with a mocked API (iPhone 13, Pixel 7, iPad Pro 11 landscape; 8 interface styles at small/normal/large scale, compact/comfortable density, reduced motion; Android back, drag-down, Escape); desktop hover actions and right-click menu unchanged
0.2.30 scope:
- optional CalDAV/CardDAV server (`[dav]`, off by default) on the webmail calendar and contacts tables, migration V055; exercised end to end with the ActivePush client request set and the python-caldav client against a live server with a stub IMAP login
0.2.29 scope:
- realtime refresh of the currently open message thread when NewMessages arrives
- correct reply recipients for own sent messages in threads, Roundcube-style mailbox thread/flat list toggle (compact icon-button matching Select messages, with an icon reflecting the active mode), and stronger Polish legacy-encoding/mojibake repair, plus the 0.2.27 mail-list and appearance baseline
0.2.25 scope:
- filters: apply now on existing messages (`POST /api/filters/match`, IMAP UID SEARCH bounds + cache), exceptions, new conditions and actions, templates, missing-folder creation, "always move from this sender"
- message context menu with submenus that fit the window
- fixes: editing a predefined style no longer mixes two styles; "Soft" contrast keeps borders
0.2.24 scope:
- "Style gradients" now applies at every style depth through `59-skin-gradients.css`, to every predefined style except RustBox
- adds Windows XP, Mac OS X Aqua, iOS 6, Windows 7 Aero, Frutiger Aero and BeOS inspired styles (43 styles in total)
- time-of-day theme, per-account colour and second calendar reminder with defaults (V054), grouped style selector with short names, custom-colour contrast warning, message-list skeleton, theme cross-fade
- fixes: light-theme selection colour, subject truncation, flagged marker, reader-toolbar overflow
0.2.23 scope:
- adds Appearance "Style depth" (`skin_depth`, V053): colors only / standard (default) / immersive
- adds a separate "Style gradients" switch (`skin_gradients`, V053, default off); every stylesheet gradient is gated by it
- interface-style standard: every predefined style ships light+dark palettes, a fixed layout profile and structural design tokens; 23 existing styles adapted, 14 new styles added (Dracula, Catppuccin, Solarized, Gruvbox, Tokyo Night, Rosé Pine, Everforest, Synthwave, Paper, Terminal, Aqua Glass, Linear, Material You, Retro 95)
- V051/V052 consolidated into the fresh schemas; compatibility baseline V052
- fresh MySQL schema: `message_loading_mode` uses an expression default
0.2.22 scope:
- adds message snooze (`snoozed_messages`, V052): hides a message client-side from its current folder view until a chosen wake time; purely client-side hiding, no server-side pagination/count SQL changes, no IMAP side effects
- adds an in-memory "Undo Send" window (`undo_send_window_secs`, default 8s, max 120s): a submitted message is staged and only dispatched to SMTP once the window elapses unless cancelled first; not persisted, so a restart during the window drops the staged send
- new endpoints: `GET/DELETE /messages/send/{pending_send_id}`, `POST/DELETE /messages/{folder}/{uid}/snooze`, `GET /messages/snoozed`, `GET /folders/{name}/snoozed-uids`
0.2.21 scope:
- adds configurable login brute-force protection (`[login_rate_limit]`): per-IP token bucket plus per-mailbox and per-IP failed-attempt lockouts with exponential back-off; locked attempts return 429 + Retry-After before any IMAP connection
- the public `/api/auth/policy` GET no longer consumes the per-IP login budget
- adds `cookie_secure = auto | always | never`; `auto` sets the cookie `Secure` attribute only for requests that really arrived over HTTPS, so plain-HTTP installs keep working
- adds attachment memory guards: attachment sets that cannot fit `max_message_size_mb` once MIME-encoded are rejected with 413 from stored sizes before any file is loaded (send, IMAP draft snapshot, attach-message, streaming upload), and `max_concurrent_message_builds` / `message_build_wait_secs` bound how many requests hold attachments in RAM at once
- adds gmail-style message row opening: Ctrl/Cmd+click opens a message in a new tab, Shift+click opens it in a popup window, both deep-linked (outside selection mode; existing shift/ctrl selection behavior while selecting is unchanged)
- fixes the Axum route visibility mismatch for `AttachMessageSourceRequest`, allowing the forward-as-attachment handler to compile from `routes::mod`
- falls back to IMAP `INTERNALDATE` for messages with a missing/invalid message `Date` and repairs cached missing dates when opening a folder
- forwards a message as a real server-created `message/rfc822` `.eml` draft attachment
- HTML message reader iframe height follows the actual remaining reader viewport rather than a fixed 68vh, with resize/fullscreen recalculation
- multi-account sessions expose a top-bar quick switcher, per-browser combined notification selection, per-account notification preferences and the administrator `multi_account_enabled` feature gate
- plain-text and HTML signature controls have aligned heights; raw HTML can be pasted into a modal, previewed/edited visually and extended with inline images
- synchronization spinner covers mailbox pagination, progressive/full-folder loading and message/draft body retrieval
- Shift-click selects a contiguous visible-message range; pagination includes first/last page jumps
- signature HTML formatting preserves selected text across toolbar clicks, including formatting removal, while collapsed-caret clicks cannot enable sticky bold/italic/underline typing modes
- the HTML signature editor is no longer wrapped in a label, preventing whitespace/outside clicks from implicitly activating the Bold toolbar button
- new messages with visible signatures provide an editable body area above a more subtle, better-spaced separator
- wide message panes use compact one-line rows by default, with a persisted Mailbox preference to disable them
- signatures support per-identity silent append-on-send (default) or visible/editable composer insertion
- durable blob deletion outbox covers filesystem/shared POSIX/NFS and S3, pairing metadata deletion with cleanup enqueue in one SQL transaction and retrying storage failures without permanent dead-lettering
- failed finalized uploads and failed storage-migration destination objects are queued for cleanup
- saving/updating a draft stays silent in realtime: Drafts still refreshes, but no “new message” toast/system notification is emitted
- composer `Delete draft` removes the current verified IMAP draft and cascades local draft attachment metadata/blob cleanup
- explicit Save draft action before Cancel forces a real IMAP Drafts snapshot while background autosave behavior remains unchanged
- manual IMAP save reports APPEND/snapshot failures instead of showing false success
- draft recovery restores attachment metadata and the attachment strip keeps inline plus regular files visible together
- new uploads merge against the full server-side draft attachment list so adding another file cannot replace an existing visible entry
- composer UX regression fixed: image actions are contextual/floating instead of a dedicated layout row
- multiple inline images/attachments append independently and do not replace the active selection
- same-name/same-size files are no longer silently de-duplicated
- recipient list collapsing, mailbox pagination, progressive loading, full-folder loading, and reader Details label
- composer fields can collapse to maximize the message editor, and invalid recipient input expands them again
- inline attachment embedding/re-embedding, clipboard/file drop insertion, image drag repositioning, and pointer resize handles
- message-body links without an explicit target open outside the sandboxed preview in a new tab/window, while explicit targets are preserved
- page refresh/session restoration does not emit a false “Successfully logged in” toast
- real login still emits the success toast
- message preview documents explicitly use UTF-8 and RFC 2047 headers support UTF-8, ISO-8859-2 and Windows-1250 decoding
- PDF attachment preview uses the authenticated inline URL for Safari/iPhone compatibility
- 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 adds the CalDAV/CardDAV columns (`dav_name`, `dav_data`) and a non-unique contacts e-mail index; V056 is next
- version metadata identifies 0.2.31
Completed in this environment:
- offline release validator: scripts/validate-release-static.py (467/467 checks passed)
- browser JavaScript assembled in manifest order passes `node --check`
- mailbox thread grouping smoke test covers a three-message References chain, expand/collapse behavior and flat-mode fallback
- reply-recipient helper smoke test covers own-account, alias and normal incoming-message replies
- supplied legacy Polish `.eml` decodes to the expected ISO-8859-2 subject and UTF-8 quoted-printable body in an independent MIME parser check
- all JSON files parse successfully
- Python helper scripts compile successfully
- shell scripts pass `bash -n`
- release metadata consistently identifies 0.2.31
- 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
Not executable in this packaging environment:
- cargo fmt/test/clippy/build with Rust 1.99.1, because Rust/Cargo is not installed
- live PostgreSQL/MySQL/S3/NFS/OpenSearch/IMAP integration tests
The final release gate on a Rust host is:
bash scripts/build-release.sh
This file intentionally does not claim blocked runtime/integration checks as passed.
+788
View File
@@ -0,0 +1,788 @@
# TODO — cluster-safe storage and external search backends
## Goal
Remove the hard dependency on a shared local `data_dir` for multi-node RustBox deployments and make filesystem-backed data explicitly pluggable.
The target architecture should allow fully stateless RustBox application nodes using:
- a shared MySQL/PostgreSQL database for mailbox metadata and cached message bodies,
- optional Redis for shared sessions/events,
- shared blob/object storage for draft attachments and other binary data,
- an optional external full-text indexer,
- no writable node-local state required for normal cluster operation.
This is a follow-up to the 0.1.2 database/cluster audit. RustBox 0.2.0 implements the storage abstraction, S3/shared-POSIX support, OpenSearch backend and durable indexing pipeline. Live interoperability tests against SILO/MinIO/AWS S3/Ceph RGW and a real multi-node cluster remain release-validation work.
---
## Current state in 0.1.2
Shared through SQL:
- mailbox/folder metadata,
- cached message bodies,
- application data stored in MySQL/PostgreSQL,
- persistent sessions when SQL is selected.
Node-local data may still exist under `data_dir/<user_hash>/` for single-node features such as local Tantivy and OpenPGP private-key material. Draft attachments use the configured `[storage]` backend.
Cluster configuration is explicit and capability-based:
```toml
cluster_mode = true
pgp_enabled = false
[storage]
backend = "s3"
[search]
backend = "opensearch"
```
Shared POSIX storage is also supported with `storage.backend="filesystem"` and `storage.shared=true`. Local Tantivy remains single-node only.
---
# 1. Shared data / blob storage abstraction
## Required design
Introduce a storage interface independent from `data_dir`.
Conceptual API:
```text
BlobStorage
put(key, stream, metadata)
get(key) -> stream
delete(key)
exists(key)
stat(key)
```
Application code must store a logical object key in SQL, not an absolute filesystem path.
Suggested key layout:
```text
users/<user_hash>/drafts/<draft_id>/attachments/<attachment_id>
```
The backend is responsible for translating that logical key to a filesystem path or object-store key.
## Variant A — local filesystem
Target: single-node installations.
```toml
[storage]
backend = "filesystem"
path = "/var/lib/rustbox/data"
```
Properties:
- simplest deployment,
- no external dependency,
- valid for one RustBox node,
- not considered cluster-safe unless the path is actually on shared storage.
## Variant B — shared POSIX filesystem
Target: existing clusters with NFS/CephFS/GlusterFS or another shared mount.
```toml
[storage]
backend = "filesystem"
path = "/srv/rustbox-shared"
shared = true
```
Supported use:
- draft attachments,
- other blob-like application files.
Do **not** treat a shared filesystem as a supported way to run multiple concurrent Tantivy writers. Tantivy indexing must remain a separate concern.
Operational notes:
- identical mount path is not required once SQL stores logical keys rather than absolute paths,
- application startup should check read/write access,
- health endpoint should expose storage availability,
- temporary uploads must be written atomically and cleaned after failure.
## Variant C — S3-compatible object storage
Target: recommended multi-node/stateless deployment.
Supported implementations should include any sufficiently compatible S3 API, for example:
- AWS S3,
- MinIO,
- Ceph RGW,
- other S3-compatible services.
Suggested configuration:
```toml
[storage]
backend = "s3"
bucket = "rustbox"
region = "eu-central-1"
endpoint = "https://s3.example.net" # optional for AWS, required for many compatible services
prefix = "rustbox/"
path_style = false
# Credentials should preferably come from environment/instance identity,
# not plaintext TOML.
```
Required behavior:
- streaming upload/download,
- multipart upload for large objects where useful,
- checksum/ETag validation where reliable,
- abort incomplete multipart uploads,
- retry transient errors,
- configurable request/connect timeouts,
- TLS verification enabled by default,
- optional server-side encryption configuration,
- lifecycle support for abandoned temporary objects,
- no public bucket requirement.
The database should keep object metadata and logical object keys. RustBox nodes must not depend on any local copy being present.
## Variant D — additional cloud/object backends
Future adapters may add:
```toml
backend = "azure_blob"
```
or:
```toml
backend = "gcs"
```
Do not hard-code cloud-specific behavior into attachment code. Keep it behind the storage interface.
Priority should be:
1. `filesystem`,
2. `s3`,
3. additional adapters only when there is demand.
---
# 2. Cluster behavior after storage abstraction
When `storage.backend = "s3"`, RustBox allows cluster mode without a writable shared `data_dir`. Cluster validation is capability-based and depends on the selected storage/search backends.
Expected rules:
| Configuration | Cluster allowed | Notes |
|---|---:|---|
| local filesystem | no | unless explicitly marked/verified as shared |
| shared POSIX filesystem | yes | attachments/blobs only |
| S3-compatible storage | yes | preferred stateless mode |
| external index + S3 | yes | full cluster mode |
| local Tantivy | no | single-node only |
| index disabled | yes | SQL search only |
Suggested validation logic:
```text
cluster_mode = false
-> filesystem + Tantivy allowed
cluster_mode = true
-> blob storage must be cluster-safe
-> local Tantivy forbidden
-> PGP requires a cluster-safe key backend or must be disabled
```
---
# 3. Search/index backend abstraction
Search must not be coupled to the blob storage backend.
Introduce a separate interface such as:
```text
SearchBackend
index_message(document)
delete_message(message_id)
search(query, filters, paging)
health()
rebuild(...)
```
Suggested configuration:
```toml
[search]
backend = "disabled"
```
Possible values:
```text
disabled
local_tantivy
quickwit
opensearch
solr
```
Additional adapters can be added later without changing mail/cache storage.
---
## Search variant 0 — disabled
```toml
[search]
backend = "disabled"
```
Use cases:
- cluster without external search service,
- minimal deployments,
- recovery/degraded mode.
Behavior:
- SQL-backed metadata/header search remains available,
- deep body full-text search is unavailable,
- no index files on application nodes.
This must remain a fully supported configuration.
---
## Search variant 1 — local Tantivy
```toml
[search]
backend = "local_tantivy"
path = "/var/lib/rustbox/data"
```
Use case:
- single-node RustBox.
Rules:
- allowed only when `cluster_mode = false`,
- current Tantivy behavior can be retained behind the new `SearchBackend` interface,
- index remains rebuildable from authoritative data,
- the index must never be treated as the only copy of mailbox data.
Do not support multi-node writers against one shared Tantivy directory.
---
## Search variant 2 — Quickwit
```toml
[search]
backend = "quickwit"
url = "http://quickwit:7280"
index = "rustbox-mails"
```
Why evaluate it:
- written in Rust,
- based on Tantivy,
- designed around distributed search and object storage,
- conceptually close to the current local Tantivy implementation.
Required evaluation before implementation:
- message update/delete semantics,
- latency for newly indexed mail,
- tenant/user isolation strategy,
- reindex cost,
- index schema evolution,
- attachment/body size limits,
- failure/retry behavior,
- suitability for mutable mailbox state rather than only append-heavy workloads.
Do not select Quickwit solely because it uses Tantivy; validate mail-specific update/delete behavior first.
---
## Search variant 3 — OpenSearch
```toml
[search]
backend = "opensearch"
urls = ["https://search-1.example.net:9200", "https://search-2.example.net:9200"]
index_prefix = "rustbox"
```
Why support it:
- mature distributed indexing/search,
- common in existing infrastructure,
- strong filtering and full-text capabilities,
- suitable for larger installations.
Trade-offs:
- heavier operational footprint,
- JVM memory requirements,
- index lifecycle/sharding needs administration.
OpenSearch is a good enterprise/existing-infrastructure option, but should not become mandatory for RustBox.
---
## Search variant 4 — Solr / SolrCloud
```toml
[search]
backend = "solr"
url = "https://solr.example.net/solr/rustbox"
```
Why keep it as an option:
- mature Lucene full-text engine,
- still used in established environments,
- supports distributed SolrCloud deployments.
Trade-offs:
- JVM-based,
- cluster operation is not simpler than lightweight alternatives,
- adds a separate operational stack.
Treat Solr as an optional adapter for environments that already operate it, not as the default RustBox cluster indexer.
---
# 4. Recommended deployment variants
## Variant 1 — simple single node
```text
RustBox
├─ SQLite/MySQL/PostgreSQL
├─ local filesystem attachments
└─ local Tantivy
```
Example:
```toml
cluster_mode = false
[storage]
backend = "filesystem"
path = "/var/lib/rustbox/data"
[search]
backend = "local_tantivy"
```
Best for small installations.
---
## Variant 2 — cluster with shared filesystem
```text
RustBox node 1 ─┐
RustBox node 2 ─┼── MySQL/PostgreSQL
RustBox node 3 ─┘
│
└──────── shared NFS/CephFS for attachments
Search: disabled or external
```
Example:
```toml
cluster_mode = true
[storage]
backend = "filesystem"
path = "/srv/rustbox-shared"
shared = true
[search]
backend = "disabled"
```
This is the lowest-change migration path from 0.1.2.
---
## Variant 3 — stateless cluster with S3, no external indexer
```text
RustBox node 1 ─┐
RustBox node 2 ─┼── MySQL/PostgreSQL
RustBox node 3 ─┘
│
└──────── S3 / MinIO / Ceph RGW
Search: SQL-only
```
Example:
```toml
cluster_mode = true
[storage]
backend = "s3"
bucket = "rustbox"
endpoint = "https://minio.example.net"
[search]
backend = "disabled"
```
This should be a first-class supported production configuration.
---
## Variant 4 — stateless cluster with S3 + external indexer
```text
┌── MySQL/PostgreSQL
RustBox nodes ──────┼── S3-compatible blob storage
└── external search cluster
```
Example with Quickwit:
```toml
cluster_mode = true
[storage]
backend = "s3"
bucket = "rustbox"
endpoint = "https://s3.example.net"
[search]
backend = "quickwit"
url = "http://quickwit:7280"
index = "rustbox-mails"
```
Example with OpenSearch:
```toml
[search]
backend = "opensearch"
urls = ["https://search.example.net:9200"]
index_prefix = "rustbox"
```
This is the preferred architecture for larger multi-node installations requiring deep full-text search.
---
# 5. Durable indexing pipeline
Do not make correctness depend on an HTTP request successfully updating the external index synchronously.
Add a durable indexing queue/outbox in the shared SQL database.
Conceptual flow:
```text
mail/cache transaction
│
├── update authoritative SQL data
└── append search_outbox event
│
v
index worker(s)
│
v
Quickwit/OpenSearch/Solr
```
Required properties:
- idempotent events,
- retry with backoff,
- dead-letter/error visibility,
- safe concurrent workers,
- delete events,
- full reindex command,
- index version/schema version,
- ability to rebuild the index from authoritative SQL/IMAP data,
- index lag metric.
The external index must be treated as derived/rebuildable state, never as the source of truth.
---
# 6. PGP/key storage in cluster mode
PGP private keys should not automatically follow the generic attachment/blob backend.
Current safe rule remains:
```toml
pgp_enabled = false
```
for cluster mode until a dedicated key-storage design exists.
Future variants to evaluate:
1. encrypted keys in the shared SQL database,
2. encrypted objects in S3 with a separate master-key/KMS strategy,
3. Vault/HSM/KMS-backed key handling,
4. dedicated secrets backend.
Requirements:
- encryption at rest independent of ordinary object-storage ACLs,
- no plaintext private key in shared object storage,
- identical behavior from every RustBox node,
- rotation/recovery procedure,
- auditability.
Do not treat shared blob storage as a solution for private-key consistency.
---
# 7. WebSocket / IMAP IDLE cluster events
Separate from storage and search, the current in-process event bus is node-local.
TODO:
- add optional cluster event backend,
- initial candidate: Redis Pub/Sub or Redis Streams,
- propagate mailbox/notification invalidations across nodes,
- retain local fast-path for single-node mode,
- define behavior during broker outages.
This is not required for SQL cache consistency, but is required for reliable real-time UI updates across nodes.
---
# 8. Configuration model
RustBox 0.2.0 uses one configuration model only. There are no compatibility aliases for pre-0.2 storage/search keys.
```toml
[storage]
backend = "filesystem"
path = "/var/lib/rustbox/data"
shared = false
[search]
backend = "local_tantivy"
```
Cluster deployments must select cluster-safe backends explicitly (`storage.shared=true` for shared POSIX or `storage.backend="s3"`; `search.backend="opensearch"` or `disabled`).
---
# 9. Migration of existing attachments
Provide an explicit migration command rather than silently moving data during normal startup.
Example target CLI:
```text
rustbox storage migrate \
--from filesystem:/var/lib/rustbox/data \
--to s3:rustbox \
--verify
```
Requirements:
- resumable,
- idempotent,
- checksum/size verification,
- dry-run mode,
- progress reporting,
- do not delete source objects until verification passes,
- update SQL references transactionally or through a safe staged migration,
- rollback/retry documentation.
---
# 10. Health, metrics and operations
Expose at least:
```text
storage backend status
storage request failures
storage latency
index backend status
index queue depth
index lag
index failures/retries
last successful index operation
```
Startup behavior:
- invalid cluster/storage combination -> hard configuration error,
- external index unavailable -> application may start in degraded mode if configured,
- shared SQL unavailable -> application startup/request handling must fail clearly,
- object storage unavailable -> mail reading from SQL may continue, but attachment operations must fail explicitly.
Suggested option:
```toml
[search]
required = false
```
When `required = false`, RustBox can run with SQL search while the external index is unavailable.
---
# 11. Security requirements
For object storage:
- private buckets/containers only,
- least-privilege credentials,
- environment/instance/workload identity preferred over TOML secrets,
- TLS by default,
- optional custom CA bundle for private S3-compatible services,
- prevent object-key traversal/injection,
- server-side encryption option,
- never expose backend credentials to the browser.
For external search:
- authenticated TLS connection,
- per-user/tenant filtering must be enforced server-side by RustBox,
- never trust client-provided tenant/user IDs,
- index documents must not allow cross-user search leakage,
- document deletion must propagate to the index.
---
# 12. Implementation order
## Phase 1 — storage abstraction
- [x] introduce `BlobStorage` interface,
- [x] convert draft attachments from absolute paths to logical object keys,
- [x] implement filesystem backend,
- [x] retain existing behavior for single-node installations,
- [x] add storage health checks.
## Phase 2 — S3 backend
- [x] implement S3-compatible backend,
- [x] streaming upload/download,
- [x] retries/timeouts/checksums,
- [ ] SILO Compose integration validation,
- [ ] MinIO integration validation,
- [ ] AWS S3 integration validation,
- [ ] Ceph RGW compatibility validation,
- [x] migration command from filesystem to S3,
- [x] validate cluster safety directly from `storage.backend` and `storage.shared`.
## Phase 3 — search abstraction
- [x] introduce `SearchBackend` interface,
- [x] move current Tantivy code behind `local_tantivy`,
- [x] preserve `disabled` backend,
- [x] reject `local_tantivy` in cluster mode,
- [x] define canonical indexed message document/schema.
## Phase 4 — durable index events
- [x] add SQL search outbox/queue,
- [x] add index worker locking/concurrency,
- [x] retries/backoff,
- [x] delete events,
- [x] reindex command,
- [x] index lag/error metrics.
## Phase 5 — external index adapters
Recommended evaluation order:
RustBox 0.2.0 selects **OpenSearch** as the supported external adapter because its update/delete/reindex semantics map directly to mutable mailbox documents. Quickwit and Solr remain optional future adapters.
- [ ] Quickwit proof of concept,
- [x] OpenSearch adapter,
- [x] compare update/delete/reindex behavior and operational footprint,
- [x] choose documented recommended backend,
- [ ] Solr adapter only if there is real deployment demand.
## Phase 6 — cluster PGP
- [ ] design dedicated encrypted key backend,
- [x] keep PGP disabled in cluster mode until completed,
- [ ] evaluate Vault/KMS/SQL-encrypted/S3-encrypted variants,
- [ ] add migration procedure for existing local keys.
## Phase 7 — cross-node events
- [ ] introduce cluster event interface,
- [ ] Redis implementation,
- [ ] WebSocket/IMAP IDLE fan-out,
- [ ] reconnection/degraded-mode behavior.
---
# 13. Acceptance criteria
The work is complete when all of the following are true:
- [ ] three or more RustBox nodes can run with no writable shared POSIX filesystem,
- [ ] nodes can be destroyed/recreated without losing attachments or search state,
- [ ] S3-compatible storage works for draft attachments (live SILO/MinIO/AWS validation pending),
- [x] local filesystem remains supported for single-node mode,
- [x] `cluster_mode = true` accepts S3-backed storage without any legacy shared-data-dir switch,
- [x] local Tantivy is rejected in cluster mode,
- [x] external index failure does not corrupt SQL/cache state,
- [x] full index rebuild is possible,
- [x] index events are durable and retryable,
- [x] SQL remains the authoritative cache/data source,
- [x] no PGP private key depends on node-local filesystem in supported cluster mode,
- [x] configuration and migration paths are documented for every supported deployment variant.
---
# Recommended target
For the long-term cluster architecture, prefer:
```text
RustBox nodes (stateless)
│
├── PostgreSQL/MySQL authoritative metadata + mailbox cache
├── Redis (optional) sessions + future cross-node events
├── S3-compatible storage draft attachments / blob data
└── external indexer optional deep full-text search
```
Recommended supported profiles:
1. **Single node:** filesystem + local Tantivy.
2. **Small cluster:** shared filesystem or S3 + search disabled.
3. **Preferred stateless cluster:** S3-compatible storage + external indexer.
4. **Enterprise/existing infrastructure:** S3/shared storage + OpenSearch or optional Solr adapter.
The key design rule is to keep **SQL cache**, **blob storage**, **search index**, and **private-key storage** as separate concerns. None of them should force all application nodes to share the same writable local directory.
-1
View File
@@ -1 +0,0 @@
7f1507c486b78cf33e54beb701dd460d3bd2bd5d
+9 -1
View File
@@ -58,9 +58,11 @@ fn main() {
let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR"));
let css_dir = root.join("src/web/css");
let js_dir = root.join("src/web/js");
let theme_bootstrap_path = root.join("src/web/theme-bootstrap.js");
for path in [
root.join("src/web/index.html"),
theme_bootstrap_path.clone(),
root.join("src/web/icons.svg"),
root.join("src/web/i18n/en.json"),
root.join("src/web/i18n/pl.json"),
@@ -70,8 +72,14 @@ fn main() {
let css_bundle = bundle_manifest(&root, &css_dir, &css_dir.join("manifest.txt"), ".css");
let js_bundle = bundle_manifest(&root, &js_dir, &js_dir.join("manifest.txt"), ".js");
let theme_bootstrap = fs::read_to_string(&theme_bootstrap_path)
.unwrap_or_else(|error| panic!("{}: {error}", theme_bootstrap_path.display()));
let asset_id = fnv1a64(&[css_bundle.as_bytes(), js_bundle.as_bytes()]);
let asset_id = fnv1a64(&[
css_bundle.as_bytes(),
js_bundle.as_bytes(),
theme_bootstrap.as_bytes(),
]);
println!("cargo:rustc-env=RUSTBOX_ASSET_ID={asset_id:016x}");
fs::write(out.join("rustbox-ui.css"), css_bundle).expect("write RustBox CSS bundle");
+93 -1
View File
@@ -2,6 +2,7 @@
host = "127.0.0.1"
port = 3001
environment = "development"
cookie_secure = "auto"
# Global light/dark theme: system | dark | light
default_theme = "system"
# Global interface style used by the login screen and by accounts set to
@@ -9,8 +10,27 @@ default_theme = "system"
# apple-mail, yahoo, hey, nord, minimal and Roundcube variants.
default_interface_skin = "rustbox"
data_dir = ".dev/data"
log_level = "debug"
session_timeout_hours = 24
# Application database. The selected engine stores all RustBox account data.
# With session_backend="database", it stores sessions as well.
database_backend = "sqlite" # sqlite | mysql | pgsql
# SQLite defaults to data_dir/rustbox.sqlite. MySQL/PostgreSQL require database_url.
# database_url = "postgres://rustbox:secret@db.example.com/rustbox"
session_backend = "database" # database | redis
cluster_mode = false
pgp_enabled = true
# For shared database sessions, Redis sessions, or cluster mode provide the same 32-byte key (base64) on all nodes.
# session_encryption_key = "..."
# Direct Redis session backend:
# redis_url = "redis://redis.example.com:6379/"
# Or Redis Sentinel:
# redis_sentinel_nodes = ["sentinel-a:26379", "sentinel-b:26379", "sentinel-c:26379"]
# redis_sentinel_master = "mymaster"
# redis_sentinel_password = "sentinel-secret"
# redis_password = "secret"
# Mail server configuration is administrator-owned. If omitted, development
# also defaults to localhost:993 (IMAP) and localhost:587 (SMTP).
# Certificate verification is disabled automatically only for loopback hosts.
@@ -19,11 +39,83 @@ tls_enabled = true
# Per-file raw upload limit and final RFC822/MIME MTA limit (MiB).
max_attachment_size_mb = 10
max_message_size_mb = 50
# Memory guard for messages with attachments. Sending a message or saving an IMAP
# draft snapshot loads every attachment into RAM and the MIME builder keeps several
# copies (about 3-4x the message size). Attachment sets that cannot fit
# max_message_size_mb are rejected with 413 before any file is read, and at most
# max_concurrent_message_builds requests may hold attachments in memory at once;
# the others wait up to message_build_wait_secs and then get 503.
# Rough peak RAM = max_concurrent_message_builds x 4 x max_message_size_mb.
max_concurrent_message_builds = 4
message_build_wait_secs = 30
# ManageSieve. Leave sieve_host unset to use imap_host.
# "Undo Send" window in seconds (0 disables; max 120). See config.example.toml.
undo_send_window_secs = 8
# Optional administrator-facing features.
sieve_enabled = true
diagnostics_enabled = true
about_enabled = true
# Allow more than one signed-in account in the same browser session.
multi_account_enabled = true
security_headers_enabled = false
# ManageSieve. Leave sieve_host unset to use imap_host. Ignored when disabled.
sieve_port = 4190
sieve_tls_mode = "starttls" # starttls | implicit | none
sieve_allow_insecure_auth = false
sieve_timeout_secs = 10
trusted_proxies = "127.0.0.1,::1"
[sync]
interval_secs = 30
# Maximum number of full message bodies fetched from IMAP during one sync pass when
# automatic deep indexing is enabled. Higher values fill the cache/index faster but
# increase network traffic, application database/search-storage usage and indexing CPU/I/O bursts.
deep_index_batch_size = 10
# Development search/cache profile.
[search]
backend = "local_tantivy"
# Force automatic full-body cache/index for every account in this development instance.
# Each periodic sync may download up to sync.deep_index_batch_size full MIME bodies.
# This is intentionally ON in dev so cache/index behaviour is visible during testing.
# Setting it to false falls back to the existing per-account deep_index preference.
deep_index = true
# Search outbox worker cadence/batch; these do NOT control IMAP body downloads.
worker_interval_secs = 2
worker_batch_size = 100
worker_max_attempts = 12
# Periodic IMAP reconciliation. Active folders still use IMAP IDLE for near-real-time
# updates; this interval controls the fallback/background pass for counters, flags,
# new headers in other folders, and the automatic deep-index pass. Lower values make
# background state converge faster but increase IMAP traffic and server load.
# Built-in CalDAV / CardDAV server (calendar + contacts for phones, ActivePush, DAVx5).
[dav]
enabled = true
# Administrator-only system diagnostics endpoint. Disabled by default.
# When enabled it exposes a redacted runtime/configuration report plus live probes
# for SQL, session persistence (database/Redis), blob storage (filesystem/S3),
# search (Tantivy/OpenSearch), IMAP IDLE counters and security telemetry.
# Access requires BOTH an allowed source network and the configured auth method.
# Secrets (passwords, tokens, S3 keys, session keys and URL credentials) are never returned.
[diagnostics_endpoint]
enabled = false
path = "/api/system/diagnostics"
# Default/fail-safe allowlist: only this host. Add trusted LAN/pod/monitoring CIDRs explicitly.
allowed_networks = ["127.0.0.1/32", "::1/128"]
# token | basic. Credentials should normally come from .env / secret manager, not TOML.
auth = "token"
# token = "replace-with-at-least-24-random-characters"
# basic_username = "rustbox-monitor"
# basic_password = "replace-with-a-long-random-password"
# E-mail addresses and stable user hashes are omitted by default; aggregate counts remain available.
include_user_identifiers = false
# Timeout applied independently to database/session/storage/search probes.
probe_timeout_secs = 5
# Rolling in-memory window used for login failures, 403, 429 and 5xx attack/problem signals.
security_window_secs = 900
+196 -5
View File
@@ -1,16 +1,49 @@
# RustBox 0.0.5
# RustBox 0.2.16
host = "127.0.0.1"
port = 3001
environment = "production"
# Secure attribute of session cookies: auto | always | never.
# auto (default) sets Secure only when the browser reached RustBox over HTTPS:
# X-Forwarded-Proto / Forwarded: proto= / X-Forwarded-Ssl from a trusted proxy,
# otherwise the scheme of the browser's Origin/Referer. Plain-HTTP installations
# therefore keep working. Use "always" behind HTTPS if your proxy strips headers.
cookie_secure = "auto"
# Global light/dark theme: system | dark | light
default_theme = "system"
# Global interface style used by the login screen and by accounts set to
# "Global default". Presets include gmail, proton, zoho, outlook, fastmail,
# apple-mail, yahoo, hey, nord, minimal and Roundcube variants.
# "Global default". Each user can additionally choose a style depth (colors only,
# standard or immersive) in Appearance. Available styles:
# rustbox, roundcube-classic, roundcube-larry, roundcube-elastic, clean, gmail,
# proton, zoho, outlook, minimal, graphite, high-contrast, fastmail, apple-mail,
# yahoo, hey, nord, github, gitlab, discord, slack, reddit, notion, dracula,
# catppuccin, solarized, gruvbox, tokyo-night, rose-pine, everforest, synthwave,
# paper, terminal, aqua, linear, material, retro, winxp, osx, ios6, win7,
# frutiger-aero, beos
default_interface_skin = "gmail"
data_dir = "/var/lib/rustbox"
log_dir = "/var/log/rustbox"
# trace | debug | info | warn | error | off. RUST_LOG still overrides this value.
log_level = "info"
session_timeout_hours = 24
# Application database. The selected engine stores all RustBox account data.
# With session_backend="database", it stores sessions as well.
database_backend = "sqlite" # sqlite | mysql | pgsql
# SQLite defaults to data_dir/rustbox.sqlite. MySQL/PostgreSQL require database_url.
# database_url = "postgres://rustbox:secret@db.example.com/rustbox"
session_backend = "database" # database | redis
cluster_mode = false
# For shared database sessions, Redis sessions, or cluster mode provide the same 32-byte key (base64) on all nodes.
# session_encryption_key = "..."
# Direct Redis session backend:
# redis_url = "redis://redis.example.com:6379/"
# Or Redis Sentinel:
# redis_sentinel_nodes = ["sentinel-a:26379", "sentinel-b:26379", "sentinel-c:26379"]
# redis_sentinel_master = "mymaster"
# redis_sentinel_password = "sentinel-secret"
# redis_password = "secret"
remember_me_enabled = true
remember_me_days = 30
@@ -30,10 +63,35 @@ tls_enabled = true
max_attachment_size_mb = 10
# Maximum size of the complete RFC822/MIME message submitted to the MTA (MiB).
max_message_size_mb = 50
# Memory guard for messages with attachments. Sending a message or saving an IMAP
# draft snapshot loads every attachment into RAM and the MIME builder keeps several
# copies (about 3-4x the message size). Attachment sets that cannot fit
# max_message_size_mb are rejected with 413 before any file is read, and at most
# max_concurrent_message_builds requests may hold attachments in memory at once;
# the others wait up to message_build_wait_secs and then get 503.
# Rough peak RAM = max_concurrent_message_builds x 4 x max_message_size_mb.
max_concurrent_message_builds = 4
message_build_wait_secs = 30
# "Undo Send" window in seconds. A submitted message is held in memory (not
# persisted) for this long before it is actually dispatched to SMTP, so the
# sender can cancel it. 0 disables the feature and sends immediately. Must be
# <= 120; keep this short, since a server restart during the window loses the
# staged send.
undo_send_window_secs = 8
# Optional reverse-proxy subpath, e.g. "/mail".
# base_path = "/mail"
# Optional product features. Disabled features are not registered in the API/UI.
sieve_enabled = true
diagnostics_enabled = true
about_enabled = true
# Allow more than one signed-in account in the same browser session.
multi_account_enabled = true
# Adds content-type-aware browser security headers: document policies on HTML, private no-store/framing/CORP policy on /api, Referrer-Policy on HTML/CSS, and nosniff broadly.
security_headers_enabled = false
# ManageSieve. If sieve_host is omitted, imap_host from the authenticated
# session is used. Plaintext authentication stays blocked unless explicitly
# enabled.
@@ -44,7 +102,6 @@ sieve_allow_insecure_auth = false
sieve_timeout_secs = 10
trusted_proxies = "127.0.0.1,::1"
# Optional server-wide defaults for special IMAP folders. These are used when
# a mailbox has no per-account override and the named folder exists.
# default_sent_folder = "Sent"
@@ -58,7 +115,8 @@ trusted_proxies = "127.0.0.1,::1"
outgoing_headers = []
# OpenPGP. Private keys are stored per user below data_dir with 0700/0600
# permissions. Passphrases are never persisted. WKD is enabled by default;
# HKP lookup is opt-in and HTTPS-only.
# HKP lookup is opt-in and HTTPS-only. Cluster mode requires pgp_enabled=false.
pgp_enabled = true
pgp_wkd_enabled = true
pgp_hkp_enabled = false
pgp_hkp_url = "https://keys.openpgp.org"
@@ -67,3 +125,136 @@ pgp_max_key_bytes = 1048576
# Private-key export is deliberately disabled unless the administrator opts in.
pgp_allow_private_export = false
# Login brute-force protection (POST /api/auth/login). Enabled by default.
# Environment overrides use a double underscore, e.g.
# LOGIN_RATE_LIMIT__ACCOUNT_MAX_FAILURES=10 or LOGIN_RATE_LIMIT__ENABLED=false.
# Client IPs are taken from X-Forwarded-For only when the peer is in trusted_proxies.
# State is kept in memory on each node: behind a load balancer every node counts
# separately, so use sticky routing or lower thresholds in cluster mode.
[login_rate_limit]
enabled = true
# Per-IP token bucket: up to ip_burst attempts at once, then one new attempt
# every ip_replenish_secs seconds. Requests over the limit get HTTP 429.
ip_burst = 5
ip_replenish_secs = 12
# Failed logins counted inside failure_window_secs before a lockout.
# account_max_failures: per mailbox (case-insensitive), from any IP.
# ip_max_failures: per client IP, across all mailboxes (password spraying).
# 0 disables that lockout type.
account_max_failures = 5
ip_max_failures = 20
failure_window_secs = 900
# First lockout length; every consecutive lockout of the same key doubles it
# up to max_lockout_secs. Locked attempts are refused before contacting IMAP.
# Trade-off: an attacker can keep a known mailbox locked for webmail logins
# (IMAP/SMTP clients are unaffected), so keep max_lockout_secs moderate.
lockout_secs = 300
max_lockout_secs = 3600
# IPs/CIDRs exempt from the per-IP lockout (account lockouts still apply).
exempt_networks = []
# Memory cap per tracking table (accounts, IPs).
max_tracked_keys = 100000
[storage]
backend = "filesystem" # filesystem | s3
# path = "/var/lib/rustbox/blobs" # defaults to data_dir for filesystem
shared = false # true only when every RustBox node sees the same filesystem
prefix = ""
# S3-compatible settings:
# bucket = "rustbox"
# region = "us-east-1"
# endpoint = "https://s3.example.com" # MinIO/Ceph RGW/etc. optional for AWS S3
# path_style = false
# allow_http = false
request_timeout_secs = 30
connect_timeout_secs = 5
max_retries = 3
retry_timeout_secs = 60
# access_key_id = "..." # AWS_* environment variables are also supported
# secret_access_key = "..."
# server_side_encryption = "AES256" # or "aws:kms"
# kms_key_id = "..."
# Periodic IMAP reconciliation. Active folders still use IMAP IDLE; this is the
# background/fallback cadence for folder counters, flags, new headers in other
# folders, and the automatic deep-index pass. Lower values improve convergence but
# increase IMAP traffic and mail-server load.
[sync]
interval_secs = 30
# Maximum full message bodies downloaded from IMAP per periodic pass when deep index
# is effective. Larger batches catch up faster at the cost of network traffic,
# application-database/search-storage usage and indexing CPU/I/O.
deep_index_batch_size = 10
# Search is independent from blob storage. Local Tantivy is single-node only.
[search]
backend = "local_tantivy" # disabled | local_tantivy | opensearch
# path = "/var/lib/rustbox/search" # defaults to data_dir for local_tantivy
# urls = ["https://opensearch.example.com:9200"]
index_prefix = "rustbox"
required = false
allow_insecure_http = false
request_timeout_secs = 10
# Server-wide automatic full-body cache/index switch. When true, RustBox proactively
# downloads message bodies from IMAP and indexes them for every account, up to
# sync.deep_index_batch_size per sync pass. This increases IMAP traffic,
# application-database/search-storage usage and indexing load. Production default
# is false. When false,
# the existing per-account deep_index preference may still opt an account in.
deep_index = false
# Search outbox worker cadence/batch; these do NOT control IMAP body downloads.
worker_interval_secs = 2
worker_batch_size = 100
worker_max_attempts = 8
# username = "rustbox"
# password = "..."
# RustBox 0.2 blob/search backends
# Blob storage is independent from the SQL database. PostgreSQL/MySQL can still use
# local filesystem storage in single-node mode. In cluster mode use S3-compatible
# storage or a genuinely shared POSIX filesystem (NFS/CephFS/GlusterFS).
# Built-in CalDAV / CardDAV server. Disabled by default.
# Every account gets one calendar and one address book, backed by the same tables
# as the webmail calendar and contacts:
# https://mail.example.com/dav/calendars/<login>/default/
# https://mail.example.com/dav/addressbooks/<login>/default/
# Clients may also start from https://mail.example.com/dav/ or rely on
# /.well-known/caldav and /.well-known/carddav. Authentication is HTTP Basic with
# the IMAP login and password (checked against imap_host, brute-force protected by
# [login_rate_limit]). Serve RustBox over HTTPS when this is enabled.
# Environment overrides: DAV__ENABLED=true, DAV__AUTH_CACHE_SECS=300, ...
[dav]
enabled = false
# Seconds a successful Basic login is remembered before IMAP is asked again
# (DAV clients authenticate on every request). 0 disables the cache.
auth_cache_secs = 300
# Largest accepted calendar object / vCard (bytes).
max_resource_size = 8388608
# Display names of the collections.
calendar_name = "Calendar"
addressbook_name = "Contacts"
# Administrator-only system diagnostics endpoint. Disabled by default.
# When enabled it exposes a redacted runtime/configuration report plus live probes
# for SQL, session persistence (database/Redis), blob storage (filesystem/S3),
# search (Tantivy/OpenSearch), IMAP IDLE counters and security telemetry.
# Access requires BOTH an allowed source network and the configured auth method.
# Secrets (passwords, tokens, S3 keys, session keys and URL credentials) are never returned.
[diagnostics_endpoint]
enabled = false
path = "/api/system/diagnostics"
# Default/fail-safe allowlist: only this host. Add trusted LAN/pod/monitoring CIDRs explicitly.
allowed_networks = ["127.0.0.1/32", "::1/128"]
# token | basic. Credentials should normally come from .env / secret manager, not TOML.
auth = "token"
# token = "replace-with-at-least-24-random-characters"
# basic_username = "rustbox-monitor"
# basic_password = "replace-with-a-long-random-password"
# E-mail addresses and stable user hashes are omitted by default; aggregate counts remain available.
include_user_identifiers = false
# Timeout applied independently to database/session/storage/search probes.
probe_timeout_secs = 5
# Rolling in-memory window used for login failures, 403, 429 and 5xx attack/problem signals.
security_window_secs = 900
+49 -3
View File
@@ -4,6 +4,17 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"
# Local development settings live in .env. RustBox reads environment variables
# from the process; it does not parse dotenv files itself. Export the file here
# before applying development defaults so explicit .env values always win.
ENV_FILE="${RUSTBOX_ENV_FILE:-$ROOT/.env}"
if [ -f "$ENV_FILE" ]; then
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
fi
if ! command -v cargo >/dev/null 2>&1; then
echo "error: Rust/Cargo is required" >&2
exit 1
@@ -12,7 +23,28 @@ fi
mkdir -p "$ROOT/.dev/data"
export RUSTBOX_CONFIG="${RUSTBOX_CONFIG:-$ROOT/config.dev.toml}"
export DATA_DIR="${DATA_DIR:-$ROOT/.dev/data}"
export RUST_LOG="${RUST_LOG:-rustbox=debug,tower_http=info}"
# Keep application debugging useful without enabling verbose INFO/DEBUG output
# from every dependency. A legacy/simple RUST_LOG level from .env is scoped to
# RustBox automatically; explicit directive filters (containing '=' or ',') are
# respected exactly as provided.
case "${RUST_LOG:-}" in
""|debug)
export RUST_LOG="warn,rustbox=debug,tower_http=info"
;;
trace)
export RUST_LOG="warn,rustbox=trace,tower_http=debug"
;;
info)
export RUST_LOG="warn,rustbox=info,tower_http=info"
;;
warn|error|off)
export RUST_LOG="${RUST_LOG}"
;;
*)
export RUST_LOG="${RUST_LOG}"
;;
esac
if [ ! -f "$RUSTBOX_CONFIG" ]; then
echo "error: config not found: $RUSTBOX_CONFIG" >&2
@@ -20,8 +52,22 @@ if [ ! -f "$RUSTBOX_CONFIG" ]; then
fi
echo "RustBox: http://127.0.0.1:3001"
echo "Config: $RUSTBOX_CONFIG"
echo "Data: $DATA_DIR"
display_path() {
local value="$1"
if [[ "$value" == "$ROOT"/* ]]; then
printf './%s' "${value#"$ROOT"/}"
else
printf '%s' "$(basename "$value")"
fi
}
echo "Config: $(display_path "$RUSTBOX_CONFIG")"
echo "Data: $(display_path "$DATA_DIR")"
if [ -f "$ENV_FILE" ]; then
echo "Env: $(display_path "$ENV_FILE")"
fi
echo "IMAP: ${IMAP_HOST:-localhost}:${IMAP_PORT:-993}"
echo "SMTP: ${SMTP_HOST:-${IMAP_HOST:-localhost}}:${SMTP_PORT:-587}"
echo
cargo run --quiet --bin rustbox -- --check-config
+4
View File
@@ -0,0 +1,4 @@
target
.git
RustBox*.zip
.dev
+63
View File
@@ -0,0 +1,63 @@
# RustBox Docker Compose defaults.
# Copy to docker/.env if you want persistent local values:
# cp .env.example .env
# Secrets below are development examples only. Change them outside local testing.
# Application HTTP listener and published host port.
HOST=0.0.0.0
PORT=3001
# Mail servers used by RustBox.
IMAP_HOST=mail.example.com
IMAP_PORT=993
SMTP_HOST=mail.example.com
SMTP_PORT=587
TLS_ENABLED=true
# Application logging.
RUST_LOG=warn,rustbox=info,tower_http=info
# Shared session encryption key. Required to be identical on all RustBox nodes.
# Replace for any non-local deployment.
SESSION_ENCRYPTION_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
# PostgreSQL variant.
POSTGRES_PASSWORD=rustbox-dev-password
# S3/SILO variant.
S3_ACCESS_KEY=rustbox
S3_SECRET_KEY=rustbox-s3-dev-password
# HA/Garage variant. Development credentials only; replace outside local testing.
GARAGE_ACCESS_KEY_ID=GKb69252a3b0643e8bd08d4cd4
GARAGE_SECRET_ACCESS_KEY=9115be9c9e4994306a4176543b0db461f4eb04c5c8a388676d1b57392d0f4e93
GARAGE_CAPACITY=10G
SIEVE_ENABLED=false
DIAGNOSTICS_ENABLED=true
ABOUT_ENABLED=true
SECURITY_HEADERS_ENABLED=false
# MySQL variant.
MYSQL_PASSWORD=rustbox-dev-password
MYSQL_ROOT_PASSWORD=rustbox-root-password
# NFS variant. NFS_SERVER is required by compose-mysql-nfs-opensearch.yml.
NFS_SERVER=10.0.0.10
NFS_EXPORT=/exports/rustbox
# Optional Compose project name. The compose files also declare `name: rustbox`.
# A shell-exported COMPOSE_PROJECT_NAME overrides the top-level `name:` field.
# Recommended launch commands therefore use `docker compose -p rustbox ...`.
COMPOSE_PROJECT_NAME=rustbox
# development = cookies work over plain HTTP; production = Secure cookies, requires HTTPS
ENVIRONMENT=development
# Administrator-only diagnostics endpoint (requires corresponding TOML section).
# DIAGNOSTICS_ENDPOINT__ENABLED=true
# DIAGNOSTICS_ENDPOINT__AUTH=token
# DIAGNOSTICS_ENDPOINT__TOKEN=replace-with-at-least-24-random-characters
# Alternative Basic Auth:
# DIAGNOSTICS_ENDPOINT__AUTH=basic
# DIAGNOSTICS_ENDPOINT__BASIC_USERNAME=rustbox-monitor
# DIAGNOSTICS_ENDPOINT__BASIC_PASSWORD=replace-with-a-long-random-password
+13
View File
@@ -0,0 +1,13 @@
FROM rust:1.99.1-trixie AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/*
COPY . .
RUN cargo generate-lockfile && cargo build --release --locked
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3t64 && rm -rf /var/lib/apt/lists/*
RUN useradd --system --uid 10001 --create-home --home-dir /var/lib/rustbox rustbox
COPY --from=builder /build/target/release/rustbox /usr/local/bin/rustbox
USER rustbox
EXPOSE 3001
ENTRYPOINT ["/usr/local/bin/rustbox"]
+204
View File
@@ -0,0 +1,204 @@
# RustBox Docker
Run all commands from the `docker/` directory.
Most Compose files use the project name `rustbox`; the HA/Garage example uses `rustbox-ha`. The examples pass `-p` explicitly so an exported `COMPOSE_PROJECT_NAME` cannot attach RustBox to an unrelated Compose project/network.
## Quick start
Two RustBox nodes + Envoy + Valkey + PostgreSQL + OpenSearch + Garage S3:
```bash
PORT=3009 \
IMAP_HOST=mail.example.com \
SMTP_HOST=mail.example.com \
docker compose -p rustbox-ha -f compose-ha-garage.yml up --build
```
Verify load balancing and the selected backend header:
```bash
curl -sSI http://localhost:3009/ | grep -i '^x-rustbox-backend:'
curl -sSI http://localhost:3009/ | grep -i '^x-rustbox-backend:'
```
PostgreSQL + S3/SILO + OpenSearch:
```bash
PORT=3009 \
IMAP_HOST=mail.example.com \
SMTP_HOST=mail.example.com \
docker compose -p rustbox -f compose-postgres-s3-opensearch.yml up --build
```
SQLite, local blobs and local Tantivy:
```bash
IMAP_HOST=mail.example.com \
SMTP_HOST=mail.example.com \
docker compose -p rustbox -f compose-single-sqlite.yml up --build
```
MySQL + NFS + OpenSearch:
```bash
NFS_SERVER=10.0.0.10 \
NFS_EXPORT=/exports/rustbox \
IMAP_HOST=mail.example.com \
SMTP_HOST=mail.example.com \
docker compose -p rustbox -f compose-mysql-nfs-opensearch.yml up --build
```
Check only the RustBox project:
```bash
docker compose -p rustbox -f compose-postgres-s3-opensearch.yml ps
```
Stop it without deleting persistent volumes:
```bash
docker compose -p rustbox -f compose-postgres-s3-opensearch.yml down
```
To also delete the Compose volumes, use `down -v` only when you really want to remove local database, S3 and OpenSearch data.
## Environment variables
Copy the supplied template if you prefer a persistent local configuration:
```bash
cp .env.example .env
```
| Variable | Default | Used by | Description |
|---|---|---|---|
| `HOST` | `0.0.0.0` | all | Address RustBox listens on inside the container. |
| `PORT` | `3001` | all | Published HTTP port. Single-node examples also use it as the RustBox listener; HA keeps RustBox on `3001` internally and maps `${PORT}` to Envoy `8080`. |
| `IMAP_HOST` | `mail.example.com` | all | IMAP server hostname used for mailbox access/authentication. Set this for a real mail system. |
| `IMAP_PORT` | `993` | all | IMAP server port. |
| `SMTP_HOST` | `mail.example.com` | all | SMTP server hostname used for sending mail. |
| `SMTP_PORT` | `587` | all | SMTP submission port. |
| `TLS_ENABLED` | `true` | all | Enables TLS for mail connections according to RustBox configuration. |
| `RUST_LOG` | `warn,rustbox=info,tower_http=info` | all | Rust logging filter. Keeps dependencies quiet by default; use explicit target directives for dependency debugging. |
| `SESSION_ENCRYPTION_KEY` | development key | PostgreSQL, MySQL | Shared key used to encrypt session data. All application nodes must use the same value. Replace it outside local testing. |
| `POSTGRES_PASSWORD` | `rustbox-dev-password` | PostgreSQL | Password for PostgreSQL user `rustbox`; also used in `DATABASE_URL`. |
| `S3_ACCESS_KEY` | `rustbox` | PostgreSQL/S3 | Access key for the bundled S3-compatible SILO service and RustBox S3 client. |
| `S3_SECRET_KEY` | `rustbox-s3-dev-password` | PostgreSQL/S3 | Secret key for SILO and RustBox S3 client. Replace it outside local testing. |
| `MYSQL_PASSWORD` | `rustbox-dev-password` | MySQL/NFS | Password for MySQL user `rustbox`; also used in `DATABASE_URL`. |
| `MYSQL_ROOT_PASSWORD` | `rustbox-root-password` | MySQL/NFS | MySQL root password used by the container and health check. |
| `NFS_SERVER` | none | MySQL/NFS | NFS server address. Required for `compose-mysql-nfs-opensearch.yml`. |
| `NFS_EXPORT` | `/rustbox` | MySQL/NFS | Export path on the NFS server. |
| `GARAGE_ACCESS_KEY_ID` | development key | HA/Garage | Garage S3 access key imported by the one-shot initializer. Replace outside local testing. |
| `GARAGE_SECRET_ACCESS_KEY` | development secret | HA/Garage | Garage S3 secret key. Replace outside local testing. |
| `GARAGE_CAPACITY` | `10G` | HA/Garage | Capacity assigned to the single Garage node for layout balancing. |
| `SIEVE_ENABLED` | `false` | HA/Garage | Enables ManageSieve routes/UI on both RustBox nodes. |
| `DIAGNOSTICS_ENABLED` | `true` | HA/Garage | Enables diagnostics routes/UI. |
| `ABOUT_ENABLED` | `true` | HA/Garage | Enables the About panel; keyboard shortcuts stay available independently. |
| `SECURITY_HEADERS_ENABLED` | `false` | HA/Garage | Enables RustBox browser security headers. |
| `COMPOSE_PROJECT_NAME` | `rustbox` in `.env.example` | Compose | Compose project name. A value exported in the shell overrides the file's `name:`. The documented commands use `-p rustbox`, which has the highest priority. |
`RUSTBOX_CONFIG` and `DATABASE_URL` are set by the Compose files themselves and normally should not be supplied from the shell for these bundled topologies.
## Avoiding another Compose project
If `docker compose ... ps` shows containers from another application, inspect the effective project-name override:
```bash
printf 'COMPOSE_PROJECT_NAME=%s\n' "${COMPOSE_PROJECT_NAME:-<unset>}"
docker compose -p rustbox -f compose-postgres-s3-opensearch.yml ps
```
For RustBox use `-p rustbox` in operational commands. This isolates service discovery too: the RustBox container resolves PostgreSQL as `postgres` only on the RustBox Compose network.
Useful DNS/database checks:
```bash
docker compose -p rustbox -f compose-postgres-s3-opensearch.yml \
run --rm --no-deps --entrypoint getent rustbox hosts postgres
docker compose -p rustbox -f compose-postgres-s3-opensearch.yml \
exec postgres pg_isready -U rustbox -d rustbox
```
## Topologies
### `compose-ha-garage.yml`
Runs two RustBox application containers behind Envoy round-robin load balancing. PostgreSQL stores account/application state, Valkey stores shared sessions, OpenSearch provides the shared search index, and Garage provides S3-compatible blob storage. Only Envoy publishes a host port; the application nodes stay internal.
Envoy adds `X-RustBox-Backend` with the selected upstream address to every proxied response. WebSocket upgrades are enabled and Envoy health-checks `/api/health` on both nodes. The example uses a fixed Docker subnet because Garage advertises its RPC address; change `172.28.80.0/24` in both the Compose file and `configs/ha-garage.toml` if it conflicts with your network.
Garage is a single storage node in this local example. The application tier is redundant, but the backing services are not an HA production cluster by themselves.
### `compose-single-sqlite.yml`
Single RustBox node with SQLite, filesystem blob storage and local Tantivy search. Persistent application data is stored in the `rustbox-data` Docker volume.
### `compose-postgres-s3-opensearch.yml`
Cluster-ready backing topology with PostgreSQL, SILO providing an S3-compatible API, and OpenSearch. The RustBox application container itself has no persistent `data_dir` volume.
The bundled SILO topology is intended for local/integration validation of the S3 API. Production deployments should use a security-reviewed S3-compatible service with an appropriate lifecycle and HA policy.
### `compose-mysql-nfs-opensearch.yml`
MySQL, shared POSIX/NFS blob storage and OpenSearch. The NFS volume driver is deliberate: a normal local Docker named volume is not shared between Docker hosts and therefore is not a valid blob backend for a multi-host RustBox deployment. These examples intentionally run one RustBox application container; the shared backing services demonstrate storage/database topology, not multi-app load balancing.
## Production notes
Replace all example passwords, S3 credentials and `SESSION_ENCRYPTION_KEY` before production use.
The OpenSearch examples disable its security plugin to keep the local Compose topology self-contained. Production should use TLS/authentication and HTTPS URLs in `search.urls`.
The PostgreSQL/SILO and MySQL/NFS examples intentionally start one RustBox application container. `compose-ha-garage.yml` demonstrates two application nodes sharing SQL, sessions, blob storage and search behind Envoy. For production, make the backing PostgreSQL/Valkey/OpenSearch/Garage services appropriately redundant and terminate TLS before or at the load balancer.
## Sessions and HTTP/HTTPS
Docker Compose defaults to `ENVIRONMENT=development`. In this mode session cookies do not carry the `Secure` attribute, so login works when RustBox is opened directly over plain HTTP, for example `http://localhost:3009`.
For a production deployment behind HTTPS set:
```bash
ENVIRONMENT=production docker compose -p rustbox -f compose-postgres-s3-opensearch.yml up -d --build
```
For the HA example, expose Envoy only behind HTTPS/TLS termination and use:
```bash
ENVIRONMENT=production docker compose -p rustbox-ha -f compose-ha-garage.yml up -d --build
```
In `production`, RustBox intentionally marks authentication cookies as `Secure`. Opening the application directly over plain HTTP in that mode causes the browser not to send the session cookie and protected requests return `Invalid or expired session`.
After changing from production/HTTP to development/HTTP, log in again. If the browser still has stale RustBox state, clear cookies/site data for the RustBox origin once and reload.
## OpenSearch disk watermark / 429 errors
The `opensearch-data` named volume still consumes space on the filesystem used by Docker. Running OpenSearch in a container does not give it an independent disk. If the Docker host filesystem reaches OpenSearch's flood-stage threshold, OpenSearch protects indices with `read_only_allow_delete` and RustBox indexing receives HTTP 429 responses.
Check the actual filesystem seen by the container:
```bash
docker compose -p rustbox -f compose-postgres-s3-opensearch.yml exec opensearch \
df -h /usr/share/opensearch/data
```
Also inspect Docker usage on the host:
```bash
df -h
docker system df
docker volume inspect rustbox_opensearch-data
```
Free disk space first. Do not permanently lower the OpenSearch flood-stage safety threshold. If the index remains read-only after disk usage is back below the high watermark, clear the block explicitly:
```bash
docker compose -p rustbox -f compose-postgres-s3-opensearch.yml exec opensearch \
curl -fsS -X PUT http://localhost:9200/rustbox-messages-v1/_settings \
-H 'Content-Type: application/json' \
-d '{"index.blocks.read_only_allow_delete":null}'
```
RustBox 0.2.8 retains the OpenSearch outbox backoff introduced in 0.2.2, so these 429/flood-stage responses no longer produce one warning per queued event on a full disk.
+157
View File
@@ -0,0 +1,157 @@
name: rustbox-ha
x-rustbox-common: &rustbox-common
build:
context: ..
dockerfile: docker/Dockerfile
depends_on:
postgres:
condition: service_healthy
valkey:
condition: service_healthy
garage-init:
condition: service_completed_successfully
opensearch:
condition: service_healthy
environment: &rustbox-environment
RUSTBOX_CONFIG: /etc/rustbox/rustbox.toml
ENVIRONMENT: ${ENVIRONMENT:-development}
HOST: 0.0.0.0
PORT: 3001
IMAP_HOST: ${IMAP_HOST:-mail.example.com}
IMAP_PORT: ${IMAP_PORT:-993}
SMTP_HOST: ${SMTP_HOST:-mail.example.com}
SMTP_PORT: ${SMTP_PORT:-587}
TLS_ENABLED: ${TLS_ENABLED:-true}
RUST_LOG: ${RUST_LOG:-warn,rustbox=info,tower_http=info}
DATABASE_URL: postgres://rustbox:${POSTGRES_PASSWORD:-rustbox-dev-password}@postgres:5432/rustbox
SESSION_ENCRYPTION_KEY: ${SESSION_ENCRYPTION_KEY:-MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=}
AWS_ACCESS_KEY_ID: ${GARAGE_ACCESS_KEY_ID:-GKb69252a3b0643e8bd08d4cd4}
AWS_SECRET_ACCESS_KEY: ${GARAGE_SECRET_ACCESS_KEY:-9115be9c9e4994306a4176543b0db461f4eb04c5c8a388676d1b57392d0f4e93}
SIEVE_ENABLED: ${SIEVE_ENABLED:-false}
DIAGNOSTICS_ENABLED: ${DIAGNOSTICS_ENABLED:-true}
ABOUT_ENABLED: ${ABOUT_ENABLED:-true}
SECURITY_HEADERS_ENABLED: ${SECURITY_HEADERS_ENABLED:-false}
volumes:
- ./configs/ha-garage.toml:/etc/rustbox/rustbox.toml:ro
expose:
- "3001"
networks:
- rustbox-ha
restart: unless-stopped
services:
postgres:
image: postgres:17-trixie
environment:
POSTGRES_DB: rustbox
POSTGRES_USER: rustbox
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-rustbox-dev-password}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U rustbox -d rustbox"]
interval: 5s
timeout: 5s
retries: 20
volumes:
- postgres-ha-data:/var/lib/postgresql/data
networks:
- rustbox-ha
valkey:
image: valkey/valkey:9.1.3-alpine
command: ["valkey-server", "--appendonly", "yes"]
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20
volumes:
- valkey-data:/data
networks:
- rustbox-ha
opensearch:
image: opensearchproject/opensearch:3.9.0
environment:
discovery.type: single-node
DISABLE_INSTALL_DEMO_CONFIG: "true"
DISABLE_SECURITY_PLUGIN: "true"
OPENSEARCH_JAVA_OPTS: -Xms512m -Xmx512m
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:9200/_cluster/health >/dev/null || exit 1"]
interval: 10s
timeout: 5s
retries: 30
volumes:
- opensearch-ha-data:/usr/share/opensearch/data
networks:
- rustbox-ha
garage:
image: dxflrs/garage:v2.5.0
environment:
RUST_LOG: ${GARAGE_RUST_LOG:-info}
command: ["/garage", "-c", "/etc/garage.toml", "server"]
healthcheck:
test: ["CMD", "/garage", "-c", "/etc/garage.toml", "status"]
interval: 5s
timeout: 5s
retries: 30
volumes:
- ./garage/garage.toml:/etc/garage.toml:ro
- garage-meta:/var/lib/garage/meta
- garage-data:/var/lib/garage/data
networks:
rustbox-ha:
ipv4_address: 10.240.6.10
garage-init:
build:
context: ..
dockerfile: docker/garage/init/Dockerfile
depends_on:
garage:
condition: service_healthy
environment:
GARAGE_ACCESS_KEY_ID: ${GARAGE_ACCESS_KEY_ID:-GKb69252a3b0643e8bd08d4cd4}
GARAGE_SECRET_ACCESS_KEY: ${GARAGE_SECRET_ACCESS_KEY:-9115be9c9e4994306a4176543b0db461f4eb04c5c8a388676d1b57392d0f4e93}
GARAGE_BUCKET: rustbox
GARAGE_CAPACITY: ${GARAGE_CAPACITY:-10G}
volumes:
- ./garage/garage.toml:/etc/garage.toml:ro
- garage-meta:/var/lib/garage/meta
- garage-data:/var/lib/garage/data
networks:
- rustbox-ha
rustbox-1:
<<: *rustbox-common
rustbox-2:
<<: *rustbox-common
envoy:
image: envoyproxy/envoy:v1.40.2
depends_on:
- rustbox-1
- rustbox-2
ports:
- "${PORT:-3001}:8080"
volumes:
- ./envoy/envoy.yaml:/etc/envoy/envoy.yaml:ro
networks:
- rustbox-ha
restart: unless-stopped
volumes:
postgres-ha-data:
valkey-data:
opensearch-ha-data:
garage-meta:
garage-data:
networks:
rustbox-ha:
ipam:
config:
- subnet: 10.240.6.0/24
+71
View File
@@ -0,0 +1,71 @@
name: rustbox
services:
mysql:
image: mysql:8.4
environment:
MYSQL_DATABASE: rustbox
MYSQL_USER: rustbox
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-rustbox-dev-password}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rustbox-root-password}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-rustbox-root-password}"]
interval: 5s
timeout: 5s
retries: 30
volumes:
- mysql-data:/var/lib/mysql
opensearch:
image: opensearchproject/opensearch:3.9.0
environment:
discovery.type: single-node
DISABLE_INSTALL_DEMO_CONFIG: "true"
DISABLE_SECURITY_PLUGIN: "true"
OPENSEARCH_JAVA_OPTS: -Xms512m -Xmx512m
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:9200/_cluster/health >/dev/null || exit 1"]
interval: 10s
timeout: 5s
retries: 30
volumes:
- opensearch-data:/usr/share/opensearch/data
rustbox:
build:
context: ..
dockerfile: docker/Dockerfile
depends_on:
mysql:
condition: service_healthy
opensearch:
condition: service_healthy
ports:
- "${PORT:-3001}:${PORT:-3001}"
environment:
RUSTBOX_CONFIG: /etc/rustbox/rustbox.toml
ENVIRONMENT: ${ENVIRONMENT:-development}
HOST: ${HOST:-0.0.0.0}
PORT: ${PORT:-3001}
IMAP_HOST: ${IMAP_HOST:-mail.example.com}
IMAP_PORT: ${IMAP_PORT:-993}
SMTP_HOST: ${SMTP_HOST:-mail.example.com}
SMTP_PORT: ${SMTP_PORT:-587}
TLS_ENABLED: ${TLS_ENABLED:-true}
RUST_LOG: ${RUST_LOG:-warn,rustbox=info,tower_http=info}
DATABASE_URL: mysql://rustbox:${MYSQL_PASSWORD:-rustbox-dev-password}@mysql:3306/rustbox
SESSION_ENCRYPTION_KEY: ${SESSION_ENCRYPTION_KEY:-MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=}
volumes:
- ./configs/mysql-nfs-opensearch.toml:/etc/rustbox/rustbox.toml:ro
- rustbox-nfs:/mnt/rustbox-shared
restart: unless-stopped
volumes:
mysql-data:
opensearch-data:
rustbox-nfs:
driver: local
driver_opts:
type: nfs
o: addr=${NFS_SERVER:?set NFS_SERVER},rw,nfsvers=4
device: :${NFS_EXPORT:-/rustbox}
+91
View File
@@ -0,0 +1,91 @@
name: rustbox
services:
postgres:
image: postgres:17-trixie
environment:
POSTGRES_DB: rustbox
POSTGRES_USER: rustbox
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-rustbox-dev-password}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U rustbox -d rustbox"]
interval: 5s
timeout: 5s
retries: 20
volumes:
- postgres-data:/var/lib/postgresql/data
s3:
image: docker.io/pgsty/silo:RELEASE.2026-10-26T00-00-00Z
command: server /data --console-address :9001
environment:
MINIO_ROOT_USER: ${S3_ACCESS_KEY:-rustbox}
MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY:-rustbox-s3-dev-password}
healthcheck:
test: ["CMD", "silo", "healthcheck", "ready"]
interval: 5s
timeout: 5s
retries: 30
volumes:
- s3-data:/data
s3-init:
image: docker.io/pgsty/mc:RELEASE.2026-10-26T00-00-00Z
depends_on:
s3:
condition: service_healthy
environment:
MC_HOST_local: http://${S3_ACCESS_KEY:-rustbox}:${S3_SECRET_KEY:-rustbox-s3-dev-password}@s3:9000
command: ["mb", "--ignore-existing", "local/rustbox"]
opensearch:
image: opensearchproject/opensearch:3.9.0
environment:
discovery.type: single-node
DISABLE_INSTALL_DEMO_CONFIG: "true"
DISABLE_SECURITY_PLUGIN: "true"
OPENSEARCH_JAVA_OPTS: -Xms512m -Xmx512m
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:9200/_cluster/health >/dev/null || exit 1"]
interval: 10s
timeout: 5s
retries: 30
volumes:
- opensearch-data:/usr/share/opensearch/data
rustbox:
build:
context: ..
dockerfile: docker/Dockerfile
depends_on:
postgres:
condition: service_healthy
s3-init:
condition: service_completed_successfully
opensearch:
condition: service_healthy
ports:
- "${PORT:-3001}:${PORT:-3001}"
environment:
RUSTBOX_CONFIG: /etc/rustbox/rustbox.toml
ENVIRONMENT: ${ENVIRONMENT:-development}
HOST: ${HOST:-0.0.0.0}
PORT: ${PORT:-3001}
IMAP_HOST: ${IMAP_HOST:-mail.example.com}
IMAP_PORT: ${IMAP_PORT:-993}
SMTP_HOST: ${SMTP_HOST:-mail.example.com}
SMTP_PORT: ${SMTP_PORT:-587}
TLS_ENABLED: ${TLS_ENABLED:-true}
RUST_LOG: ${RUST_LOG:-warn,rustbox=info,tower_http=info}
DATABASE_URL: postgres://rustbox:${POSTGRES_PASSWORD:-rustbox-dev-password}@postgres:5432/rustbox
SESSION_ENCRYPTION_KEY: ${SESSION_ENCRYPTION_KEY:-MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=}
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY:-rustbox}
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY:-rustbox-s3-dev-password}
volumes:
- ./configs/postgres-s3-opensearch.toml:/etc/rustbox/rustbox.toml:ro
restart: unless-stopped
volumes:
postgres-data:
s3-data:
opensearch-data:
+27
View File
@@ -0,0 +1,27 @@
name: rustbox
services:
rustbox:
build:
context: ..
dockerfile: docker/Dockerfile
ports:
- "${PORT:-3001}:${PORT:-3001}"
environment:
RUSTBOX_CONFIG: /etc/rustbox/rustbox.toml
ENVIRONMENT: ${ENVIRONMENT:-development}
HOST: ${HOST:-0.0.0.0}
PORT: ${PORT:-3001}
IMAP_HOST: ${IMAP_HOST:-mail.example.com}
IMAP_PORT: ${IMAP_PORT:-993}
SMTP_HOST: ${SMTP_HOST:-mail.example.com}
SMTP_PORT: ${SMTP_PORT:-587}
TLS_ENABLED: ${TLS_ENABLED:-true}
RUST_LOG: ${RUST_LOG:-warn,rustbox=info,tower_http=info}
volumes:
- ./configs/single-sqlite.toml:/etc/rustbox/rustbox.toml:ro
- rustbox-data:/var/lib/rustbox
restart: unless-stopped
volumes:
rustbox-data:
+53
View File
@@ -0,0 +1,53 @@
host = "0.0.0.0"
port = 3001
environment = "development"
database_backend = "pgsql"
session_backend = "redis"
redis_url = "redis://valkey:6379/"
cluster_mode = true
pgp_enabled = false
sieve_enabled = false
diagnostics_enabled = true
about_enabled = true
security_headers_enabled = false
trusted_proxies = "10.240.6.0/24"
[storage]
backend = "s3"
bucket = "rustbox"
region = "garage"
endpoint = "http://garage:3900"
path_style = true
allow_http = true
prefix = "mail"
[sync]
# Background IMAP reconciliation cadence; active folders still use IDLE.
interval_secs = 30
# Full bodies fetched per pass when search.deep_index is effective.
deep_index_batch_size = 10
[search]
backend = "opensearch"
# Dev: proactively cache/index full message bodies for testing.
deep_index = true
urls = ["http://opensearch:9200"]
index_prefix = "rustbox"
allow_insecure_http = true
required = true
worker_interval_secs = 2
worker_batch_size = 100
worker_max_attempts = 8
# Administrator-only system diagnostics. Disabled by default and loopback-only when enabled.
# For monitoring in Docker/Kubernetes, explicitly add the monitoring/pod CIDR and keep
# token/basic credentials in the orchestrator secret store.
[diagnostics_endpoint]
enabled = false
path = "/api/system/diagnostics"
allowed_networks = ["127.0.0.1/32", "::1/128"]
auth = "token"
include_user_identifiers = false
probe_timeout_secs = 5
security_window_secs = 900
+43
View File
@@ -0,0 +1,43 @@
host = "0.0.0.0"
port = 3001
environment = "production"
database_backend = "mysql"
session_backend = "database"
cluster_mode = true
pgp_enabled = false
[storage]
backend = "filesystem"
path = "/mnt/rustbox-shared"
shared = true
[sync]
# Background IMAP reconciliation cadence; active folders still use IDLE.
interval_secs = 30
# Full bodies fetched per pass when search.deep_index is effective.
deep_index_batch_size = 10
[search]
backend = "opensearch"
# Production: do not proactively download/store every full message body.
deep_index = false
urls = ["http://opensearch:9200"]
index_prefix = "rustbox"
allow_insecure_http = true
required = true
worker_interval_secs = 2
worker_batch_size = 100
worker_max_attempts = 8
# Administrator-only system diagnostics. Disabled by default and loopback-only when enabled.
# For monitoring in Docker/Kubernetes, explicitly add the monitoring/pod CIDR and keep
# token/basic credentials in the orchestrator secret store.
[diagnostics_endpoint]
enabled = false
path = "/api/system/diagnostics"
allowed_networks = ["127.0.0.1/32", "::1/128"]
auth = "token"
include_user_identifiers = false
probe_timeout_secs = 5
security_window_secs = 900
@@ -0,0 +1,47 @@
host = "0.0.0.0"
port = 3001
environment = "production"
database_backend = "pgsql"
session_backend = "database"
cluster_mode = true
pgp_enabled = false
[storage]
backend = "s3"
bucket = "rustbox"
region = "us-east-1"
endpoint = "http://s3:9000"
path_style = true
allow_http = true
prefix = "mail"
[sync]
# Background IMAP reconciliation cadence; active folders still use IDLE.
interval_secs = 30
# Full bodies fetched per pass when search.deep_index is effective.
deep_index_batch_size = 10
[search]
backend = "opensearch"
# Production: do not proactively download/store every full message body.
deep_index = false
urls = ["http://opensearch:9200"]
index_prefix = "rustbox"
allow_insecure_http = true
required = true
worker_interval_secs = 2
worker_batch_size = 100
worker_max_attempts = 8
# Administrator-only system diagnostics. Disabled by default and loopback-only when enabled.
# For monitoring in Docker/Kubernetes, explicitly add the monitoring/pod CIDR and keep
# token/basic credentials in the orchestrator secret store.
[diagnostics_endpoint]
enabled = false
path = "/api/system/diagnostics"
allowed_networks = ["127.0.0.1/32", "::1/128"]
auth = "token"
include_user_identifiers = false
probe_timeout_secs = 5
security_window_secs = 900
+38
View File
@@ -0,0 +1,38 @@
host = "0.0.0.0"
port = 3001
environment = "production"
data_dir = "/var/lib/rustbox"
database_backend = "sqlite"
session_backend = "database"
cluster_mode = false
pgp_enabled = true
[storage]
backend = "filesystem"
path = "/var/lib/rustbox/blobs"
shared = false
[sync]
# Background IMAP reconciliation cadence; active folders still use IDLE.
interval_secs = 30
# Full bodies fetched per pass when search.deep_index is effective.
deep_index_batch_size = 10
[search]
backend = "local_tantivy"
# Production: do not proactively download/store every full message body.
deep_index = false
path = "/var/lib/rustbox/search"
# Administrator-only system diagnostics. Disabled by default and loopback-only when enabled.
# For monitoring in Docker/Kubernetes, explicitly add the monitoring/pod CIDR and keep
# token/basic credentials in the orchestrator secret store.
[diagnostics_endpoint]
enabled = false
path = "/api/system/diagnostics"
allowed_networks = ["127.0.0.1/32", "::1/128"]
auth = "token"
include_user_identifiers = false
probe_timeout_secs = 5
security_window_secs = 900
+63
View File
@@ -0,0 +1,63 @@
static_resources:
listeners:
- name: rustbox_http
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: rustbox_ingress
use_remote_address: true
upgrade_configs:
- upgrade_type: websocket
route_config:
name: rustbox_routes
virtual_hosts:
- name: rustbox
domains: ["*"]
response_headers_to_add:
- header:
key: X-RustBox-Backend
value: "%UPSTREAM_REMOTE_ADDRESS%"
append_action: OVERWRITE_IF_EXISTS_OR_ADD
routes:
- match:
prefix: "/"
route:
cluster: rustbox
timeout: 0s
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: rustbox
type: STRICT_DNS
connect_timeout: 2s
lb_policy: ROUND_ROBIN
health_checks:
- timeout: 2s
interval: 5s
unhealthy_threshold: 2
healthy_threshold: 1
http_health_check:
path: /api/health
load_assignment:
cluster_name: rustbox
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: rustbox-1
port_value: 3001
- endpoint:
address:
socket_address:
address: rustbox-2
port_value: 3001
+12
View File
@@ -0,0 +1,12 @@
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
replication_factor = 1
rpc_bind_addr = "0.0.0.0:3901"
rpc_public_addr = "10.240.6.10:3901"
rpc_secret = "8f2ec1b94f2ff5fc39391d8e1666929f8d6adc4f2270d5c950866a0a65e5f4b3"
[s3_api]
s3_region = "garage"
api_bind_addr = "0.0.0.0:3900"
root_domain = ".s3.garage.localhost"
+6
View File
@@ -0,0 +1,6 @@
FROM dxflrs/garage:v2.5.0 AS garage
FROM alpine:3.22
COPY --from=garage /garage /usr/local/bin/garage
COPY docker/garage/init/init.sh /usr/local/bin/init-garage
RUN chmod +x /usr/local/bin/init-garage
ENTRYPOINT ["/usr/local/bin/init-garage"]
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
set -eu
CONFIG=/etc/garage.toml
STATUS=/tmp/garage-status
until garage -c "$CONFIG" status >"$STATUS" 2>/dev/null; do
sleep 1
done
node_id="$(awk '/NO ROLE ASSIGNED/ { print $1; exit }' "$STATUS")"
if [ -n "$node_id" ]; then
garage -c "$CONFIG" layout assign -z local -c "${GARAGE_CAPACITY:-10G}" "$node_id"
garage -c "$CONFIG" layout apply --version 1
fi
garage -c "$CONFIG" key import -n rustbox --yes \
"$GARAGE_ACCESS_KEY_ID" "$GARAGE_SECRET_ACCESS_KEY" >/dev/null 2>&1 || true
garage -c "$CONFIG" bucket create "$GARAGE_BUCKET" >/dev/null 2>&1 || true
garage -c "$CONFIG" bucket allow --read --write \
"$GARAGE_BUCKET" --key "$GARAGE_ACCESS_KEY_ID"
+147
View File
@@ -0,0 +1,147 @@
# CalDAV / CardDAV server
RustBox can serve each account's calendar and contacts over CalDAV (RFC 4791) and
CardDAV (RFC 6352). The DAV view and the webmail use the same tables, so an event
created on a phone appears in the webmail calendar and a contact edited in the
webmail appears on the phone after its next sync.
The server is **off by default**.
## Enabling
```toml
[dav]
enabled = true
auth_cache_secs = 300 # remember a successful login for 5 minutes (0 = always ask IMAP)
max_resource_size = 8388608 # largest accepted event / vCard in bytes
calendar_name = "Calendar"
addressbook_name = "Contacts"
```
Environment overrides use the usual form: `DAV__ENABLED=true`, `DAV__AUTH_CACHE_SECS=60`.
Run RustBox behind HTTPS when DAV is enabled: clients send the mail password with
every request (HTTP Basic).
## URLs
Below `<base_path>/dav` (with the default base path: `https://mail.example.com/dav/`):
| Resource | Path |
| :--- | :--- |
| Service root (for discovery) | `/dav/` |
| Principal | `/dav/principals/<login>/` |
| Calendar home | `/dav/calendars/<login>/` |
| Calendar | `/dav/calendars/<login>/default/` |
| Address book home | `/dav/addressbooks/<login>/` |
| Address book | `/dav/addressbooks/<login>/default/` |
`/.well-known/caldav` and `/.well-known/carddav` redirect to `<base_path>/dav/`
(RFC 6764). They are registered at the host root even when RustBox runs under a
base path; a reverse proxy that only forwards the base path must forward these two
paths as well.
`<login>` is the IMAP login the client authenticates with (normally the e-mail
address). A client may only open its own collections; other logins get `403`.
## Authentication
HTTP Basic with the IMAP login and password. RustBox checks them against the
configured `imap_host` exactly like the webmail login and protects the endpoint
with the same brute-force guard (`[login_rate_limit]`). A successful login is cached
in memory for `auth_cache_secs`, keyed by a hash of login and password, so changing
the mail password takes effect for DAV within that time. The tenant database is
created on the first DAV login, so DAV works for accounts that never opened the
webmail.
## Supported protocol
- `OPTIONS` (no authentication), `PROPFIND` (Depth 0/1), `PROPPATCH` (answered with
`403` per property: collection properties are fixed), `GET`/`HEAD`, `PUT`, `DELETE`.
- `REPORT`: `calendar-query` (VEVENT with optional `time-range`),
`calendar-multiget`, `addressbook-query` (prop-filter / text-match, `anyof`/`allof`,
`limit`), `addressbook-multiget`. Other reports (for example `sync-collection`)
return `403 DAV:supported-report`; `sync-token` is not advertised, so clients fall
back to CTag polling.
- Strong ETags (hash of the served object) with `If-Match` / `If-None-Match`,
and a collection CTag (`CS:getctag`) that changes on every change, including
webmail edits and deletions.
- One calendar (VEVENT only; `supported-calendar-component-set` lists VEVENT) and
one address book (vCard 3.0 and 4.0) per account. `MKCALENDAR` / `MKCOL` return
`403`. Tasks (VTODO) are not supported.
- RFC 4791 UID uniqueness: a second resource with an existing UID is refused with
`CALDAV:no-uid-conflict` — except that an event created in the webmail (for
example from an e-mail invitation) is adopted, so accepting an invitation on a
phone does not duplicate it.
## How data is mapped
Objects uploaded by a client are stored verbatim (`dav_data`) under the client's
resource name (`dav_name`) and decoded into the webmail columns:
| iCalendar | Webmail field |
| :--- | :--- |
| `SUMMARY`, `DESCRIPTION`, `LOCATION` | title, description, location |
| `DTSTART` / `DTEND` / `DURATION` | start / end (times with `TZID` are converted to UTC; dates make an all-day event) |
| `RRULE` | recurrence rule |
| `ATTENDEE`, `ORGANIZER` | attendees, organizer |
| `STATUS`, `URL`, `COLOR` | status, meeting link, colour |
| first two `VALARM` triggers before the start | reminders |
Only the master component is decoded; recurrence overrides (`RECURRENCE-ID`),
`EXDATE`, `VTIMEZONE` and unknown properties stay in the stored object and are
served back unchanged.
| vCard | Webmail field |
| :--- | :--- |
| `FN` (or `N`) | name |
| `EMAIL` (preferred first) | e-mail and further addresses |
| `TEL`, `ADR` | phone numbers, postal addresses |
| `ORG`, `TITLE`, `NOTE`, `URL`, `BDAY` | company, job title, notes, website, birthday |
| embedded `PHOTO` (JPEG/PNG/GIF/WebP up to 3 MB) | contact photo |
Cards without an e-mail address are accepted. Since migration V055 several
contacts may share an e-mail address.
When an event or contact that came from a DAV client is edited in the webmail,
only the properties whose value actually changed are rewritten in the stored
object (plus `DTSTAMP`/`LAST-MODIFIED`/`SEQUENCE` or `REV`); time zones, exceptions,
alarms the webmail does not model and vendor properties are kept. Events and
contacts created in the webmail are served as `<id>.ics` / `<id>.vcf` with a
generated object (iCalendar 2.0 / vCard 3.0).
## Clients
- **ActivePush** (Exchange ActiveSync gateway): no code changes are needed, only
the backend URLs. Its default paths already match RustBox:
```toml
[caldav]
enabled = true
base_url = "https://mail.example.com"
path = "/dav/calendars/%u/"
[carddav]
enabled = true
base_url = "https://mail.example.com"
path = "/dav/addressbooks/%u/"
```
With a RustBox `base_path` (for example `/mail`) prefix the paths:
`/mail/dav/calendars/%u/`. ActivePush authenticates with the same login and
password the phone uses, which must be the IMAP login RustBox accepts. Tasks stay
disabled because the calendar advertises VEVENT only; creating extra calendar or
contact folders from the phone is refused.
- **DAVx⁵, Thunderbird, Apple Calendar/Contacts, Evolution**: use the server URL
`https://mail.example.com/` (discovery through `.well-known`) or
`https://mail.example.com/dav/`, and the mail login and password.
## Limits
- Recurring events are not expanded server side: a `time-range` query returns a
recurring event whenever its first occurrence starts before the end of the range.
- ETags and the CTag are computed from the objects on each request; this is cheap
for personal calendars and address books (thousands of entries) but means a CTag
poll reads the whole collection.
- Contact groups are not exported as group vCards.
- Contacts photos added in the webmail are not included in generated vCards.
+165
View File
@@ -0,0 +1,165 @@
# 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.
- Optional CalDAV/CardDAV server (`[dav]`): the same calendar and contacts sync with phones, Thunderbird, DAVx⁵, Apple Calendar/Contacts or an ActiveSync gateway such as ActivePush. See [CALDAV_CARDDAV.md](CALDAV_CARDDAV.md).
![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) |
|---|---|
| <img src="screenshots/mobile-list.png" width="300" alt="Mobile list"> | <img src="screenshots/mobile-reader.png" width="300" alt="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)
+164 -14
View File
@@ -1,18 +1,167 @@
# RustBox 0.0.4 - implementation status
# RustBox 0.2.31 - implementation status
This file tracks the internal `0.0.4` baseline. The user-approved `docs/RUSTBOX_PLAN.md` remains authoritative. A feature is only listed as implemented when working code exists in this source tree; partial items are called out explicitly.
- 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.31` 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.31 touch action sheet
- On touch screens (`hover: none`) a long press (450 ms) on a message row opens `#context-menu` as a bottom sheet: sender/subject header, quick actions (read/unread and flag via the flags endpoint, archive/delete via `performBulk` on that message with undo), then the desktop context-menu items with drill-down submenus.
- The sheet uses the theme tokens (`--ui-size`, `--ui-scale-factor`, `--density-factor`, `--icon-size`, `--radius-*`), is capped at 560 px on tablets, owns one history entry (Android back closes it) and closes on backdrop tap, drag-down, Escape or any action. Rows are not draggable on touch screens; the hover quick-action overlay is hidden there.
- Context-menu "Mark read/unread" and "Flag/Unflag" update flags without opening the message.
## 0.2.30 CalDAV / CardDAV server
- Optional (`[dav] enabled = true`): one calendar (VEVENT) and one address book per account under `<base_path>/dav/`, RFC 6764 `.well-known` redirects, HTTP Basic auth against IMAP with a login cache and the login brute-force guard.
- PROPFIND, REPORT (calendar-query with time-range, calendar-multiget, addressbook-query, addressbook-multiget), GET/HEAD, PUT, DELETE, strong ETags with If-Match/If-None-Match, CTag. Not implemented: sync-collection, VTODO, MKCALENDAR/MKCOL, server-side recurrence expansion.
- Uploaded objects are stored verbatim (`dav_data`, migration V055) and webmail edits are patched into them per property. Details: `docs/CALDAV_CARDDAV.md`.
## 0.2.29 realtime open-thread refresh
- When realtime reports `NewMessages`, the currently open message detail is re-fetched and its thread is re-rendered if the user is still viewing the same message.
## 0.2.28 thread replies and legacy Polish mail decoding
- Reply composition detects all configured account/identity addresses; replying to an own sent message selects the first non-own recipient and Reply all keeps the remaining external participants.
- The mailbox toolbar toggles the existing `thread_mode` preference between a grouped thread list and a flat message list. Thread groups use Message-ID/In-Reply-To/References metadata, can be expanded/collapsed, and the reader follows the same preference.
- Header/body rendering repairs common Polish mojibake while retaining RFC 2047 charset decoding; cached summaries/details are normalized on output so old cached rows benefit without a schema migration.
## 0.2.27 mail-list and appearance polish
- Monochrome changes on a predefined style promote it to a custom copy before applying the change.
- Wide message rows give the subject more horizontal space and expose its full value in a tooltip.
- Read/unread, flag and open actions appear to the left on hover without reserving hidden row width.
- Shift-click popups use a reader-only embedded layout from initial render.
## 0.2.26 licensing and documentation
- 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
- **Apply now** (`POST /api/filters/match`, `src/routes/filters.rs`): the client sends the rule as a JSON tree (`{op: allof|anyof, args}`, `{not}`, `{test: {kind, match, value, header}}`, max depth 12 / 64 tests). The server translates it into two IMAP `UID SEARCH` criteria — an upper bound (superset) and a lower bound (certainly matching) — and runs them via the new `ImapClient::search_uids`. Header/subject `:contains` tests and sizes are exact; address tests (IMAP `FROM` also matches display names), `:is` / `:matches`, non-ASCII values and has-attachment are narrowed from the `messages` cache with three-valued evaluation; messages that still cannot be decided are counted as `uncertain` and not touched. At most 20 000 UIDs are returned.
- The client (`91-sieve-apply.js`) applies, per message: flags (`\Seen`, IMAP keywords), labels (bulk tag endpoint), copies, then one move/Trash — via the existing message endpoints, 4 requests at a time with progress. Redirect, discard and set-flags are not applied to existing messages and are listed as skipped in the confirmation.
- Entry points: *Check matches / Apply now* in the rule editor (re-check required after the rule changes), *Apply now* per rule in the rule list (rules using syntax the editor cannot rewrite are refused), the quick filter dialog option, and *Always move messages from this sender…* in the message context menu.
- Editor additions: exceptions (`allof (<conditions>, not anyof (<exceptions>))`, parsed back), conditions `to-only`, `cc-only`, `size-over` / `size-under`, `has-attachment` (requires `mime`), `priority-high` (`X-Priority` 1/2 or `Importance: high`), actions `label` (`addflag "RustBox-<tag id>"`) and `trash`, templates, missing-folder creation on save. No schema change.
- Not done: drag-and-drop reordering of rules (the ↑/↓ buttons remain).
## 0.2.25 interface fixes
- Message context menu: grouped into submenus (Forward, Labels, Snooze, Filters, Sender, More); menus are capped at the window height and scroll; scrolling inside a menu does not close it; submenus open on hover, click or →, close with ← / Esc.
- Predefined-style editing: enabling it copies the style's layout profile into the user's display preferences and the layout then follows those values while the style's palette, tokens and gradients stay active. A colour change asks to create a RustBox custom copy built from the computed palette on screen plus the current layout, and pins `theme` to the resolved light/dark mode. Changing `interface_skin` clears `preset_scale_unlocked`; changing `skin_depth` while editing re-seeds the layout.
- `data-contrast="soft"` no longer defines `--line` from itself (a CSS custom-property cycle that left it empty).
## 0.2.24 style gradients and retro styles
- "Style gradients" (`skin_gradients`) now works at every style depth. Gradients live only in `59-skin-gradients.css`, generated from one table: `--grad-backdrop`, `--grad-appbar`, `--grad-toolbar`, `--grad-sidebar`, `--grad-selected` and `--grad-primary` are painted as `background-image` layers over the colours chosen by the palette and depth, so translucent defaults work for every style; signature styles override them with opaque gradients and declare matching app-bar / button text colours. The RustBox custom style is excluded. `57-skin-tokens.css` no longer contains gradients.
- Adds Windows XP, Mac OS X Aqua, iOS 6, Windows 7 Aero, Frutiger Aero and BeOS inspired styles, each with palettes, a layout profile, structural tokens and signature gradients.
- Theme "By time of day" (`theme = schedule`, `theme_light_from` / `theme_dark_from`, migration V054): evaluated in the browser's local time, re-checked every minute.
- Optional per-account colour (`account_color`, V054): 3px stripe on the app bar and a dot before the account address. Display preferences are stored per account, so each account keeps its own colour.
- Custom colours show a live WCAG contrast warning (text/panel, text/background, text/selection 4.5:1; secondary text and accent 3:1; button text on accent 4.5:1). Hidden for predefined palettes.
- Message list shows skeleton rows while a folder loads; theme / style changes cross-fade for 0.3s (disabled with reduced motion).
- Fixes: the default RustBox selection colour (`#28352e`) is treated as automatic so the light theme keeps a light selection; subjects take priority over snippets in message rows; flagged messages use a red side bar; reader-toolbar button labels collapse below 820px.
- Message rows already show label chips (`message.tags`).
- Calendar: optional second reminder per event (`reminder_extra_minutes`, V054; choices from "at start" to "1 week before"), default reminders for new events in Settings → Notifications (`calendar_settings.default_reminder_minutes` / `default_extra_reminder_minutes`), both exported as VALARMs. All-day events are reminded relative to the working-day start (default 09:00). `UpdateEvent` now distinguishes an absent reminder from `null`, so clearing a reminder works.
- The interface-style selector is grouped with `<optgroup>`s (labels translated through the scoped `_text` map) and uses short names; `INTERFACE_SKINS` follows the same order.
## 0.2.23 interface styles
- Adds Appearance → **Style depth** (`display_preferences.skin_depth`, migration V053): `colors` applies only a predefined style's palette and leaves layout controls editable; `standard` (default) keeps the previous palette + fixed layout behavior; `immersive` additionally applies the style's structural tokens.
- Adds Appearance → **Style gradients** (`display_preferences.skin_gradients`, same V053 migration, default off). Every gradient a style uses (immersive backdrops, app bars, primary buttons, the Synthwave logo, Roundcube Classic bevels in any depth) is scoped to `html[data-skin-gradients="true"]`; with the switch off the style uses flat fallback colors. The release validator rejects ungated gradients.
- Interface-style standard: each predefined style has light and dark palettes (`55-roundcube-skins.css`, `56-interface-styles.css`), a fixed layout profile (`interfaceStyleProfiles`) and a structural token block in `57-skin-tokens.css` (fonts, heading treatment, control/button/compose/avatar radii, floating vs. flush panes, pane borders/shadows/glass, app bar and search field, sidebar colors, selection background/marker, row cards, primary/compose buttons, focus ring, bevels). `58-skin-immersive.css` consumes the tokens only at the immersive depth; floating-pane geometry is limited to the desktop three-pane layout (>820px).
- All 23 existing styles were adapted to the standard; 14 styles were added: Dracula, Catppuccin, Solarized, Gruvbox, Tokyo Night, Rosé Pine, Everforest, Synthwave, Paper, Terminal, Aqua Glass, Linear inspired, Material You inspired, Retro 95.
- The style catalog lives in `display_preferences::INTERFACE_SKINS` and is shared by preference and `default_interface_skin` validation; `scripts/validate-release-static.py` checks catalog/selector/profile/palette/token parity.
- V051 and V052 are consolidated into the fresh schemas (baseline V052). The fresh MySQL schema now uses an expression default for `mail_preferences.message_loading_mode`.
## 0.2.22 snooze and undo send
- Adds message snooze: `snoozed_messages` (migration V052) is a local-only cache table — snoozing a message records a wake time and the client hides that message from its current folder view until then, via `GET /folders/{name}/snoozed-uids`; it has no IMAP side effects and does not touch server-side folder pagination/count SQL in `routes/messages/mod.rs`. `POST/DELETE /messages/{folder}/{uid}/snooze` set/clear a snooze, and `GET /messages/snoozed` lists everything currently sleeping across folders for the "View snoozed messages…" dialog. UI: a "Snooze…" context-menu entry and the `b` shortcut (message open) prompt for a duration; `Shift+B` opens the snoozed-messages list.
- Adds an "Undo Send" window: a new `undo_send_window_secs` config knob (default 8, max 120 seconds; 0 disables it and restores immediate synchronous send). When enabled, a submitted message is staged in an in-memory `UndoSendRegistry` (`src/undo_send.rs`) instead of being dispatched immediately; the HTTP response returns right away with `status: "pending"`, and a delayed task actually sends the message once the window elapses unless cancelled first. `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: a server restart during the window loses the staged send, the same as an in-flight SMTP connection would be lost. The composer shows a toast with an "Undo" action and live countdown for the window.
- Known limitation: a brand-new, never-autosaved compose that is sent and then left un-undone relies on the existing autosave/draft-recovery mechanism for recovery if the dispatch itself fails; this is not a new guarantee beyond what autosave already provides.
## 0.2.21 login hardening
- Adds configurable login brute-force protection (`[login_rate_limit]`): per-IP token bucket plus per-mailbox and per-IP failed-attempt lockouts with exponential back-off; locked attempts return 429 + Retry-After before any IMAP connection
- The public `/api/auth/policy` GET no longer consumes the per-IP login budget
- Adds `cookie_secure = auto | always | never`; `auto` sets the cookie `Secure` attribute only for requests that really arrived over HTTPS, so plain-HTTP installs keep working
- Adds attachment memory guards: attachment sets that cannot fit `max_message_size_mb` once MIME-encoded are rejected with 413 from stored sizes before any file is loaded (send, IMAP draft snapshot, attach-message, streaming upload), and `max_concurrent_message_builds` / `message_build_wait_secs` bound how many requests hold attachments in RAM at once
- Adds Gmail-style message row opening: Ctrl/Cmd+click opens a message in a new tab, Shift+click opens it in a popup window, both deep-linked (outside selection mode; existing shift/ctrl selection behavior while selecting is unchanged)
## 0.2.20 browser mail UX
- The forward-as-attachment route request extractor is crate-visible, matching the handler visibility required by the parent router and avoiding Rust `private_interfaces`/private-type build errors.
- Mailbox message dates fall back to IMAP `INTERNALDATE` when the RFC 5322 `Date` header is missing/invalid; stale cached missing dates are repaired on folder access.
- Forward-as-attachment creates a server-side `message/rfc822` `.eml` attachment from the original IMAP RFC822 source.
- HTML message previews use the full remaining vertical reader area and recompute their iframe height as reader chrome/layout changes.
- Signature management uses equal-height text and HTML fields and includes a raw-HTML modal with a sanitized visual editor and inline image import.
- Mail loading activity is represented by the app-bar spinner for pagination, progressive/full-folder retrieval and individual message/draft loading.
- Shift-click selects a contiguous range in the visible message list, and pagination includes direct first/last page navigation.
- Signature HTML formatting now preserves the selected text when toolbar controls are used, so bold/italic/underline and remove-format can be toggled reliably.
- New messages with a visible signature always get an editable body line before the signature; the composer separator is spaced farther from the body and rendered as a subtle solid rule.
- Wide message panes use a default-on compact single-line row layout (sender, subject/snippet, date and icons); users can disable it in Appearance settings.
- Each identity can choose signature insertion: `silent` appends only during send and keeps the editor clean, while `visible` inserts an editable/removable signature into the composer.
- Large To/Cc lists collapse to a compact recipient summary and expand on demand.
- Mailbox loading defaults to pagination at 300 messages per page. Users can switch to smooth progressive loading or explicit full-folder loading in Mailbox settings.
- The composer can collapse From/To/Cc/Bcc/Subject/options from its header so the editor receives the freed vertical space; invalid recipient input automatically reopens the fields.
- The composer exposes an explicit `Save draft` action before `Cancel`; it forces a real IMAP Drafts snapshot with the current MIME body and attachments while background autosave remains local/server-working-state only.
- The composer exposes `Delete draft` for IMAP and recovered drafts; deletion verifies the current IMAP UID/UIDVALIDITY, expunges the server draft first, then removes the local autosave row, cascaded attachment rows and uploaded attachment blobs.
- HTML composing embeds newly added PNG/JPEG/GIF/WebP images inline by default while preserving deleted inline content as reusable attachments.
- Attachment chips can re-embed images or insert CID links for other files, and selected inline images expose quick size/alignment controls plus the advanced crop editor.
- Clipboard image paste and file drag/drop insert at the current/drop caret; already embedded images can be dragged to a new body position.
- Selected inline images expose four pointer resize handles that preserve aspect ratio, and attachment chips distinguish image attachments from ordinary files.
- Message-body links without an explicit `target` default to `target="_blank"` so external navigation opens outside the sandboxed mail-preview iframe; explicit targets are preserved and `_blank` links receive `noopener noreferrer`.
- Login success is shown only after a real login request; refreshing/restoring an existing authenticated session is silent.
- Message/HTML preview documents explicitly declare UTF-8, and encoded-word header decoding delegates first to `mail-parser` for broader charset coverage, including UTF-8, ISO-8859-2 and Windows-1250 subjects and display names.
- PDF attachments are previewed from the authenticated inline attachment URL instead of a `blob:` iframe, improving Safari/iPhone compatibility.
- Composing displays attachment/message size limits as one compact line without configuration/MIME implementation details.
- Initial synchronization remains silent for new-mail notifications; bulk operations retain one summary toast; browser/system modifier shortcuts remain preserved.
- Mailbox pagination is enabled by default at 300 messages per page; the API/preferences accept page sizes up to 300, with progressive and load-all alternatives.
- Blob deletion uses a durable SQL outbox for filesystem/shared POSIX/NFS and S3 backends; attachment/draft metadata deletion is transactionally paired with cleanup enqueue and failed deletes retry in the background.
- V047-V052 are consolidated into the V052 compatibility baseline (0.2.23). V053 adds `skin_depth` and `skin_gradients` to existing V052 tenants; V054 is next.
## 0.2.8 browser security headers
- `security_headers_enabled` remains opt-in and disabled by default for compatibility; optional browser-security headers are centralized in this middleware, so disabling it removes the RustBox-added security profile while normal response semantics such as `Content-Type` and route-specific caching remain.
- HTML/XHTML documents receive the full document profile: CSP, `X-Frame-Options: DENY`, `Permissions-Policy`, `Cross-Origin-Opener-Policy: same-origin`, `Referrer-Policy: no-referrer` and `X-Content-Type-Options: nosniff`.
- The HTML CSP allows only same-origin scripts (`script-src 'self'`) and disables inline script attributes; the theme bootstrap is an external fingerprinted asset. Stylesheets are same-origin only (`style-src 'self'`), while `style-src-attr 'unsafe-inline'` is narrowly retained because the current UI still sets per-element style attributes dynamically.
- Private `/api` responses receive `Cache-Control: no-store`, `X-Content-Type-Options: nosniff` and `Cross-Origin-Resource-Policy: same-origin`. API responses are normally non-frameable (`frame-ancestors 'none'` / `X-Frame-Options: DENY`); `application/pdf` is the narrow exception and allows same-origin framing for the attachment preview (`frame-ancestors 'self'` / `SAMEORIGIN`). API JSON does not receive the full document CSP, `Permissions-Policy`, COOP or `Referrer-Policy`.
- CSS receives `Referrer-Policy: no-referrer` and `X-Content-Type-Options: nosniff`; application assets also receive same-origin CORP without redundant HTML document policies.
- HSTS is deliberately not synthesized by RustBox because the application may sit behind a TLS-terminating reverse proxy and cannot reliably determine the public HTTPS boundary; HSTS belongs at that HTTPS edge.
- The public `/api/auth/policy` is data-minimized to the single login-form capability `rememberMeEnabled`. Optional feature availability (OpenPGP, Sieve, diagnostics, About, multi-account sessions) is returned only from authenticated `/api/auth/session-policy`; session/remember durations are not exposed to the browser policy endpoints.
## 0.2.8 storage/search architecture
- SQL backend selection (`sqlite`/`mysql`/`pgsql`) is independent from blob storage and search backend selection.
- Cluster mode clears an implicit/default `data_dir`; PostgreSQL/MySQL single-node deployments may still intentionally use `data_dir`-backed filesystem storage and local Tantivy.
- Blob storage supports local/shared POSIX filesystems and S3-compatible object storage. Shared POSIX is an explicit capability (`storage.shared=true`) suitable for operator-provided NFS/CephFS/GlusterFS mounts.
- Draft attachment upload/read/delete/snapshot/send paths use the `BlobStorage` abstraction and logical object keys instead of assuming node-local paths.
- OpenSearch is available as the cluster-capable indexer. Local Tantivy remains single-node only; search may also be disabled.
- Search mutations use a durable SQL outbox with claim/lease, retry/backoff and dead-letter state; the index remains rebuildable derived state.
- The V050 compatibility baseline includes the former V047 attachment/search-outbox changes, V048 folder counter generation, V049 blob deletion outbox and compact-row/signature preferences. V051 adds adaptive HTML-mail colors and mailbox loading mode; V052 is reserved as the next migration number.
- SQL global-schema initialization is centralized in `db::pool::configure()`; database-backed sessions do not repeat startup DDL for SQLite, MySQL or PostgreSQL.
- Docker Compose examples cover standalone SQLite, PostgreSQL + SILO (S3-compatible) + OpenSearch, and MySQL + NFS + OpenSearch.
- Live SILO/MinIO/AWS S3/Ceph RGW, live NFS multi-host, live OpenSearch and full multi-node failover acceptance remain release-environment validation items and are not represented as completed here.
## 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
- Folder totals and unread counters use authoritative IMAP `STATUS (... MESSAGES UNSEEN ...)` data. If STATUS is unavailable for a selected mailbox, RustBox falls back to `UID SEARCH UNSEEN`; it never treats SELECT/EXAMINE `UNSEEN` (the first unseen sequence number) as an unread count.
- Folder counter reconciliation runs independently from content synchronization and repairs `MESSAGES/UNSEEN` even when `HIGHESTMODSEQ` is unchanged. A manual counter refresh reuses the cached folder tree while it is fresh and batches STATUS calls on one IMAP session instead of forcing another LIST.
- Read/unread/flag mutations update the affected row/badge optimistically, then reconcile against authoritative IMAP state and publish targeted flag/`FolderStatus` events without replacing the whole message list. The active folder is watched through IMAP IDLE; other folders are reconciled every 30 seconds.
- Large folders may hydrate message headers incrementally, but the sidebar total/unread badge remains server-authoritative. IDLE/periodic reconciliation keeps the CONDSTORE cursor separate from counter refresh, falls back to a full `UID FLAGS` comparison when CONDSTORE is unavailable, and discards partial IMAP fetch streams rather than applying incomplete state. The top-right activity indicator distinguishes active synchronization/indexing from an incomplete local IMAP header cache and exposes backlog details in its tooltip.
- Wide folder badges display `total / unread` while unread is non-zero and total alone at zero; narrow sidebars switch to the compact existing counter presentation.
- IMAP folder list, hierarchy, create, create subfolder, rename, delete, subscribe/unsubscribe, plus collapsible branch navigation with per-account remembered expansion state.
- Special folders stored per account: Sent, Drafts, Trash, Junk and Archive. Spam/Junk additionally supports a browser-wide default destination with a per-account override and IMAP special-use/name fallback.
- Special-folder references follow folder renames and are cleared when a referenced folder is deleted.
@@ -23,9 +172,10 @@ This file tracks the internal `0.0.4` baseline. The user-approved `docs/RUSTBOX_
- Multi-select and bulk read/unread, flag/unflag, archive, move, copy, label and delete actions, including explicit select-all across the complete cached search result set.
- Drag-and-drop a message onto a folder to move it.
- Previous/next message navigation plus keyboard actions for common reader operations, compose, selection mode and selecting the current message.
- After moving/deleting the currently opened message (including Spam, Not spam, Archive and Move), the reader automatically opens the next list item and falls back to the previous item when the removed message was last.
- Message reading, collapsible thread navigation, attachments, raw headers, full RFC822 `.eml` download via `BODY.PEEK[]` and `.eml` import into a selected IMAP folder via APPEND.
- Message/folder context menus with common Roundcube-style actions, including folder properties, full message source, Message-ID/address copy and mailing-list unsubscribe; the reader sender line also exposes compose/contact/search/filter actions.
- Mailbox quota display where the IMAP server exposes quota, including warning/danger states at 80%/95%.
- Mailbox quota display where the IMAP server exposes quota, including warning/danger states at 80%/95%. The storage summary opens a per-folder usage view backed by IMAP message sizes, and folder properties expose the measured size when the server permits the read.
- Per-account mailbox behavior: 20/50/100/200 messages per page, date/sender/subject/size sorting in both directions, configurable mark-read delay, threaded/flat reader mode, HTML/plain preference, remote-image policy including Contacts-only loading, quote limiting, optional list snippets and a default “flagged on top” mode. Each folder can inherit, force-enable or force-disable flagged-first ordering; sorting is applied before pagination.
- IMAP capability diagnostics for IDLE, MOVE, UIDPLUS, CONDSTORE, QRESYNC, SORT, THREAD, NAMESPACE, SPECIAL-USE, QUOTA, ACL, LIST-EXTENDED and UTF8=ACCEPT.
- Forward-as-attachment using `message/rfc822`.
@@ -38,8 +188,8 @@ This file tracks the internal `0.0.4` baseline. The user-approved `docs/RUSTBOX_
- HTML, plain-text and Markdown compose modes. Markdown keeps its source text for the plain alternative and generates escaped/sanitized HTML for preview and delivery without accepting raw Markdown HTML.
- Expanded HTML toolbar: bold, italic, underline, strike, lists, quote, alignment, links, font, size, color, clear formatting, undo/redo.
- Draft autosave, explicit recovery of saved/browser-emergency drafts, and attachment upload/delete. Deliberate IMAP Drafts snapshots carry a stable RustBox draft identifier; the previous remote snapshot is removed only after the new UID is resolved unambiguously and UIDVALIDITY still matches.
- Drag-and-drop attachments with visible upload progress. Attachment uploads stream multipart chunks into `.part` files with an enforced size limit, atomic final rename and stale/incomplete temporary-file cleanup instead of buffering the whole attachment in memory.
- Draft autosave, explicit recovery of saved/browser-emergency drafts, and attachment upload/delete. IMAP Drafts messages are also first-class editable drafts: RustBox opens their RFC822/MIME content directly from IMAP (including external-client drafts), stages only the normal autosave/recovery state locally while editing, and replaces the source UID only after the new snapshot is appended and resolved safely. `X-RustBox-Draft-ID` remains an internal UID-resolution aid for RustBox-created snapshots, not a prerequisite for opening or editing a draft.
- Drag-and-drop attachments with visible upload progress. Attachment uploads stream multipart chunks through `BlobStorage` with an enforced size limit instead of buffering the whole attachment in memory; filesystem and S3-compatible backends share the same logical-object flow.
- Inline image upload using CID attachments, including paste and drag/drop of PNG/JPEG/GIF/WebP images into the composer. During editing, CID images use authenticated same-origin draft-attachment preview URLs and are serialized back to `cid:` references for saved/sent MIME content. Inline images can be resized, aligned left/center/right/inline and cropped in-app (GIF cropping is intentionally disabled to preserve animation); display-only resizing does not rewrite the source file. Unused CID attachments are cleaned when their image is removed from the editor.
- Attachment reminder for common English/Polish wording.
- Multiple identities.
@@ -84,13 +234,14 @@ This file tracks the internal `0.0.4` baseline. The user-approved `docs/RUSTBOX_
- Account switch/remove/add workflow for multiple authenticated accounts in one browser session.
- Active-session list across browsers while the RustBox process is running, with creation/last-active timestamps, friendly browser/device labels, full User-Agent hover detail, individual revoke and revoke-all-others.
- Diagnostics panel exposing IMAP capabilities (including persistent custom-keyword support), authenticated SMTP connectivity, ManageSieve status, browser WebSocket state, local synchronization-cache timestamps/counts and live IMAP IDLE task/subscriber counts without returning server host names or credentials. Public liveness (`/api/health`) and local-storage readiness (`/api/ready`) endpoints are available for service supervision.
- Search-index diagnostics expose cached/indexed counts, with a rebuild operation from the local SQLite cache.
- Optional administrator system diagnostics endpoint (disabled by default) supports explicit IP/CIDR allowlisting plus token/Basic authentication, redacts configured secrets, probes SQL/session/storage/search backends, reports active sessions and IMAP IDLE state, and surfaces bounded node-local security signals for failed logins, 403/429 responses and 5xx failures.
- Search-index diagnostics expose cached/indexed counts, with a rebuild operation from the configured account-data backend.
- IMAP reconnect control drops reusable request-path sessions and verifies a fresh authenticated connection.
- Graceful SIGTERM/Ctrl+C shutdown stops accepting new requests and aborts dedicated IMAP IDLE tasks before process exit.
### Personalization
- Persistent per-user display preferences in SQLite.
- Persistent per-user display preferences in the configured application database.
- Light/dark/system theme.
- Presets: sage, blue, teal, cyan, indigo, slate, graphite, amber, orange, violet, rose, red and custom.
- Custom accent, background, panel and text colors.
@@ -104,12 +255,11 @@ This file tracks the internal `0.0.4` baseline. The user-approved `docs/RUSTBOX_
- Fixed the `E0499` mutable-borrow failure in `mark_all_read`: the `uid_store()` stream is dropped before `session.logout()`.
- Fixed a duplicate `else` introduced in the calendar delete handler during the calendar expansion.
- All 45 SQLite migrations are present, contiguous and wired sequentially in the migration runner during static validation; V042 adds trusted senders, V043 saved responses, V044 persisted interface skins and V045 saved-response format.
- All executable Rust SQL is centralized under `src/queries/{sqlite,mysql,pgsql}/queries.rs`, including session queries.
## Partially implemented - keep visible as incomplete
- Sieve visual builder now supports a three-condition nested AND/OR group, negation, two actions, capability-aware disabling, managed-rule ordering and scheduled vacation dates. An arbitrary-depth condition tree, arbitrary-length action stack and full capability-aware condition/action matrix remain unfinished.
- Drafts are stored locally and can be recovered after interrupted editing/SMTP failure. Remote replacement is implemented conservatively: RustBox resolves the new UID with UIDNEXT plus an exact per-draft header search and only removes the previous snapshot when resolution and UIDVALIDITY are safe; wider real-server compatibility still needs the release gate.
- Composer autosave/recovery state is stored locally, while deliberately saved drafts are standard RFC822/MIME messages in the configured IMAP Drafts folder. IMAP drafts from RustBox or another client open directly in the composer; replacement is conservative (append/resolve first, then remove the prior UID after UIDVALIDITY verification). Wider real-server compatibility still needs the release gate.
- Calendar is local/ICS-capable and stores recurrence/attendee/reminder metadata, but recurrence expansion/edit semantics, invitation responses and CalDAV synchronization are not finished.
- Notifications are wired through IMAP IDLE/WebSocket, include permission status/test controls, and local calendar reminders can show browser notifications and play the configured sound while RustBox is open. Service-worker/background delivery is still not finished.
- Identities emit Organization metadata; composer priority, MDN request headers and validated administrator-defined outbound headers are wired. SMTP DSN negotiation remains to be completed.
@@ -124,9 +274,9 @@ This file tracks the internal `0.0.4` baseline. The user-approved `docs/RUSTBOX_
- Spam backend adapters for rspamd/SpamAssassin/custom command.
- Calendar Month/Week/Day/Agenda views are present; recurrence expansion/exceptions, invitation response workflow and CalDAV remain.
- Full CardDAV synchronization.
- Password-change adapters and persistent cross-restart session inventory/device metadata. Live cross-browser sessions can already be listed/revoked while the process is running.
- Password-change adapters remain unfinished. Session inventory/device metadata is now persistent across restarts and shared-session backends support cross-node listing/revocation.
- Diagnostics now includes IMAP reconnect, SMTP connectivity, index rebuild and last local folder/message synchronization metadata; deeper background task/queue metrics and repair workflows remain.
- Full security, acceptance and performance matrix from `docs/RUSTBOX_PLAN.md`.
- Full live security, acceptance and performance matrix remains release-environment work tracked in `TODO.md`.
## UI reliability / workflow fixes in the current snapshot
+11 -6
View File
@@ -1,17 +1,16 @@
# 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:
4. Run the release gate and package only after it passes:
```bash
cargo fmt --all -- --check
cargo test --locked
cargo clippy --locked --all-targets -- -D warnings
./scripts/build-release.sh
./scripts/package-release.sh
```
`package-release.sh` runs `scripts/build-release.sh` first. That gate regenerates the lockfile, runs the static validator, `cargo fmt --check`, the full `cargo test --locked` suite, Clippy with `-D warnings`, and the locked release build. The ZIP and SHA256 are created only after all of those steps succeed.
5. Verify:
```bash
@@ -26,3 +25,9 @@ RUSTBOX_CONFIG=./config.dev.toml ./target/release/rustbox --check-config
## Browser asset build
Browser JavaScript source lives in functional modules under `src/web/js/`, ordered by `src/web/js/manifest.txt`. `build.rs` concatenates those modules into the embedded `rustbox-ui.js` asset during every Cargo build. The repository has no duplicate `ui/` snapshot; `src/web/` is the only browser UI source.
## Updating an existing checkout to 0.2.16
Do not overlay the release with `cp *` or another command that skips dotfiles. That can retain stale `.github`, `.env.example`, or `Cargo.lock` files.
Use a clean checkout/extraction, or a dotfile-aware copy such as `rsync -a` / `cp -a source/. destination/`. The source release intentionally does not ship `Cargo.lock`; CI and `scripts/build-release.sh` generate it before running `--locked` Cargo commands.
+100
View File
@@ -0,0 +1,100 @@
# RustBox 0.2.16 storage/search operations
RustBox 0.2.16 treats SQL, blob storage and search as independent capabilities. The database engine does not decide whether `data_dir` is allowed.
## Deployment matrix
| Mode | SQL | Blob storage | Search | Persistent `data_dir` |
| --- | --- | --- | --- | --- |
| Single node | SQLite/MySQL/PostgreSQL | local filesystem | local Tantivy | normally yes |
| Single node | MySQL/PostgreSQL | S3 | OpenSearch/disabled | no, if PGP is disabled and no local path is selected |
| Cluster | MySQL/PostgreSQL | S3-compatible | OpenSearch/disabled | disabled by default |
| Cluster | MySQL/PostgreSQL | shared POSIX (NFS/CephFS/GlusterFS) | OpenSearch/disabled | disabled by default |
In cluster mode an implicit/default `data_dir` is cleared. An explicitly configured `data_dir` is still accepted for deliberate local/migration use, but cluster validation rejects local-only features. PostgreSQL/MySQL remain valid with `data_dir` in single-node deployments.
## Blob storage
`storage.backend="filesystem"` supports both local and shared POSIX filesystems. `storage.shared=true` is an operator assertion that every RustBox node sees the same path and the same files; RustBox cannot turn a local disk or a host-local Docker volume into shared storage.
For NFS/CephFS/GlusterFS:
- mount the same logical path on every node;
- keep ownership/permissions consistent;
- validate rename/read/write/delete behavior before production;
- monitor filesystem availability and capacity outside RustBox.
`storage.backend="s3"` supports S3-compatible object storage. The backend streams uploads, applies request/retry timeouts, records SHA-256 attachment metadata and supports S3 server-side encryption options. For multipart uploads interrupted by process/node loss, configure an object-store lifecycle rule to expire abandoned multipart uploads.
Attachment SQL rows store logical object keys. This keeps SQL independent from a node-specific filesystem path.
## Durable blob deletion
Attachment deletion is not best-effort. RustBox writes a `blob_delete_outbox` entry in the same SQL transaction that removes the attachment row or draft row, then attempts the physical delete immediately. A background worker retries failures with claim leases and exponential backoff until the object is gone; object-store `NotFound` is treated as success. The same path is used for local filesystem, shared POSIX/NFS and S3-compatible backends. Failed finalized uploads and failed migration destination objects are also queued for cleanup.
This avoids orphan accumulation when storage is temporarily unavailable while also avoiding the inverse failure mode where a blob is deleted first and the SQL transaction later fails.
## Search and outbox
OpenSearch is the cluster-capable indexer. Every indexed document is scoped by `user_hash`; queries add a server-side tenant filter and deterministic document IDs are routed by tenant.
Search changes use a durable SQL outbox:
1. message mutations that already use a SQL transaction record the indexing event in that same transaction; other derived-state updates enqueue through the same tenant connection;
2. workers claim events with a lease;
3. OpenSearch operations are idempotent;
4. failures are retried with backoff;
5. exhausted events remain visible as dead-letter state.
Search is derived state. A full rebuild is available with:
```bash
rustbox search reindex
```
The rebuild removes superseded outbox events older than the rebuild start and keeps events created or refreshed during the rebuild. In a multi-node deployment run a full reindex in a controlled maintenance window and quiesce search workers on the other RustBox nodes; otherwise a direct rebuild can race an already claimed event from another node. Normal outbox processing does not require this maintenance step.
If `search.required=false`, an unavailable OpenSearch backend may leave the application usable while the outbox grows; readiness reports degraded search and workers retry later. With `search.required=true`, search health is a startup/readiness requirement.
### Automatic body cache / deep index
Automatic full-body caching is controlled by `search.deep_index`. When it is `true` and the search backend is enabled, every periodic IMAP reconciliation may fetch full MIME bodies for messages that have headers cached but no body/index yet. `sync.deep_index_batch_size` caps the number of bodies fetched in one pass. Full bodies are stored in the per-account application database/cache and then queued/indexed, so enabling this increases IMAP bandwidth, application-database/search-storage use and indexing CPU/I/O.
`search.deep_index=false` is the production default. It prevents a server-wide forced full-mailbox download, but preserves the existing per-account `deep_index` opt-in. Development configuration sets `search.deep_index=true` so cache/index behavior is exercised without an additional preference change.
`sync.interval_secs` controls the periodic reconciliation cadence. Active folders still use IMAP IDLE for near-real-time events; the periodic pass is the fallback/background reconciliation for counters, flags and headers in other folders and is also the cadence for automatic deep-index batches. Lower values converge faster but create more IMAP requests.
## Failure behavior
- SQL unavailable: authoritative mailbox/session/account operations cannot proceed.
- S3/shared filesystem unavailable: attachment upload/download/delete and storage readiness fail; SQL data is not silently rewritten to another backend.
- OpenSearch unavailable: outbox events remain durable and retry; SQL remains the source for rebuilding the index.
- One RustBox node lost: S3/shared POSIX attachments and shared SQL remain available to other nodes; local-only Tantivy/SQLite are rejected in cluster mode.
- Shared POSIX mounted only on some nodes: configuration may look valid but runtime attachment operations will fail or diverge. `storage.shared=true` must only be used after infrastructure-level verification.
## Attachment migration
Migrate old filesystem attachments to the configured backend with a dry run first:
```bash
rustbox storage migrate --from filesystem:/old/data/root --to configured --dry-run
rustbox storage migrate --from filesystem:/old/data/root --to configured --verify
```
The migration streams each file, verifies size/SHA-256 when requested and changes the SQL row only after a successful destination write. Source files are not deleted automatically. Run the migration during a controlled maintenance window or otherwise prevent concurrent attachment mutation for the affected drafts.
## Cluster prerequisites
A supported cluster requires:
- MySQL or PostgreSQL as shared SQL;
- S3-compatible storage or a genuinely shared POSIX filesystem;
- OpenSearch or disabled full-text search;
- `pgp_enabled=false` until cluster-safe private-key storage exists;
- the same explicit `session_encryption_key` on every node.
Cross-node realtime event fan-out remains separate from blob/search storage and is not implemented by the 0.2.0 storage work.
## Validation status
The source package includes `scripts/validate-release-static.py` for offline structural checks. Full release validation still requires a Rust 1.99.1 build host and live service tests against the exact PostgreSQL/MySQL, S3/NFS and OpenSearch infrastructure used in production. Live SILO/MinIO/AWS/Ceph RGW and multi-node acceptance items remain intentionally unchecked in `TODO.md` until executed.
+149
View File
@@ -0,0 +1,149 @@
# Administrator system diagnostics endpoint
RustBox can expose a single administrator-only JSON endpoint for operational and security diagnostics. It is intended for local troubleshooting, monitoring agents and cluster operations. It is **disabled by default** and, when enabled without changing the allowlist, accepts only loopback clients.
The endpoint is separate from the user-facing `/api/diagnostics/*` routes. It does not require a RustBox mailbox session. Instead, access requires **both**:
1. the effective client IP to match `diagnostics_endpoint.allowed_networks`, and
2. the configured diagnostics token or Basic Auth credentials.
Requests from a non-allowed address receive HTTP `403`. Requests from an allowed address with missing/invalid diagnostics credentials receive HTTP `401`. Critical backend probe failures return HTTP `503`; healthy or degraded reports return HTTP `200` with `status` set to `ok` or `degraded`.
## Configuration
```toml
[diagnostics_endpoint]
enabled = false
path = "/api/system/diagnostics"
allowed_networks = ["127.0.0.1/32", "::1/128"]
auth = "token" # token | basic
# token = "replace-with-at-least-24-random-characters"
# basic_username = "rustbox-monitor"
# basic_password = "replace-with-a-long-random-password"
include_user_identifiers = false
probe_timeout_secs = 5
security_window_secs = 900
```
`path` must remain below `/api/system/` so it cannot collide with normal application API routes.
### Token authentication
Prefer keeping the token outside TOML, for example in `.env` or an orchestrator secret:
```bash
DIAGNOSTICS_ENDPOINT__ENABLED=true
DIAGNOSTICS_ENDPOINT__AUTH=token
DIAGNOSTICS_ENDPOINT__TOKEN='a-long-random-secret-at-least-24-characters'
```
Example request:
```bash
curl -fsS \
-H "Authorization: Bearer $DIAGNOSTICS_TOKEN" \
http://127.0.0.1:3001/api/system/diagnostics
```
`X-RustBox-Diagnostics-Token` is accepted as an alternative to the Bearer header for monitoring systems that cannot set an `Authorization` header.
### Basic authentication
```toml
[diagnostics_endpoint]
enabled = true
allowed_networks = ["127.0.0.1/32", "::1/128"]
auth = "basic"
```
```bash
DIAGNOSTICS_ENDPOINT__BASIC_USERNAME='rustbox-monitor'
DIAGNOSTICS_ENDPOINT__BASIC_PASSWORD='a-long-random-monitoring-password'
```
Basic Auth must only be used over HTTPS/TLS (directly or at a trusted reverse proxy), because the credentials are not encrypted by the Basic scheme itself.
## Networks and reverse proxies
The network allowlist accepts IPv4/IPv6 addresses and CIDRs. Single addresses may also be specified in TOML.
Example monitoring network:
```toml
[diagnostics_endpoint]
enabled = true
allowed_networks = [
"127.0.0.1/32",
"::1/128",
"10.42.20.0/24",
"2001:db8:42::/64",
]
auth = "token"
```
For a reverse proxy, configure `trusted_proxies` separately. RustBox only accepts `X-Forwarded-For` for diagnostics when the directly connected proxy is trusted. If proxy headers arrive from an untrusted peer, diagnostics fails closed with HTTP `403` rather than treating the proxy address as the real client.
```toml
trusted_proxies = "127.0.0.1/32,10.42.0.0/16"
[diagnostics_endpoint]
enabled = true
allowed_networks = ["10.42.20.0/24"]
auth = "token"
```
A trusted proxy should provide a valid `X-Forwarded-For`. `Forwarded` or `X-Real-IP` without `X-Forwarded-For` is deliberately rejected for this endpoint. In production, also restrict the diagnostics path at the firewall, ingress or reverse-proxy layer.
## Returned data
The report contains:
- application version, process uptime/start time and PID; host-wide CPU, memory, load-average and other machine statistics are deliberately not collected;
- effective non-secret configuration and feature switches;
- IMAP/SMTP/Sieve endpoint configuration without credentials;
- SQL backend, engine version, query latency, account/schema versions, SQLite file size or SQL pool size/idle connection count;
- session persistence health and aggregate active session/user/browser counts for SQL or Redis;
- local-node cached sessions, timestamps and mail endpoints; e-mail/user hash identifiers appear only when `include_user_identifiers=true`;
- filesystem/S3 configuration plus a live storage health probe;
- Tantivy/OpenSearch configuration; OpenSearch adds cluster health, node/shard counts and index document count;
- active IMAP IDLE task/subscriber counts;
- request counters and a bounded in-memory security window for failed logins, access denials, rate-limit responses and server errors;
- configuration warnings for risky settings such as disabled TLS verification, HTTP OpenSearch/S3, global diagnostics allowlists or production browser security headers being disabled.
## Secret handling
The endpoint never returns diagnostics credentials, mailbox passwords, session encryption keys, Redis passwords, S3 access/secret/session keys, OpenSearch passwords or full credential-bearing DSNs. URLs are reduced to scheme/host/port/path and query strings/userinfo are removed. Secret fields are represented only as `...Configured: true|false` where useful.
`include_user_identifiers=false` is the default. With that setting, local session details omit e-mail addresses and stable user hashes while aggregate user/session counts remain visible.
All endpoint responses, including `401` and `403`, carry `Cache-Control: no-store`, `Pragma: no-cache`, `X-Content-Type-Options: nosniff` and `Cross-Origin-Resource-Policy: same-origin` independently of the general browser security-header switch.
## Security signals
The telemetry buffer is bounded and in-memory; it stores only timestamp, event category, source IP and request path. It does **not** store credentials, Authorization headers, cookies, tokens, request bodies or mail content.
Within `security_window_secs` the report includes counts and top source IPs for:
- rejected login attempts;
- HTTP `429` rate limiting;
- HTTP `403` access denials;
- HTTP `5xx` server failures;
- denied diagnostics access.
`bruteForceSignal` becomes true after repeated login rejection/rate-limit activity, while `diagnosticsAttackSignal` flags repeated attempts against the management endpoint. These are preliminary operational signals, not an IDS or an account-lockout mechanism. In a multi-node deployment the counters are node-local; aggregate application/security logs in a central monitoring/SIEM system for cluster-wide detection.
## Process and host metrics
The diagnostics endpoint deliberately avoids host-wide metrics and platform-specific process probes. It does not read `/proc`, query system CPU/RAM/load averages, enumerate host resources or add a system-information dependency. The portable process section is limited to the RustBox PID, while process uptime/start time and request/security counters come from RustBox's own in-memory runtime state. This keeps the endpoint low-overhead and portable across supported operating systems.
## Probe cost
The endpoint performs live backend checks, so it should not be polled at sub-second frequency:
- SQL executes lightweight health/version/schema queries;
- Redis uses `PING` plus bounded `SCAN`/`MGET` of at most 5000 session records and reports `truncated=true` if the cap is hit;
- S3/filesystem health writes and deletes a tiny temporary `_health/...` object;
- OpenSearch queries cluster health and the configured index count;
- each backend probe has its own `probe_timeout_secs` timeout.
A polling interval around tens of seconds is appropriate for typical monitoring. Use the normal `/api/health` or `/api/ready` endpoints for very frequent liveness/readiness checks.
-134
View File
@@ -1,134 +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
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/migrations/... -> RustBox migrations/...
Oxi backend/tests/... -> RustBox tests/...
Oxi backend/examples/... -> RustBox examples/...
Oxi backend/docs/... -> RustBox docs/...
```
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.0.1 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 <tag-or-commit>
```
The report separates:
1. Oxi `backend/` changes that can be mapped into RustBox;
2. Oxi `frontend/` changes that require a manual UI port;
3. other Oxi root-level changes that require manual review;
4. 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;
6. rewrites only patch metadata so `backend/<path>` becomes `<path>`;
7. applies the mapped delta with Git three-way merge support;
8. does not import Oxi frontend sources;
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 or root-level files, the finish step refuses to advance the baseline until you explicitly acknowledge those reviews:
```bash
./scripts/finish-upstream.sh --ui-reviewed --root-reviewed
```
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 <oxi-commit>
```
This creates `port/oxi-<sha>` 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:
- `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.
+51 -6
View File
@@ -11,18 +11,63 @@ 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:
```bash
cargo fmt --all -- --check
cargo test --locked
cargo clippy --locked --all-targets -- -D warnings
./scripts/build-release.sh
```
The release script runs formatting validation, locked tests, Clippy with warnings denied, and a locked release build before accepting the binary.
## Roundcube contacts and identities migration
The migration is a standalone Python administration tool: `scripts/migrate_roundcube.py`. It does not start RustBox and is not exposed by the application UI/API.
By default it reads RustBox database settings from `RUSTBOX_CONFIG` or `/etc/rustbox/rustbox.toml`, recognizes `database_backend = "sqlite" | "mysql" | "pgsql"`, and uses `database_url`. For SQLite without `database_url`, it follows RustBox behavior and uses `data_dir/rustbox.sqlite`.
CLI values can override the target independently with `--config`, `--target-backend` and `--target-url`. The Roundcube source is MySQL and can be supplied either as `--roundcube-url` or as `--roundcube-host`, `--roundcube-port`, `--roundcube-database`, `--roundcube-db-user` and `--roundcube-db-password`.
The default mode migrates every matching Roundcube user. RustBox accounts must already be provisioned so their isolated table prefixes exist in `rustbox_accounts`. Unmatched users are skipped and reported. A single account can be selected with `--roundcube-account`.
Dependencies:
```bash
python3 -m pip install pymysql
# additionally only for a PostgreSQL RustBox target:
python3 -m pip install 'psycopg[binary]'
```
Recommended dry run using the active RustBox config:
```bash
./scripts/migrate_roundcube.py \
--roundcube-url 'mysql://roundcube:secret@127.0.0.1/roundcube' \
--dry-run
```
Run the migration:
```bash
./scripts/migrate_roundcube.py \
--roundcube-url 'mysql://roundcube:secret@127.0.0.1/roundcube'
```
Example with explicit Roundcube connection fields and an overridden MySQL RustBox target:
```bash
./scripts/migrate_roundcube.py \
--config /etc/rustbox/rustbox.toml \
--target-backend mysql \
--target-url 'mysql://rustbox:secret@db/rustbox' \
--roundcube-host rc-db \
--roundcube-database roundcube \
--roundcube-db-user roundcube \
--roundcube-db-password secret
```
Use `--roundcube-prefix rc_` when Roundcube uses prefixed table names. The migration is idempotent for practical reruns: contacts are matched by email and identities by their stable identity fields before insert/update.
Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

+3 -3
View File
@@ -6,7 +6,7 @@ use std::path::Path;
#[path = "../src/email_theme.rs"]
mod email_theme;
use email_theme::{detect_email_theme, EmailTheme};
use email_theme::{EmailTheme, detect_email_theme};
fn main() {
let debugging_dir = Path::new("../debugging");
@@ -66,13 +66,13 @@ fn main() {
}
println!("{}", "-".repeat(60));
println!("Passed: {}, Failed: {}", passed, failed);
println!("Passed: {passed}, Failed: {failed}");
}
fn test_file(path: &Path, expected: &str) -> (bool, String) {
let content = match fs::read(path) {
Ok(c) => c,
Err(e) => return (false, format!("Error reading: {}", e)),
Err(e) => return (false, format!("Error reading: {e}")),
};
let msg = match MessageParser::default().parse(&content) {
+68
View File
@@ -0,0 +1,68 @@
# RustBox database schema and migrations
RustBox keeps one complete fresh-install schema per supported SQL backend:
- `sqlite/schema.sql`
- `mysql/schema.sql`
- `pgsql/schema.sql`
## Compatibility baseline
The consolidated compatibility baseline is **V052** (RustBox 0.2.23). Historical migrations up to and including V052 are already represented by the complete `schema.sql` files and are intentionally not shipped as standalone runtime migrations in this source tree:
- V047-V050 — attachment/search-outbox changes, folder counter generation, blob deletion outbox, compact-row and signature preferences (consolidated in 0.2.16);
- V051 — `display_preferences.adaptive_email_colors` (default `0`) and `mail_preferences.message_loading_mode` (default `pagination`);
- V052 — the `snoozed_messages` table used by Snooze (local cache state only, no IMAP side effects, no foreign keys).
Databases older than V052 must first be upgraded with a RustBox release that can reach V052. RustBox 0.2.22 accepts V050 and V051 databases and migrates them to V052, so a tenant that has not logged in since an older release should be opened once with 0.2.22 before upgrading. Tenants below the baseline are refused with an explicit error instead of being partially migrated.
The MySQL fresh schema now declares `mail_preferences.message_loading_mode` with an expression default (`DEFAULT ('pagination')`); MySQL rejects literal defaults on `TEXT` columns.
## V053 — RustBox 0.2.23
V053 adds two appearance columns to `display_preferences`.
`skin_depth`, default `standard`, controls how far a predefined interface style may reshape the interface:
- `colors` — only the style's palette is applied; layout, shapes and typography come from your own appearance settings;
- `standard` — palette plus the style's fixed layout preferences (the behavior of earlier releases);
- `immersive` — additionally applies the style's structural design tokens: typography, control and avatar shapes, floating panes, app-bar treatment, selection markers and backdrop.
`skin_gradients`, default `0` / off, is a separate switch for every gradient a style uses (backdrops, app bars, buttons, Roundcube Classic bevels). When off, each style falls back to flat colors defined next to its gradient.
Fresh installations already contain both columns in `schema.sql` and are registered at the latest schema version. Existing V052 tenants execute `V053__skin_depth_and_gradients.sql`.
## V054 — RustBox 0.2.24
V054 adds three appearance columns to `display_preferences` and calendar reminder columns:
- `theme_light_from` / `theme_dark_from`, defaults `07:00` / `19:00` (`HH:MM`, local time of the browser) — used when `theme = 'schedule'`: light between the two times, dark otherwise (the window may wrap past midnight);
- `account_color`, default empty — an optional per-account colour shown as a stripe on the app bar and a dot next to the account address;
- `calendar_events.reminder_extra_minutes` (nullable) — an optional second reminder per event, e.g. one day before;
- `calendar_settings.default_reminder_minutes` / `default_extra_reminder_minutes` (nullable) — reminders pre-selected for new events.
Fresh installations already contain the columns in `schema.sql`. Existing V053 tenants execute `V054__theme_schedule_account_color_and_reminders.sql`.
## V055 — CalDAV / CardDAV
V055 prepares the calendar and contacts tables for the optional CalDAV/CardDAV server (`[dav]`):
- `calendar_events.dav_name` / `contacts.dav_name` (nullable) — resource name chosen by the DAV client (e.g. `4f9e….ics`); rows created in the webmail keep `NULL` and are served as `<id>.ics` / `<id>.vcf`;
- `calendar_events.dav_data` / `contacts.dav_data` (nullable) — the iCalendar object / vCard exactly as uploaded, so time zones, recurrence exceptions, alarms and vendor properties survive; webmail edits are patched into it;
- `idx_contacts_email` becomes a regular (non-unique) index, because an address book may contain several cards with the same address (and cards without any address);
- new indexes `idx_calendar_events_dav_name` and `idx_contacts_dav_name`.
Fresh installations already contain the columns in `schema.sql`. Existing V054 tenants execute `V055__caldav_carddav.sql`.
The next migration number is **V056**.
## Migration rules
Any future schema-changing release must:
1. update all three complete `schema.sql` files;
2. add the same logical numbered migration for SQLite, MySQL and PostgreSQL;
3. register that migration in `src/db/pool.rs`;
4. extend the offline migration tests in `scripts/validate-release-static.py`.
On MySQL, `TEXT` columns must use expression defaults such as `DEFAULT ('value')`.
-6
View File
@@ -1,6 +0,0 @@
CREATE TABLE IF NOT EXISTS user_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
INSERT OR IGNORE INTO user_meta (key, value) VALUES ('schema_version', '1');
-41
View File
@@ -1,41 +0,0 @@
-- Folder cache: stores the IMAP folder hierarchy
CREATE TABLE IF NOT EXISTS folders (
name TEXT PRIMARY KEY,
delimiter TEXT,
parent TEXT,
flags TEXT NOT NULL DEFAULT '',
is_subscribed INTEGER NOT NULL DEFAULT 1,
total_count INTEGER NOT NULL DEFAULT 0,
unread_count INTEGER NOT NULL DEFAULT 0,
uid_validity INTEGER NOT NULL DEFAULT 0,
highest_modseq INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Message header cache: stores synced email headers per folder
CREATE TABLE IF NOT EXISTS messages (
uid INTEGER NOT NULL,
folder TEXT NOT NULL,
message_id TEXT,
in_reply_to TEXT,
subject TEXT NOT NULL DEFAULT '',
from_address TEXT NOT NULL DEFAULT '',
from_name TEXT NOT NULL DEFAULT '',
to_addresses TEXT NOT NULL DEFAULT '',
cc_addresses TEXT NOT NULL DEFAULT '',
date TEXT NOT NULL,
flags TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
has_attachments INTEGER NOT NULL DEFAULT 0,
snippet TEXT NOT NULL DEFAULT '',
body_cached INTEGER NOT NULL DEFAULT 0,
body_html TEXT,
body_text TEXT,
references_header TEXT,
PRIMARY KEY (uid, folder),
FOREIGN KEY (folder) REFERENCES folders(name) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_messages_folder_date ON messages(folder, date DESC);
CREATE INDEX IF NOT EXISTS idx_messages_message_id ON messages(message_id);
CREATE INDEX IF NOT EXISTS idx_messages_in_reply_to ON messages(in_reply_to);
-6
View File
@@ -1,6 +0,0 @@
-- Add a sortable integer timestamp column for proper date ordering.
ALTER TABLE messages ADD COLUMN date_epoch INTEGER NOT NULL DEFAULT 0;
-- Rebuild index to use the new column for sorting.
DROP INDEX IF EXISTS idx_messages_folder_date;
CREATE INDEX idx_messages_folder_date ON messages(folder, date_epoch DESC);
@@ -1,3 +0,0 @@
-- Separate timestamp for when a folder's messages were last synced from IMAP,
-- distinct from `updated_at` which tracks when the folder list was last synced.
ALTER TABLE folders ADD COLUMN messages_updated_at TEXT;
@@ -1,24 +0,0 @@
CREATE TABLE IF NOT EXISTS drafts (
id TEXT PRIMARY KEY,
to_addresses TEXT NOT NULL DEFAULT '',
cc_addresses TEXT NOT NULL DEFAULT '',
bcc_addresses TEXT NOT NULL DEFAULT '',
subject TEXT NOT NULL DEFAULT '',
text_body TEXT NOT NULL DEFAULT '',
html_body TEXT,
in_reply_to TEXT,
references_header TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS draft_attachments (
id TEXT PRIMARY KEY,
draft_id TEXT NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
size INTEGER NOT NULL,
file_path TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (draft_id) REFERENCES drafts(id) ON DELETE CASCADE
);
@@ -1,3 +0,0 @@
-- Cache attachment metadata and raw headers alongside the message body.
ALTER TABLE messages ADD COLUMN attachments_json TEXT;
ALTER TABLE messages ADD COLUMN raw_headers TEXT;
-18
View File
@@ -1,18 +0,0 @@
CREATE TABLE IF NOT EXISTS contacts (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
company TEXT NOT NULL DEFAULT '',
notes TEXT NOT NULL DEFAULT '',
is_favorite INTEGER NOT NULL DEFAULT 0,
last_contacted TEXT,
contact_count INTEGER NOT NULL DEFAULT 0,
source TEXT NOT NULL DEFAULT 'manual',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_email ON contacts(email);
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
CREATE INDEX IF NOT EXISTS idx_contacts_last_contacted ON contacts(last_contacted DESC);
CREATE INDEX IF NOT EXISTS idx_contacts_contact_count ON contacts(contact_count DESC);
-9
View File
@@ -1,9 +0,0 @@
CREATE TABLE IF NOT EXISTS identities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
display_name TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL,
signature_html TEXT NOT NULL DEFAULT '',
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -1,9 +0,0 @@
CREATE TABLE IF NOT EXISTS notification_preferences (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
enabled INTEGER NOT NULL DEFAULT 1,
sound INTEGER NOT NULL DEFAULT 0,
folders TEXT NOT NULL DEFAULT '["INBOX"]',
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT OR IGNORE INTO notification_preferences (id) VALUES (1);
-16
View File
@@ -1,16 +0,0 @@
CREATE TABLE IF NOT EXISTS contact_groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS contact_group_members (
group_id TEXT NOT NULL,
contact_id TEXT NOT NULL,
PRIMARY KEY (group_id, contact_id),
FOREIGN KEY (group_id) REFERENCES contact_groups(id) ON DELETE CASCADE,
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_cgm_contact_id ON contact_group_members(contact_id);
-1
View File
@@ -1 +0,0 @@
ALTER TABLE messages ADD COLUMN reaction TEXT;
-20
View File
@@ -1,20 +0,0 @@
CREATE TABLE IF NOT EXISTS tags (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
color TEXT NOT NULL DEFAULT '#6b7280',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS message_tags (
tag_id TEXT NOT NULL,
message_uid INTEGER NOT NULL,
message_folder TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (tag_id, message_uid, message_folder),
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
FOREIGN KEY (message_uid, message_folder) REFERENCES messages(uid, folder) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_message_tags_message ON message_tags(message_uid, message_folder);
CREATE INDEX IF NOT EXISTS idx_message_tags_tag ON message_tags(tag_id);
-7
View File
@@ -1,7 +0,0 @@
CREATE TABLE IF NOT EXISTS display_preferences (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
density TEXT NOT NULL DEFAULT 'comfortable',
theme TEXT NOT NULL DEFAULT 'system',
language TEXT NOT NULL DEFAULT 'en',
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-1
View File
@@ -1 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN compose_format TEXT NOT NULL DEFAULT 'html';
-1
View File
@@ -1 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN deep_index BOOLEAN NOT NULL DEFAULT 0;
-51
View File
@@ -1,51 +0,0 @@
CREATE TABLE calendar_events (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
location TEXT NOT NULL DEFAULT '',
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
all_day INTEGER NOT NULL DEFAULT 0,
recurrence_rule TEXT,
attendees TEXT NOT NULL DEFAULT '[]',
organizer TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'confirmed',
source TEXT NOT NULL DEFAULT 'manual',
source_uid TEXT,
source_message_uid INTEGER,
source_message_folder TEXT,
meeting_url TEXT,
color TEXT,
reminder_minutes INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_calendar_events_time ON calendar_events(start_time, end_time);
CREATE INDEX idx_calendar_events_source_uid ON calendar_events(source_uid);
CREATE TABLE calendar_settings (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
default_view TEXT NOT NULL DEFAULT 'month',
week_starts_on INTEGER NOT NULL DEFAULT 0,
working_hours_start TEXT NOT NULL DEFAULT '09:00',
working_hours_end TEXT NOT NULL DEFAULT '17:00',
time_format TEXT NOT NULL DEFAULT '12h',
zoom_link TEXT NOT NULL DEFAULT '',
teams_link TEXT NOT NULL DEFAULT '',
meet_link TEXT NOT NULL DEFAULT '',
caldav_url TEXT NOT NULL DEFAULT '',
caldav_username TEXT NOT NULL DEFAULT '',
caldav_password TEXT NOT NULL DEFAULT '',
caldav_enabled INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE meeting_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url_template TEXT NOT NULL,
icon TEXT NOT NULL DEFAULT '',
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-4
View File
@@ -1,4 +0,0 @@
-- Add email_theme column to messages table
-- NULL = not yet detected (only relevant when body_html exists)
-- 0 = Light, 1 = Dark, 2 = Transparent
ALTER TABLE messages ADD COLUMN email_theme INTEGER;
-2
View File
@@ -1,2 +0,0 @@
ALTER TABLE display_preferences
ADD COLUMN animation_mode TEXT NULL;
-34
View File
@@ -1,34 +0,0 @@
-- Denormalized table of known email addresses extracted from message headers.
-- Avoids expensive json_each() scanning of to_addresses/cc_addresses columns.
CREATE TABLE IF NOT EXISTS known_addresses (
email TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
PRIMARY KEY (email)
);
CREATE INDEX IF NOT EXISTS idx_known_addresses_name ON known_addresses(name);
-- Backfill from existing messages: from_address
INSERT INTO known_addresses (email, name)
SELECT from_address, COALESCE(from_name, '')
FROM messages
WHERE from_address != ''
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != '';
-- Backfill from existing messages: to_addresses (JSON array)
INSERT INTO known_addresses (email, name)
SELECT value->>'address', COALESCE(value->>'name', '')
FROM messages, json_each(to_addresses)
WHERE json_valid(to_addresses)
AND value->>'address' IS NOT NULL
AND value->>'address' != ''
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != '';
-- Backfill from existing messages: cc_addresses (JSON array)
INSERT INTO known_addresses (email, name)
SELECT value->>'address', COALESCE(value->>'name', '')
FROM messages, json_each(cc_addresses)
WHERE json_valid(cc_addresses)
AND value->>'address' IS NOT NULL
AND value->>'address' != ''
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != '';
@@ -1,17 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN preview_pane TEXT NOT NULL DEFAULT 'right';
ALTER TABLE display_preferences ADD COLUMN ui_scale TEXT NOT NULL DEFAULT 'normal';
ALTER TABLE display_preferences ADD COLUMN color_preset TEXT NOT NULL DEFAULT 'sage';
ALTER TABLE display_preferences ADD COLUMN accent_color TEXT NOT NULL DEFAULT '#6f9c86';
ALTER TABLE display_preferences ADD COLUMN font_family TEXT NOT NULL DEFAULT 'system';
ALTER TABLE display_preferences ADD COLUMN font_size INTEGER NOT NULL DEFAULT 13;
ALTER TABLE display_preferences ADD COLUMN row_height INTEGER NOT NULL DEFAULT 58;
ALTER TABLE display_preferences ADD COLUMN folder_pane_width INTEGER NOT NULL DEFAULT 228;
ALTER TABLE display_preferences ADD COLUMN message_pane_width INTEGER NOT NULL DEFAULT 420;
ALTER TABLE display_preferences ADD COLUMN border_radius INTEGER NOT NULL DEFAULT 4;
ALTER TABLE display_preferences ADD COLUMN contrast TEXT NOT NULL DEFAULT 'normal';
ALTER TABLE display_preferences ADD COLUMN show_avatars INTEGER NOT NULL DEFAULT 1;
ALTER TABLE display_preferences ADD COLUMN show_attachment_icons INTEGER NOT NULL DEFAULT 1;
ALTER TABLE display_preferences ADD COLUMN show_flags INTEGER NOT NULL DEFAULT 1;
ALTER TABLE display_preferences ADD COLUMN reduce_motion INTEGER NOT NULL DEFAULT 0;
ALTER TABLE display_preferences ADD COLUMN date_format TEXT NOT NULL DEFAULT 'locale';
ALTER TABLE display_preferences ADD COLUMN time_format TEXT NOT NULL DEFAULT 'locale';
-5
View File
@@ -1,5 +0,0 @@
CREATE TABLE IF NOT EXISTS special_folders (
role TEXT PRIMARY KEY CHECK(role IN ('sent','drafts','trash','junk','archive')),
folder_name TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-8
View File
@@ -1,8 +0,0 @@
ALTER TABLE identities ADD COLUMN reply_to TEXT NOT NULL DEFAULT '';
ALTER TABLE identities ADD COLUMN bcc TEXT NOT NULL DEFAULT '';
ALTER TABLE identities ADD COLUMN organization TEXT NOT NULL DEFAULT '';
ALTER TABLE identities ADD COLUMN signature_text TEXT NOT NULL DEFAULT '';
ALTER TABLE identities ADD COLUMN signature_new INTEGER NOT NULL DEFAULT 1;
ALTER TABLE identities ADD COLUMN signature_reply INTEGER NOT NULL DEFAULT 1;
ALTER TABLE identities ADD COLUMN signature_forward INTEGER NOT NULL DEFAULT 1;
ALTER TABLE identities ADD COLUMN signature_position TEXT NOT NULL DEFAULT 'above';
-9
View File
@@ -1,9 +0,0 @@
CREATE TABLE IF NOT EXISTS saved_searches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
query_string TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_saved_searches_name ON saved_searches(name COLLATE NOCASE);
-3
View File
@@ -1,3 +0,0 @@
ALTER TABLE notification_preferences ADD COLUMN dnd_enabled INTEGER NOT NULL DEFAULT 0;
ALTER TABLE notification_preferences ADD COLUMN dnd_start TEXT NOT NULL DEFAULT '22:00';
ALTER TABLE notification_preferences ADD COLUMN dnd_end TEXT NOT NULL DEFAULT '07:00';
-3
View File
@@ -1,3 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN background_color TEXT NOT NULL DEFAULT '#121513';
ALTER TABLE display_preferences ADD COLUMN panel_color TEXT NOT NULL DEFAULT '#181c19';
ALTER TABLE display_preferences ADD COLUMN text_color TEXT NOT NULL DEFAULT '#edf0ec';
-14
View File
@@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS mail_preferences (
id INTEGER PRIMARY KEY CHECK (id = 1),
messages_per_page INTEGER NOT NULL DEFAULT 50,
mark_read_delay_ms INTEGER NOT NULL DEFAULT 0,
remote_images TEXT NOT NULL DEFAULT 'ask',
preferred_body TEXT NOT NULL DEFAULT 'auto',
thread_mode TEXT NOT NULL DEFAULT 'threaded',
collapse_quotes INTEGER NOT NULL DEFAULT 1,
show_snippets INTEGER NOT NULL DEFAULT 1,
default_sort TEXT NOT NULL DEFAULT 'date_desc',
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT OR IGNORE INTO mail_preferences (id) VALUES (1);
-13
View File
@@ -1,13 +0,0 @@
ALTER TABLE contacts ADD COLUMN phone TEXT NOT NULL DEFAULT '';
ALTER TABLE contacts ADD COLUMN job_title TEXT NOT NULL DEFAULT '';
ALTER TABLE contacts ADD COLUMN address TEXT NOT NULL DEFAULT '';
ALTER TABLE contacts ADD COLUMN website TEXT NOT NULL DEFAULT '';
ALTER TABLE contacts ADD COLUMN birthday TEXT NOT NULL DEFAULT '';
CREATE TABLE IF NOT EXISTS contact_photos (
contact_id TEXT PRIMARY KEY,
content_type TEXT NOT NULL,
data BLOB NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE
);
@@ -1,3 +0,0 @@
ALTER TABLE contacts ADD COLUMN emails_json TEXT NOT NULL DEFAULT '[]';
ALTER TABLE contacts ADD COLUMN phones_json TEXT NOT NULL DEFAULT '[]';
ALTER TABLE contacts ADD COLUMN addresses_json TEXT NOT NULL DEFAULT '[]';
-2
View File
@@ -1,2 +0,0 @@
ALTER TABLE drafts ADD COLUMN request_mdn INTEGER NOT NULL DEFAULT 0;
ALTER TABLE drafts ADD COLUMN priority TEXT NOT NULL DEFAULT 'normal';
@@ -1,7 +0,0 @@
ALTER TABLE mail_preferences ADD COLUMN flagged_on_top INTEGER NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS folder_preferences (
folder_name TEXT PRIMARY KEY REFERENCES folders(name) ON UPDATE CASCADE ON DELETE CASCADE,
flagged_on_top INTEGER CHECK(flagged_on_top IN (0, 1) OR flagged_on_top IS NULL),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-4
View File
@@ -1,4 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN compose_autosave_ms INTEGER NOT NULL DEFAULT 3000;
ALTER TABLE display_preferences ADD COLUMN default_priority TEXT NOT NULL DEFAULT 'normal';
ALTER TABLE display_preferences ADD COLUMN request_mdn_default INTEGER NOT NULL DEFAULT 0;
ALTER TABLE display_preferences ADD COLUMN attachment_reminder INTEGER NOT NULL DEFAULT 1;
-15
View File
@@ -1,15 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN icon_size INTEGER NOT NULL DEFAULT 18;
ALTER TABLE display_preferences ADD COLUMN toolbar_density TEXT NOT NULL DEFAULT 'normal';
ALTER TABLE display_preferences ADD COLUMN sidebar_item_height INTEGER NOT NULL DEFAULT 29;
ALTER TABLE display_preferences ADD COLUMN reader_max_width INTEGER NOT NULL DEFAULT 0;
ALTER TABLE display_preferences ADD COLUMN composer_width INTEGER NOT NULL DEFAULT 900;
ALTER TABLE display_preferences ADD COLUMN selection_color TEXT NOT NULL DEFAULT '#28352e';
ALTER TABLE display_preferences ADD COLUMN unread_font_weight INTEGER NOT NULL DEFAULT 730;
ALTER TABLE display_preferences ADD COLUMN unread_badge_style TEXT NOT NULL DEFAULT 'dot';
ALTER TABLE display_preferences ADD COLUMN compact_headers INTEGER NOT NULL DEFAULT 0;
ALTER TABLE display_preferences ADD COLUMN show_borders INTEGER NOT NULL DEFAULT 1;
ALTER TABLE display_preferences ADD COLUMN scrollbar_density TEXT NOT NULL DEFAULT 'thin';
ALTER TABLE display_preferences ADD COLUMN monochrome INTEGER NOT NULL DEFAULT 0;
ALTER TABLE display_preferences ADD COLUMN folder_background TEXT NOT NULL DEFAULT '';
ALTER TABLE display_preferences ADD COLUMN message_background TEXT NOT NULL DEFAULT '';
ALTER TABLE display_preferences ADD COLUMN reader_background TEXT NOT NULL DEFAULT '';
@@ -1 +0,0 @@
ALTER TABLE drafts ADD COLUMN compose_format TEXT NOT NULL DEFAULT 'html';
-4
View File
@@ -1,4 +0,0 @@
ALTER TABLE mail_preferences ADD COLUMN sync_imap_keywords INTEGER NOT NULL DEFAULT 0;
ALTER TABLE tags ADD COLUMN imap_keyword TEXT NOT NULL DEFAULT '';
UPDATE tags SET imap_keyword = 'RustBox-' || replace(id, '-', '') WHERE imap_keyword = '';
CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_imap_keyword ON tags(imap_keyword) WHERE imap_keyword <> '';
@@ -1,5 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN compose_font_family TEXT NOT NULL DEFAULT 'system';
ALTER TABLE display_preferences ADD COLUMN reply_position TEXT NOT NULL DEFAULT 'above';
ALTER TABLE display_preferences ADD COLUMN quote_style TEXT NOT NULL DEFAULT 'standard';
ALTER TABLE display_preferences ADD COLUMN forward_mode TEXT NOT NULL DEFAULT 'inline';
ALTER TABLE display_preferences ADD COLUMN spellcheck_enabled INTEGER NOT NULL DEFAULT 1;
@@ -1,3 +0,0 @@
ALTER TABLE drafts ADD COLUMN remote_folder TEXT;
ALTER TABLE drafts ADD COLUMN remote_uid INTEGER;
ALTER TABLE drafts ADD COLUMN remote_uid_validity INTEGER;
-24
View File
@@ -1,24 +0,0 @@
-- OpenPGP keyring metadata. Secret key material is stored outside SQLite.
CREATE TABLE IF NOT EXISTS pgp_keys (
fingerprint TEXT PRIMARY KEY,
key_id TEXT NOT NULL,
email TEXT NOT NULL DEFAULT '',
user_id TEXT NOT NULL DEFAULT '',
public_key_armor TEXT NOT NULL,
secret_key_path TEXT,
has_secret INTEGER NOT NULL DEFAULT 0,
trust TEXT NOT NULL DEFAULT 'unknown',
source TEXT NOT NULL DEFAULT 'import',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_pgp_keys_email ON pgp_keys(lower(email));
CREATE TABLE IF NOT EXISTS pgp_preferences (
id INTEGER PRIMARY KEY CHECK (id = 1),
wkd_enabled INTEGER NOT NULL DEFAULT 1,
hkp_enabled INTEGER NOT NULL DEFAULT 0,
require_trusted_encryption INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT OR IGNORE INTO pgp_preferences (id) VALUES (1);
-4
View File
@@ -1,4 +0,0 @@
ALTER TABLE identities ADD COLUMN pgp_fingerprint TEXT;
ALTER TABLE identities ADD COLUMN pgp_sign_default INTEGER NOT NULL DEFAULT 0;
ALTER TABLE identities ADD COLUMN pgp_encrypt_default INTEGER NOT NULL DEFAULT 0;
ALTER TABLE identities ADD COLUMN pgp_inline_default INTEGER NOT NULL DEFAULT 0;
Loaded 100 of 264 files, more files were not shown because too many files have changed in this diff. Show more