v0.2.16
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-02 23:32:23 +01:00
1 parent d91212d24c
commit dbe7103687
21 files changed
+182 -154

No files matched your search

+9 -4
View File
@@ -1,5 +1,13 @@
# RustBox changelog
### 0.2.16
- Added opt-in adaptive HTML-mail colors that adjust neutral backgrounds and low-contrast text to the active light/dark theme while preserving layout, images and brand colors.
- Added mailbox pagination, enabled by default at 300 messages per page, with optional smooth progressive loading or full-folder loading.
- Added runtime schema migration V051 for SQLite, MySQL and PostgreSQL. Existing V050 databases gain `display_preferences.adaptive_email_colors` and `mail_preferences.message_loading_mode` before the new preferences are queried.
- Kept V050 as the consolidated compatibility baseline and removed the standalone V050 migration files because those changes are already present in the complete `schema.sql` definitions.
- Bumped the application/source release to 0.2.16.
### 0.2.15
- Fixed HTML signature editor container semantics so clicks on whitespace/outside the editable area no longer implicitly activate the Bold toolbar button.
@@ -7,10 +15,7 @@
- Fixed visible signatures in new messages by creating an editable body entry before the signature instead of placing the signature at the top of an otherwise empty editor.
- Refined the visible-signature separator with more breathing room and a subtler solid rule so it reads as a boundary without dominating the composer.
- Strips the editor-only body-entry marker from outgoing HTML while retaining the visible-signature marker used for de-duplication.
- Added an opt-in Appearance setting that adapts neutral HTML-mail backgrounds and low-contrast text to the active light/dark theme while preserving layout, images and brand colors.
- Consolidated migrations V047-V049 into the V049 compatibility baseline and removed their standalone migration files. V050 remains the single runtime migration for the unreleased 0.2.15 schema changes. Databases older than V049 must be upgraded to that baseline first.
- Added mailbox pagination, enabled by default at 300 messages per page, with optional smooth progressive loading or full-folder loading.
- Folded `display_preferences.adaptive_email_colors` and `mail_preferences.message_loading_mode` into the unreleased V050 runtime migration; V051 remains the next migration number.
- Consolidated migrations V047-V050 into the V050 compatibility baseline and removed their standalone migration files. Databases older than V050 must be upgraded through a V050-capable release first.
- Bumped the application/source release to 0.2.15.
### 0.2.14
Generated
+1 -1
View File
@@ -3506,7 +3506,7 @@ dependencies = [
[[package]]
name = "rustbox"
version = "0.2.15"
version = "0.2.16"
dependencies = [
"aes-gcm",
"async-imap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rustbox"
version = "0.2.15"
version = "0.2.16"
edition = "2024"
rust-version = "1.99.1"
default-run = "rustbox"
+1 -1
View File
@@ -1,6 +1,6 @@
# RustBox attribution / notice
RustBox 0.2.15 is derived from the Oxi webmail project:
RustBox 0.2.16 is derived from the Oxi webmail project:
- Upstream: `c0h1b4/oxi`
- Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d`
+4 -4
View File
@@ -1,4 +1,4 @@
# RustBox 0.2.15
# RustBox 0.2.16
RustBox is a webmail derived from `c0h1b4/oxi`. The consumed Oxi revision is recorded in `UPSTREAM_BASE`.
@@ -34,7 +34,7 @@ rustbox/
`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`.
## Storage and search in 0.2.15
## Storage and search in 0.2.16
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.
@@ -168,7 +168,7 @@ rustbox search reindex
Ready Docker examples are in `docker/`: standalone SQLite, PostgreSQL + SILO (S3-compatible) + OpenSearch, and MySQL + NFS + OpenSearch.
All executable application SQL is centralized by dialect under `src/queries/{sqlite,mysql,pgsql}/queries.rs`. Migrations V047-V049 are consolidated into the V049 compatibility baseline. V050 remains the single runtime migration and contains compact rows, signature insertion mode, adaptive HTML-mail colors and mailbox loading mode. Databases older than V049 must first be upgraded to that baseline. V051 is the next migration number. See `migrations/README.md`.
All executable application SQL is centralized by dialect under `src/queries/{sqlite,mysql,pgsql}/queries.rs`. Migrations V047-V050 are consolidated into the V050 compatibility baseline; databases older than V050 must first be upgraded with a release that can reach that baseline. RustBox 0.2.16 adds V051 for adaptive HTML-mail colors and mailbox loading mode; fresh schemas already contain those columns, while existing V050 tenants migrate automatically to V051. V052 is the next migration number. See `migrations/README.md`.
## Development
@@ -237,7 +237,7 @@ Initialize Git after extracting the release archive:
```bash
git init
git add .
git commit -m "RustBox 0.2.15 release"
git commit -m "RustBox 0.2.16 release"
./scripts/setup-upstream.sh
```
+9 -9
View File
@@ -1,6 +1,6 @@
RustBox 0.2.15 release manifest
RustBox 0.2.16 release manifest
0.2.15 release changes and retained feature set:
0.2.16 release changes and retained feature set:
- 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
@@ -30,7 +30,7 @@ RustBox 0.2.15 release manifest
- 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 and folds both it and mailbox loading mode into the unreleased V050 runtime migration; V051 remains next
- 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
@@ -54,10 +54,10 @@ Search:
Database/schema:
- migrations/{sqlite,mysql,pgsql}/schema.sql contain the current fresh schema
- compatibility baseline is V049; former V047-V049 changes are consolidated into `schema.sql`, while V050 remains the single runtime migration
- databases older than V049 must be upgraded to schema V049 before using this build
- V050 contains `mail_preferences.compact_wide_rows`, `identities.signature_insert_mode`, `display_preferences.adaptive_email_colors` and `mail_preferences.message_loading_mode` because this release has not shipped yet
- next migration number: V051
- compatibility baseline is V050; former V047-V050 changes are consolidated into `schema.sql` and their standalone migrations are removed
- databases older than V050 must be upgraded with a V050-capable RustBox release before using this build
- V051 adds `display_preferences.adaptive_email_colors` and `mail_preferences.message_loading_mode` to existing V050 tenants
- next migration number: V052
Docker examples:
- docker/compose-single-sqlite.yml
@@ -67,14 +67,14 @@ Docker examples:
Operations/docs:
- standalone `scripts/migrate_roundcube.py` imports Roundcube contacts/identities using RustBox config.toml and supports SQLite/MySQL/PostgreSQL targets
- README.md documents 0.2.15 configuration
- README.md documents 0.2.16 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.15
- Cargo.toml version = 0.2.16
- 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
+6 -6
View File
@@ -1,6 +1,6 @@
RustBox 0.2.15 - final source validation
RustBox 0.2.16 - final source validation
0.2.15 scope:
0.2.16 scope:
- 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
@@ -27,8 +27,8 @@ RustBox 0.2.15 - final source validation
- 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-V049 are consolidated into compatibility baseline V049; V050 is the single runtime migration and includes adaptive HTML-mail colors and mailbox loading mode, with V051 next
- version metadata updated to 0.2.15
- V047-V050 are consolidated into compatibility baseline V050; V051 migrates existing tenants with adaptive HTML-mail colors and mailbox loading mode; V052 is next
- version metadata updated to 0.2.16
Completed in this environment:
- offline release validator: scripts/validate-release-static.py (421/421 checks passed)
@@ -36,8 +36,8 @@ Completed in this environment:
- all JSON files parse successfully
- Python helper scripts compile successfully
- shell scripts pass `bash -n`
- release metadata consistently identifies 0.2.15
- V049 baseline consolidation plus the single V050 runtime migration, adaptive colors and mailbox loading mode are validated offline
- release metadata consistently identifies 0.2.16
- V050 baseline consolidation plus the V051 adaptive-colors/mailbox-loading migration are validated offline
Not executable in this packaging environment:
+1 -1
View File
@@ -1,4 +1,4 @@
# RustBox 0.2.15
# RustBox 0.2.16
host = "127.0.0.1"
port = 3001
environment = "production"
+5 -5
View File
@@ -1,9 +1,9 @@
# RustBox 0.2.15 - implementation status
# RustBox 0.2.16 - implementation status
- Draft saves are realtime-silent: Drafts folder/list updates continue, but saving a draft does not surface as a new-mail notification.
This file tracks the `0.2.15` source snapshot. Storage/search scope and outstanding acceptance work are tracked in `TODO.md`. A feature is only listed as implemented when working code exists in this source tree; partial items are called out explicitly.
This file tracks the `0.2.16` 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.15 browser mail UX
## 0.2.16 browser mail UX
- 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.
@@ -28,7 +28,7 @@ This file tracks the `0.2.15` source snapshot. Storage/search scope and outstand
- 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-V049 are consolidated into the V049 compatibility baseline. V050 remains the single runtime migration and includes compact rows, signature insertion mode, adaptive HTML-mail colors and mailbox loading mode; V051 is next.
- V047-V050 are consolidated into the V050 compatibility baseline. RustBox 0.2.16 uses V051 to add adaptive HTML-mail colors and mailbox loading mode to existing V050 tenants; V052 is next.
## 0.2.8 browser security headers
@@ -48,7 +48,7 @@ This file tracks the `0.2.15` source snapshot. Storage/search scope and outstand
- 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 V049 compatibility baseline includes the former V047 attachment/search-outbox, V048 folder counter generation and V049 blob deletion outbox changes. V050 is the single runtime migration for compact-row/signature preferences, adaptive HTML-mail colors and mailbox loading mode. V051 is reserved as the next migration number.
- 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.
+1 -1
View File
@@ -26,7 +26,7 @@ RUSTBOX_CONFIG=./config.dev.toml ./target/release/rustbox --check-config
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.15
## 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.
+2 -2
View File
@@ -1,6 +1,6 @@
# RustBox 0.2.15 storage/search operations
# RustBox 0.2.16 storage/search operations
RustBox 0.2.15 treats SQL, blob storage and search as independent capabilities. The database engine does not decide whether `data_dir` is allowed.
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
+22 -29
View File
@@ -1,42 +1,35 @@
# Database schemas and migrations
# RustBox database schema and migrations
RustBox keeps SQL, blob storage and search as independent concerns.
RustBox keeps one complete fresh-install schema per supported SQL backend:
```text
database_backend = sqlite | mysql | pgsql
session_backend = database | redis
storage.backend = filesystem | s3
search.backend = disabled | local_tantivy | opensearch
```
- `sqlite/schema.sql`
- `mysql/schema.sql`
- `pgsql/schema.sql`
## Layout
## Compatibility baseline
```text
migrations/
├── sqlite/schema.sql
├── mysql/schema.sql
└── pgsql/schema.sql
```
The consolidated compatibility baseline is **V050**. Historical migrations V047-V050 are already represented by the complete `schema.sql` files and are intentionally not shipped as standalone runtime migrations in this source tree.
Each `schema.sql` is the complete V050 schema for fresh installations. Migrations V047-V049 have been consolidated into the **V049 compatibility baseline** and their standalone files are intentionally removed. V050 remains the single runtime migration. Because RustBox 0.2.15 has not been released yet, V050 includes `mail_preferences.compact_wide_rows`, `identities.signature_insert_mode`, `display_preferences.adaptive_email_colors` and `mail_preferences.message_loading_mode`.
Databases older than V050 must first be upgraded with a RustBox release that can reach V050. RustBox 0.2.16 accepts V050 databases and migrates them forward normally.
`mail_preferences.message_loading_mode` defaults to `pagination`; `messages_per_page` defaults to 300. Supported modes are `pagination`, `progressive` and `all`.
## V051 — RustBox 0.2.16
Databases older than **V049** are no longer migrated directly by this build. Upgrade them to schema V049 first; this build then applies V050. **V051** is the next migration number after this snapshot.
V051 adds the preferences introduced in 0.2.16:
Account data is isolated inside the selected physical database with a deterministic, collision-checked table prefix derived from the account hash. The `{{p}}` marker in tenant SQL is replaced by RustBox at provisioning time. Sessions are global and therefore are not prefixed.
- `display_preferences.adaptive_email_colors`, default `0` / disabled;
- `mail_preferences.message_loading_mode`, default `pagination`.
## Runtime migrations
`mail_preferences.messages_per_page` remains `300` by default. Supported message loading modes are `pagination`, `progressive` and `all`.
V050 is the only runtime migration in this unreleased snapshot. Future schema-changing releases must add the same logical migration for SQLite, MySQL and PostgreSQL and register it in `src/db/pool.rs`.
Fresh installations already contain the V051 columns in `schema.sql` and are registered at the latest schema version. Existing V050 tenants execute `V051__adaptive_email_colors_and_message_loading_mode.sql` before application queries use the new columns.
Redis and Redis Sentinel do not use SQL migrations.
The next migration number is **V052**.
## Engine notes
## Migration rules
- SQLite uses `data_dir/rustbox.sqlite` by default; `database_url` may point to another persistent SQLite file.
- MySQL requires `database_url` and targets MySQL 8.0.13 or newer.
- PostgreSQL requires `database_url`.
- MySQL/PostgreSQL are full application backends, not session-only databases.
- The SQL engine does not choose blob/search storage. PostgreSQL/MySQL may still use local filesystem/Tantivy in single-node mode.
- Cluster mode requires a shared MySQL/PostgreSQL database plus cluster-safe blob/search choices; local `data_dir` is not inherently required.
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`.
@@ -3,9 +3,3 @@ ALTER TABLE {{p}}display_preferences
ALTER TABLE {{p}}mail_preferences
ADD COLUMN message_loading_mode TEXT NOT NULL DEFAULT 'pagination';
ALTER TABLE {{p}}mail_preferences
ADD COLUMN compact_wide_rows BIGINT NOT NULL DEFAULT 1;
ALTER TABLE {{p}}identities
ADD COLUMN signature_insert_mode TEXT NOT NULL DEFAULT 'silent';
+1 -1
View File
@@ -1,7 +1,7 @@
-- RustBox 0.2.0 MySQL schema.
-- One physical database contains global sessions plus isolated per-account tables.
-- Application tables are materialized by replacing {{p}} with a collision-checked prefix.
-- Squashed compatibility baseline: V049. Current schema: V050.
-- Squashed compatibility baseline: V050; current schema: V051.
-- RUSTBOX:GLOBAL-BEGIN
CREATE TABLE IF NOT EXISTS rustbox_accounts (
@@ -3,9 +3,3 @@ ALTER TABLE {{p}}display_preferences
ALTER TABLE {{p}}mail_preferences
ADD COLUMN message_loading_mode TEXT NOT NULL DEFAULT 'pagination';
ALTER TABLE {{p}}mail_preferences
ADD COLUMN compact_wide_rows BIGINT NOT NULL DEFAULT 1;
ALTER TABLE {{p}}identities
ADD COLUMN signature_insert_mode VARCHAR(16) NOT NULL DEFAULT 'silent';
+1 -1
View File
@@ -1,7 +1,7 @@
-- RustBox 0.2.0 PostgreSQL schema.
-- One physical database contains global sessions plus isolated per-account tables.
-- Application tables are materialized by replacing {{p}} with a collision-checked prefix.
-- Squashed compatibility baseline: V049. Current schema: V050.
-- Squashed compatibility baseline: V050; current schema: V051.
-- RUSTBOX:GLOBAL-BEGIN
CREATE TABLE IF NOT EXISTS rustbox_accounts (
@@ -3,9 +3,3 @@ ALTER TABLE {{p}}display_preferences
ALTER TABLE {{p}}mail_preferences
ADD COLUMN message_loading_mode TEXT NOT NULL DEFAULT 'pagination';
ALTER TABLE {{p}}mail_preferences
ADD COLUMN compact_wide_rows INTEGER NOT NULL DEFAULT 1;
ALTER TABLE {{p}}identities
ADD COLUMN signature_insert_mode TEXT NOT NULL DEFAULT 'silent';
+1 -1
View File
@@ -1,7 +1,7 @@
-- RustBox 0.2.0 SQLite schema.
-- One physical database contains global sessions plus isolated per-account tables.
-- Application tables are materialized from the tenant section by replacing {{p}}.
-- Squashed compatibility baseline: V049. Current schema: V050.
-- Squashed compatibility baseline: V050; current schema: V051.
-- RUSTBOX:GLOBAL-BEGIN
CREATE TABLE IF NOT EXISTS rustbox_accounts (
+1 -1
View File
@@ -1,4 +1,4 @@
# RustBox 0.2.15 - packaged production configuration (DEB/RPM)
# RustBox 0.2.16 - packaged production configuration (DEB/RPM)
#
# This file contains all supported RustBox configuration options.
# Commented options are optional and do not change the packaged defaults.
+63 -59
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Static/offline release checks for RustBox 0.2.15.
"""Static/offline release checks for RustBox 0.2.16.
These checks intentionally avoid pretending to replace `cargo test`: they validate
configuration, SQL migrations, Docker examples, query-catalog parity and release
@@ -49,14 +49,14 @@ def extract_section(text: str, begin: str, end: str) -> str:
# Release/toolchain metadata.
cargo = tomllib.loads(read("Cargo.toml"))
require(cargo["package"]["version"] == "0.2.15", "Cargo package version is 0.2.15")
require(cargo["package"]["version"] == "0.2.16", "Cargo package version is 0.2.16")
require(cargo["package"]["rust-version"] == "1.99.1", "Cargo rust-version is 1.99.1")
toolchain = tomllib.loads(read("rust-toolchain.toml"))
require(toolchain["toolchain"]["channel"] == "1.99.1", "rust-toolchain pins Rust 1.99.1")
require((ROOT / "Cargo.lock").is_file(), "Cargo.lock is retained for reproducible application builds")
lock = tomllib.loads(read("Cargo.lock"))
lock_rustbox = next((pkg for pkg in lock.get("package", []) if pkg.get("name") == "rustbox"), {})
require(lock_rustbox.get("version") == "0.2.15", "Cargo.lock RustBox package version is 0.2.15")
require(lock_rustbox.get("version") == "0.2.16", "Cargo.lock RustBox package version is 0.2.16")
require(cargo["dependencies"].get("encoding_rs") == "0.8", "direct charset decoder dependency is pinned")
require("encoding_rs" in lock_rustbox.get("dependencies", []), "Cargo.lock records direct charset decoder dependency")
require("cargo generate-lockfile" in read("scripts/build-release.sh"), "release script generates Cargo.lock when missing")
@@ -71,10 +71,10 @@ require("make_zip.py" in package_script and "sha256sum" in package_script, "rele
require("cargo generate-lockfile" in read(".github/workflows/ci.yml"), "CI regenerates Cargo.lock before --locked checks")
require("validate-release-static.py" in read("scripts/build-release.sh"), "release script runs offline/static release gate")
require("validate-release-static.py" in read(".github/workflows/ci.yml"), "CI runs offline/static release gate")
require(read("README.md").startswith("# RustBox 0.2.15"), "README identifies RustBox 0.2.15")
require(read("RELEASE-MANIFEST.txt").startswith("RustBox 0.2.15"), "release manifest identifies RustBox 0.2.15")
require(read("RELEASE-VALIDATION.txt").startswith("RustBox 0.2.15"), "release validation identifies RustBox 0.2.15")
require(read("docs/IMPLEMENTATION_STATUS.md").startswith("# RustBox 0.2.15"), "implementation status identifies RustBox 0.2.15")
require(read("README.md").startswith("# RustBox 0.2.16"), "README identifies RustBox 0.2.16")
require(read("RELEASE-MANIFEST.txt").startswith("RustBox 0.2.16"), "release manifest identifies RustBox 0.2.16")
require(read("RELEASE-VALIDATION.txt").startswith("RustBox 0.2.16"), "release validation identifies RustBox 0.2.16")
require(read("docs/IMPLEMENTATION_STATUS.md").startswith("# RustBox 0.2.16"), "implementation status identifies RustBox 0.2.16")
bootstrap_js = read("src/web/js/00-bootstrap.js")
bindings_js = read("src/web/js/95-bindings-boot.js")
realtime_js = read("src/web/js/80-settings-realtime.js")
@@ -111,7 +111,6 @@ require('id="compose-save-draft"' in read("src/web/index.html") and '$("#compose
require('rememberSignatureRange' in bindings_js and 'selectedSignatureRange' in bindings_js and 'if (!range || range.collapsed) return null;' in bindings_js and 'state.identitySignatureRange = null;' in bindings_js and '.signature-editor-toolbar button' in bindings_js and 'event.preventDefault();' in bindings_js and 'restoreEditorRange(range, signatureEditor);' in bindings_js, "signature formatting preserves real selections and refuses collapsed-caret commands that would enable sticky typing styles")
index_html = read("src/web/index.html")
require('<div class="folder-quota-label"><span>Mailbox storage</span>' in index_html and '<span id="mail-server-label">RustBox</span><span>__RUSTBOX_VERSION__</span>' in index_html, "mailbox storage and footer labels stay protocol-neutral")
require('<div class="identity-signature-field"><span id="identity-signature-label">HTML signature / footer</span>' in index_html and 'aria-labelledby="identity-signature-label"' in index_html and '<label class="identity-signature-field">' not in index_html, "HTML signature editor is not wrapped in a label that can implicitly activate the Bold toolbar button")
require('function ensureComposeEntryBeforeSignature' in read("src/web/js/72-openpgp-identities.js") and 'data-rustbox-signature-entry' in read("src/web/js/72-openpgp-identities.js") and 'compose-body-entry' in compose_css, "new visible signatures keep an editable compose body entry above the signature")
require('.compose-signature.compose-signature-new' in compose_css and 'margin-top: 2.4em;' in compose_css and 'border-top: 1px solid color-mix(in srgb, var(--line) 58%, transparent);' in compose_css and 'removeAttribute("data-rustbox-signature-entry")' in compose_js, "new-message signature spacing is scoped, the separator is subdued, and editor-only scaffolding is stripped from outgoing HTML")
@@ -141,8 +140,8 @@ require('encoding_rs::Encoding::for_label' in read("src/imap/parse.rs") and 'dec
require('let is_pdf = response_content_type_is(&response, "application/pdf");' in read("src/routes/mod.rs") and 'HeaderValue::from_static("SAMEORIGIN")' in read("src/routes/mod.rs") and "HeaderValue::from_static(\"frame-ancestors 'self'\")" in read("src/routes/mod.rs"), "security headers allow same-origin framing for inline PDF previews only")
index_html = read("src/web/index.html")
require('class="compose-limit-summary"' in index_html and 'Per-file upload limit set with' not in index_html and 'Hard limit for the complete RFC822/MIME message' not in index_html, "Composing shows compact raw size limits without implementation details")
require("V049" in read("RELEASE-MANIFEST.txt") and "V050" in read("RELEASE-MANIFEST.txt") and "V051" in read("RELEASE-MANIFEST.txt") and "V052" not in read("RELEASE-MANIFEST.txt"), "release manifest documents V049 baseline, V050 current migration and next V051")
require("V049" in read("migrations/README.md") and "V050" in read("migrations/README.md") and "V051" in read("migrations/README.md") and "V052" not in read("migrations/README.md"), "migration guide documents V049 baseline, V050 current migration and next V051")
require("V050" in read("RELEASE-MANIFEST.txt") and "V051" in read("RELEASE-MANIFEST.txt") and "V052" in read("RELEASE-MANIFEST.txt"), "release manifest documents V050 baseline, active V051 and next V052")
require("V050" in read("migrations/README.md") and "V051" in read("migrations/README.md") and "V052" in read("migrations/README.md"), "migration guide documents V050 baseline, active V051 and next V052")
require("versions = [50]" in read("scripts/migrate_roundcube.py"), "Roundcube provisioning fallback follows the V050 consolidated baseline")
require((ROOT / "docs/STORAGE_SEARCH_OPERATIONS.md").is_file(), "storage/search operations guide exists")
require('imap_host = ""' not in read("config.dev.toml"), "development config does not override IMAP host with an invalid empty value")
@@ -552,64 +551,33 @@ try:
require(sqlite_generation == ("2",), "SQLite header-sync UPSERT remains unambiguous and increments generation")
account_default = conn.execute("PRAGMA table_info(rustbox_accounts)").fetchall()
version_default = next((row[4] for row in account_default if row[1] == "schema_version"), None)
require(str(version_default).strip("'\"") == "50", "fresh account schema version defaults to current V050")
require(str(version_default).strip("'\"") == "50", "fresh global compatibility baseline is V050 before tenant registration")
display_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__display_preferences)")}
require("adaptive_email_colors" in display_columns and str(display_columns["adaptive_email_colors"][4]) == "0", "V050 defaults adaptive HTML-mail colors off")
require("adaptive_email_colors" in display_columns and str(display_columns["adaptive_email_colors"][4]) == "0", "fresh V051 schema defaults adaptive HTML-mail colors off")
mail_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__mail_preferences)")}
require("message_loading_mode" in mail_columns and str(mail_columns["message_loading_mode"][4]).strip("\'\"") == "pagination", "V050 defaults mailbox loading mode to pagination")
require("message_loading_mode" in mail_columns and str(mail_columns["message_loading_mode"][4]).strip("\'\"") == "pagination", "fresh V051 schema defaults mailbox loading mode to pagination")
conn.close()
except Exception as exc:
errors.append(f"fresh SQLite schema execution failed: {exc}")
# V047-V049 are squashed into the V049 compatibility baseline. V050 is the single runtime migration.
# V050 upgrade execution and migration/query parity across SQL engines.
# V050 upgrades a V049-compatible account schema with all unreleased 0.2.15 preferences.
try:
conn = sqlite3.connect(":memory:")
conn.executescript(
"CREATE TABLE u_test__display_preferences (id INTEGER PRIMARY KEY);"
"CREATE TABLE u_test__mail_preferences (id INTEGER PRIMARY KEY);"
"CREATE TABLE u_test__identities (id INTEGER PRIMARY KEY);"
)
migration = read("migrations/sqlite/V050__compact_rows_and_signature_mode.sql").replace("{{p}}", "u_test__")
conn.executescript(migration)
display_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__display_preferences)")}
mail_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__mail_preferences)")}
identity_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__identities)")}
require("adaptive_email_colors" in display_columns and str(display_columns["adaptive_email_colors"][4]) == "0", "V050 defaults adaptive HTML-mail colors off in SQLite")
require("message_loading_mode" in mail_columns and str(mail_columns["message_loading_mode"][4]).strip("\'\"") == "pagination", "V050 defaults mailbox loading mode to pagination in SQLite")
require("compact_wide_rows" in mail_columns and str(mail_columns["compact_wide_rows"][4]) == "1", "V050 defaults compact wide rows on in SQLite")
require("signature_insert_mode" in identity_columns and identity_columns["signature_insert_mode"][4] == "'silent'", "V050 defaults identity signature insertion to silent in SQLite")
conn.execute("INSERT INTO u_test__display_preferences(id) VALUES (1)")
conn.execute("INSERT INTO u_test__mail_preferences(id) VALUES (1)")
conn.execute("INSERT INTO u_test__identities(id) VALUES (1)")
require(conn.execute("SELECT adaptive_email_colors FROM u_test__display_preferences WHERE id=1").fetchone()[0] == 0, "V050 applies adaptive-color default to SQLite rows")
require(conn.execute("SELECT message_loading_mode, compact_wide_rows FROM u_test__mail_preferences WHERE id=1").fetchone() == ("pagination", 1), "V050 applies mailbox pagination and compact-row defaults to SQLite rows")
require(conn.execute("SELECT signature_insert_mode FROM u_test__identities WHERE id=1").fetchone()[0] == "silent", "V050 applies silent-signature default to SQLite rows")
conn.close()
except Exception as exc:
errors.append(f"SQLite V050 migration execution failed: {exc}")
# V050 is the compatibility baseline. V051 is the active 0.2.16 migration.
# Migration/query parity across SQL engines.
active_migration = "V051__adaptive_email_colors_and_message_loading_mode.sql"
for dialect in ("sqlite", "mysql", "pgsql"):
for retired in (
"V047__blob_keys_and_search_outbox.sql",
"V048__folder_counter_generation.sql",
"V049__blob_delete_outbox.sql",
"V050__compact_rows_and_signature_mode.sql",
"V051__adaptive_email_colors.sql",
):
require(not (ROOT / f"migrations/{dialect}/{retired}").exists(), f"consolidated/unreleased migration {retired} is absent for {dialect}")
require((ROOT / f"migrations/{dialect}/V050__compact_rows_and_signature_mode.sql").is_file(), f"V050 runtime migration exists for {dialect}")
migration_text = read(f"migrations/{dialect}/V050__compact_rows_and_signature_mode.sql")
require(
"adaptive_email_colors" in migration_text
and "message_loading_mode" in migration_text
and "compact_wide_rows" in migration_text
and "signature_insert_mode" in migration_text,
f"V050 runtime migration contains all unreleased preference columns for {dialect}",
)
require(not (ROOT / f"migrations/{dialect}/{retired}").exists(), f"retired migration {retired} is absent for {dialect}")
require((ROOT / f"migrations/{dialect}/{active_migration}").is_file(), f"V051 runtime migration exists for {dialect}")
migration_text = read(f"migrations/{dialect}/{active_migration}")
require("adaptive_email_colors" in migration_text and "message_loading_mode" in migration_text and "pagination" in migration_text, f"V051 adds both 0.2.16 preferences for {dialect}")
schema_text = read(f"migrations/{dialect}/schema.sql")
require(
"Squashed compatibility baseline: V049. Current schema: V050." in schema_text
"Squashed compatibility baseline: V050; current schema: V051." in schema_text
and "counter_generation" in schema_text
and "blob_delete_outbox" in schema_text
and "compact_wide_rows" in schema_text
@@ -617,7 +585,7 @@ for dialect in ("sqlite", "mysql", "pgsql"):
and "adaptive_email_colors" in schema_text
and "message_loading_mode" in schema_text
and "pagination" in schema_text,
f"fresh {dialect} schema contains current V050 schema on the V049 compatibility baseline",
f"fresh {dialect} schema contains V050 baseline plus current V051 columns",
)
queries = read(f"src/queries/{dialect}/queries.rs")
require("adaptive_email_colors" in queries, f"display-preference query includes adaptive_email_colors for {dialect}")
@@ -635,16 +603,52 @@ for dialect in ("sqlite", "mysql", "pgsql"):
blob_qnames = set(re.findall(r"pub const (Q_\d{3})", blob_block.group(1)))
require(blob_qnames == {f"Q_{i:03d}" for i in range(1, 6)}, f"blob deletion outbox Q001-Q005 parity for {dialect}")
# Execute the real SQLite V051 migration against a production-like V050 tenant.
try:
conn = sqlite3.connect(":memory:")
conn.executescript("""
CREATE TABLE u_prod__display_preferences (
id INTEGER PRIMARY KEY,
theme TEXT NOT NULL DEFAULT 'system',
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE u_prod__mail_preferences (
id INTEGER PRIMARY KEY,
messages_per_page INTEGER NOT NULL DEFAULT 300,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO u_prod__display_preferences(id) VALUES(1);
INSERT INTO u_prod__mail_preferences(id) VALUES(1);
""")
conn.executescript(read(f"migrations/sqlite/{active_migration}").replace("{{p}}", "u_prod__"))
migrated_display = {row[1]: row for row in conn.execute("PRAGMA table_info(u_prod__display_preferences)")}
migrated_mail = {row[1]: row for row in conn.execute("PRAGMA table_info(u_prod__mail_preferences)")}
require("adaptive_email_colors" in migrated_display and str(migrated_display["adaptive_email_colors"][4]) == "0", "SQLite V051 adds adaptive_email_colors to a V050 tenant")
require("message_loading_mode" in migrated_mail and str(migrated_mail["message_loading_mode"][4]).strip("'\"") == "pagination", "SQLite V051 adds pagination mode to a V050 tenant")
values = conn.execute("SELECT adaptive_email_colors FROM u_prod__display_preferences WHERE id=1").fetchone()
mode = conn.execute("SELECT message_loading_mode FROM u_prod__mail_preferences WHERE id=1").fetchone()
require(values == (0,) and mode == ("pagination",), "SQLite V051 backfills defaults for existing V050 preference rows")
conn.close()
except Exception as exc:
errors.append(f"SQLite V051 migration execution failed: {exc}")
pool = read("src/db/pool.rs")
for dialect in ("SQLITE", "MYSQL", "PGSQL"):
require(f"const {dialect}_MIGRATIONS: &[(u32, &str)]" in pool and "V050__compact_rows_and_signature_mode.sql" in pool, f"runtime migration catalog includes V050 ({dialect.lower()})")
require("pub const BASE_SCHEMA_VERSION: u32 = 49" in pool, "consolidated squashed baseline is V049")
require("V047__blob_keys_and_search_outbox.sql" not in pool and "V048__folder_counter_generation.sql" not in pool and "V049__blob_delete_outbox.sql" not in pool and "V051__adaptive_email_colors.sql" not in pool, "retired standalone migrations are absent from the runtime catalog")
for dialect, rel in (
("SQLITE", "sqlite"),
("MYSQL", "mysql"),
("PGSQL", "pgsql"),
):
expected = f'const {dialect}_MIGRATIONS: &[(u32, &str)] = &[(51, include_str!("../../migrations/{rel}/{active_migration}"))];'
require(expected in pool, f"runtime migration catalog registers V051 for {rel}")
require("pub const BASE_SCHEMA_VERSION: u32 = 50" in pool, "consolidated compatibility baseline remains V050")
require("V050__compact_rows_and_signature_mode.sql" not in pool, "standalone V050 migration is removed from the runtime catalog")
require(active_migration in pool, "V051 preference migration is registered in the runtime catalog")
require("fn sqlite_v051_migrates_existing_v050_preferences()" in pool, "Rust unit test covers V050 to V051 SQLite migration")
require("latest_schema_version" in pool, "runtime schema version is computed from migrations")
require(
"latest_supported_sqlite = SQLITE_MIGRATIONS" in pool
and "if version > latest_supported_sqlite" in pool,
"legacy SQLite import accepts the V049 baseline, applies V050 and rejects schemas newer than this build",
"legacy SQLite import accepts V050-V051 and rejects schemas newer than this build",
)
# Tenant/import table parity and transaction-bound mutation helpers.
+53 -9
View File
@@ -16,22 +16,24 @@ use tokio::sync::Mutex as AsyncMutex;
use crate::config::AppConfig;
pub const BASE_SCHEMA_VERSION: u32 = 49;
pub const BASE_SCHEMA_VERSION: u32 = 50;
const SQLITE_SCHEMA: &str = include_str!("../../migrations/sqlite/schema.sql");
const MYSQL_SCHEMA: &str = include_str!("../../migrations/mysql/schema.sql");
const PGSQL_SCHEMA: &str = include_str!("../../migrations/pgsql/schema.sql");
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[(
50,
include_str!("../../migrations/sqlite/V050__compact_rows_and_signature_mode.sql"),
51,
include_str!(
"../../migrations/sqlite/V051__adaptive_email_colors_and_message_loading_mode.sql"
),
)];
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[(
50,
include_str!("../../migrations/mysql/V050__compact_rows_and_signature_mode.sql"),
51,
include_str!("../../migrations/mysql/V051__adaptive_email_colors_and_message_loading_mode.sql"),
)];
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[(
50,
include_str!("../../migrations/pgsql/V050__compact_rows_and_signature_mode.sql"),
51,
include_str!("../../migrations/pgsql/V051__adaptive_email_colors_and_message_loading_mode.sql"),
)];
const TENANT_TABLES: &[&str] = &[
@@ -1819,7 +1821,7 @@ fn read_legacy_sqlite(path: &Path) -> Result<Vec<LegacyTableData>, String> {
.map_err(|e| format!("failed to read legacy SQLite schema version: {e}"))?;
if version < BASE_SCHEMA_VERSION {
return Err(format!(
"legacy SQLite schema version {version} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade to schema V049 first"
"legacy SQLite schema version {version} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with a V050-capable RustBox release first"
));
}
let latest_supported_sqlite = SQLITE_MIGRATIONS
@@ -2180,7 +2182,7 @@ fn apply_sqlite_migrations(
) -> Result<(), String> {
if current < BASE_SCHEMA_VERSION {
return Err(format!(
"database schema version {current} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade to schema V049 first"
"database schema version {current} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with a V050-capable RustBox release first"
));
}
let latest = latest_schema_version();
@@ -2624,6 +2626,48 @@ mod tests {
assert_eq!(tenant, imported);
}
#[test]
fn sqlite_v051_migrates_existing_v050_preferences() {
let conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE rustbox_accounts (user_hash TEXT PRIMARY KEY, table_prefix TEXT NOT NULL, schema_version INTEGER NOT NULL, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);\n\
CREATE TABLE u_test__display_preferences (id INTEGER PRIMARY KEY, theme TEXT NOT NULL DEFAULT 'system');\n\
CREATE TABLE u_test__mail_preferences (id INTEGER PRIMARY KEY, messages_per_page INTEGER NOT NULL DEFAULT 300);\n\
INSERT INTO rustbox_accounts(user_hash, table_prefix, schema_version) VALUES('test', 'u_test__', 50);\n\
INSERT INTO u_test__display_preferences(id) VALUES(1);\n\
INSERT INTO u_test__mail_preferences(id) VALUES(1);",
)
.unwrap();
apply_sqlite_migrations(&conn, "u_test__", 50).unwrap();
let version: u32 = conn
.query_row(
"SELECT schema_version FROM rustbox_accounts WHERE table_prefix='u_test__'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(version, 51);
let adaptive: i64 = conn
.query_row(
"SELECT adaptive_email_colors FROM u_test__display_preferences WHERE id=1",
[],
|row| row.get(0),
)
.unwrap();
let mode: String = conn
.query_row(
"SELECT message_loading_mode FROM u_test__mail_preferences WHERE id=1",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(adaptive, 0);
assert_eq!(mode, "pagination");
}
#[test]
fn legacy_sqlite_v050_is_imported_and_backed_up() {
let temp = tempfile::tempdir().unwrap();