diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7aa5429..24722d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,10 @@
- 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.
-- No schema migration is required; V051 remains the next migration number.
+- 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.
- Bumped the application/source release to 0.2.15.
### 0.2.14
diff --git a/README.md b/README.md
index 16b7782..7014745 100644
--- a/README.md
+++ b/README.md
@@ -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`. The squashed compatibility baseline remains V046; RustBox 0.2.1 adds runtime migration V047 for attachment checksums/logical blob keys and the durable search outbox, RustBox 0.2.8 adds V048 for monotonic folder-counter generations used by race-safe IMAP counter reconciliation, RustBox 0.2.13 adds V049 for durable blob deletion retries, and RustBox 0.2.14 adds V050 for compact wide message-row preferences and per-identity signature insertion mode. The next migration number is V051. See `migrations/README.md`.
+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`.
## Development
diff --git a/RELEASE-MANIFEST.txt b/RELEASE-MANIFEST.txt
index 5da2568..c6d2551 100644
--- a/RELEASE-MANIFEST.txt
+++ b/RELEASE-MANIFEST.txt
@@ -21,9 +21,8 @@ RustBox 0.2.15 release manifest
- 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
-- stretches `Load more` and `Load all` evenly across the full message-list action width
+- 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
-- Load all message-list action and shorter Details label
- 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
@@ -31,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 runtime schema migration V050 for `mail_preferences.compact_wide_rows` and `identities.signature_insert_mode`; V051 remains next
+- 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
Architecture:
- database_backend = sqlite | mysql | pgsql selects authoritative SQL only
- blob storage and search are independent from the SQL engine
@@ -55,11 +54,9 @@ Search:
Database/schema:
- migrations/{sqlite,mysql,pgsql}/schema.sql contain the current fresh schema
-- compatibility baseline remains V046
-- runtime migration V047 adds attachment checksum metadata and search_outbox
-- runtime migration V048 adds `folders.counter_generation` for race-safe authoritative IMAP counter writes
-- runtime migration V049 adds `blob_delete_outbox` for durable physical blob cleanup
-- runtime migration V050 adds compact-wide-row preference and per-identity signature insertion mode
+- 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
Docker examples:
diff --git a/RELEASE-VALIDATION.txt b/RELEASE-VALIDATION.txt
index b9a0cbc..98ca8e5 100644
--- a/RELEASE-VALIDATION.txt
+++ b/RELEASE-VALIDATION.txt
@@ -17,7 +17,7 @@ RustBox 0.2.15 - final source validation
- 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, full-width Load more/Load all controls, and reader Details label
+- 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
@@ -27,17 +27,17 @@ 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
-- runtime schema migration V050 adds compact-wide-row and signature insertion preferences; V051 remains next
+- 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
Completed in this environment:
-- offline release validator: scripts/validate-release-static.py (416/416 checks passed)
+- offline release validator: scripts/validate-release-static.py (421/421 checks passed)
- browser JavaScript assembled in manifest order passes `node --check`
- all JSON files parse successfully
- Python helper scripts compile successfully
- shell scripts pass `bash -n`
- release metadata consistently identifies 0.2.15
-- V048/V049/V050 migration files/catalog registration and SQLite upgrade execution are validated offline
+- V049 baseline consolidation plus the single V050 runtime migration, adaptive colors and mailbox loading mode are validated offline
Not executable in this packaging environment:
diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md
index 20c3127..a2951e7 100644
--- a/docs/IMPLEMENTATION_STATUS.md
+++ b/docs/IMPLEMENTATION_STATUS.md
@@ -10,7 +10,7 @@ This file tracks the `0.2.15` source snapshot. Storage/search scope and outstand
- 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.
-- Message pagination exposes both `Load more` and `Load all`; both actions stretch evenly across the full list width, and the reader menu label is shortened to `Details`.
+- 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.
@@ -26,9 +26,9 @@ This file tracks the `0.2.15` source snapshot. Storage/search scope and outstand
- 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 pages default to 300 messages and the API/preferences accept values up to 300.
+- 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.
-- Runtime migration V050 adds `mail_preferences.compact_wide_rows` and `identities.signature_insert_mode` on SQLite, MySQL and PostgreSQL; V051 is next.
+- 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.
## 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.
-- V047 adds attachment checksum metadata and the search outbox across SQLite, MySQL and PostgreSQL. V048 adds monotonic folder-counter generations used by compare-and-set reconciliation of authoritative IMAP counts. V049 adds the durable blob deletion outbox. V050 adds compact-wide-row and signature insertion preferences; V051 is reserved as the next migration number.
+- 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.
- 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.
diff --git a/migrations/README.md b/migrations/README.md
index a9f7737..6f71c99 100644
--- a/migrations/README.md
+++ b/migrations/README.md
@@ -1,6 +1,6 @@
# Database schemas and migrations
-RustBox 0.2.15 keeps SQL, blob storage and search as independent concerns:
+RustBox keeps SQL, blob storage and search as independent concerns.
```text
database_backend = sqlite | mysql | pgsql
@@ -9,41 +9,26 @@ storage.backend = filesystem | s3
search.backend = disabled | local_tantivy | opensearch
```
-When `session_backend = "database"`, sessions live in the selected SQL database. `session_backend = "redis"` moves only session persistence to Redis/Redis Sentinel.
-
## Layout
```text
migrations/
-├── sqlite/
-│ ├── schema.sql
-│ ├── V047__blob_keys_and_search_outbox.sql
-│ ├── V048__folder_counter_generation.sql
-│ ├── V049__blob_delete_outbox.sql
-│ └── V050__compact_rows_and_signature_mode.sql
-├── mysql/
-│ ├── schema.sql
-│ ├── V047__blob_keys_and_search_outbox.sql
-│ ├── V048__folder_counter_generation.sql
-│ ├── V049__blob_delete_outbox.sql
-│ └── V050__compact_rows_and_signature_mode.sql
-└── pgsql/
- ├── schema.sql
- ├── V047__blob_keys_and_search_outbox.sql
- ├── V048__folder_counter_generation.sql
- ├── V049__blob_delete_outbox.sql
- └── V050__compact_rows_and_signature_mode.sql
+├── sqlite/schema.sql
+├── mysql/schema.sql
+└── pgsql/schema.sql
```
-Each `schema.sql` is the current complete schema for fresh installations. The historical compatibility baseline remains **V046**. RustBox 0.2.1 adds **V047**, which adds attachment SHA-256 metadata and the durable search outbox used by external indexing. RustBox 0.2.8 adds **V048**, which adds the monotonic `folders.counter_generation` used to reject stale IMAP counter writes even when the numeric counts temporarily return to an earlier value. RustBox 0.2.13 adds **V049**, the durable `blob_delete_outbox` used to retry physical object deletion after metadata is removed. RustBox 0.2.14 adds **V050**, which stores the default-on compact wide-row preference and each identity's signature insertion mode (`silent` or `visible`).
+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`.
-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/migration time. Sessions are global and therefore are not prefixed.
+`mail_preferences.message_loading_mode` defaults to `pagination`; `messages_per_page` defaults to 300. Supported modes are `pagination`, `progressive` and `all`.
+
+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.
+
+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.
## Runtime migrations
-Every schema-changing release must add the same logical migration for all supported SQL engines and register it in `src/db/pool.rs`. Do not update only `schema.sql`: fresh installs use the baseline while existing installations use the numbered migration path.
-
-RustBox 0.2.14 adds runtime migration **V050** on SQLite, MySQL and PostgreSQL. **V051** is the next migration number.
+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`.
Redis and Redis Sentinel do not use SQL migrations.
diff --git a/migrations/mysql/V047__blob_keys_and_search_outbox.sql b/migrations/mysql/V047__blob_keys_and_search_outbox.sql
deleted file mode 100644
index 234f13d..0000000
--- a/migrations/mysql/V047__blob_keys_and_search_outbox.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-ALTER TABLE {{p}}draft_attachments ADD COLUMN checksum_sha256 VARCHAR(64) NULL;
-
-CREATE TABLE {{p}}search_outbox (
- id VARCHAR(64) PRIMARY KEY,
- generation VARCHAR(64) NOT NULL,
- event_type VARCHAR(16) NOT NULL,
- message_uid BIGINT NOT NULL,
- folder TEXT NOT NULL,
- payload LONGTEXT NULL,
- attempts BIGINT NOT NULL DEFAULT 0,
- available_at BIGINT NOT NULL DEFAULT 0,
- claimed_until BIGINT NOT NULL DEFAULT 0,
- dead_letter BIGINT NOT NULL DEFAULT 0,
- last_error TEXT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- INDEX idx_search_outbox_ready (dead_letter, available_at, claimed_until, updated_at)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/migrations/mysql/V048__folder_counter_generation.sql b/migrations/mysql/V048__folder_counter_generation.sql
deleted file mode 100644
index 8b6e516..0000000
--- a/migrations/mysql/V048__folder_counter_generation.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE {{p}}folders ADD COLUMN counter_generation BIGINT NOT NULL DEFAULT 0;
diff --git a/migrations/mysql/V049__blob_delete_outbox.sql b/migrations/mysql/V049__blob_delete_outbox.sql
deleted file mode 100644
index 2be60f1..0000000
--- a/migrations/mysql/V049__blob_delete_outbox.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-CREATE TABLE {{p}}blob_delete_outbox (
- id VARCHAR(64) PRIMARY KEY,
- storage_key TEXT NOT NULL,
- attempts BIGINT NOT NULL DEFAULT 0,
- available_at BIGINT NOT NULL DEFAULT 0,
- claimed_until BIGINT NOT NULL DEFAULT 0,
- last_error TEXT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- INDEX idx_blob_delete_outbox_ready (available_at, claimed_until, updated_at)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/migrations/mysql/V050__compact_rows_and_signature_mode.sql b/migrations/mysql/V050__compact_rows_and_signature_mode.sql
index 905e9a2..d724db9 100644
--- a/migrations/mysql/V050__compact_rows_and_signature_mode.sql
+++ b/migrations/mysql/V050__compact_rows_and_signature_mode.sql
@@ -1,3 +1,9 @@
+ALTER TABLE {{p}}display_preferences
+ ADD COLUMN adaptive_email_colors BIGINT NOT NULL DEFAULT 0;
+
+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;
diff --git a/migrations/mysql/schema.sql b/migrations/mysql/schema.sql
index ad67033..cf0ad28 100644
--- a/migrations/mysql/schema.sql
+++ b/migrations/mysql/schema.sql
@@ -1,13 +1,13 @@
-- 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: V046. Current runtime schema: V050.
+-- Squashed compatibility baseline: V049. Current schema: V050.
-- RUSTBOX:GLOBAL-BEGIN
CREATE TABLE IF NOT EXISTS rustbox_accounts (
user_hash VARCHAR(128) PRIMARY KEY,
table_prefix VARCHAR(64) NOT NULL UNIQUE,
- schema_version BIGINT NOT NULL DEFAULT 46,
+ schema_version BIGINT NOT NULL DEFAULT 50,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -176,7 +176,8 @@ CREATE TABLE {{p}}display_preferences (
forward_mode TEXT NOT NULL DEFAULT ('inline'),
spellcheck_enabled BIGINT NOT NULL DEFAULT 1,
interface_skin TEXT NOT NULL DEFAULT ('rustbox'),
- preset_scale_unlocked BIGINT NOT NULL DEFAULT 0
+ preset_scale_unlocked BIGINT NOT NULL DEFAULT 0,
+ adaptive_email_colors BIGINT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}draft_attachments (
@@ -296,6 +297,7 @@ CREATE TABLE {{p}}known_addresses (
CREATE TABLE {{p}}mail_preferences (
id BIGINT PRIMARY KEY,
messages_per_page BIGINT NOT NULL DEFAULT 300,
+ message_loading_mode TEXT NOT NULL DEFAULT 'pagination',
mark_read_delay_ms BIGINT NOT NULL DEFAULT 0,
remote_images TEXT NOT NULL DEFAULT ('ask'),
preferred_body TEXT NOT NULL DEFAULT ('auto'),
diff --git a/migrations/pgsql/V047__blob_keys_and_search_outbox.sql b/migrations/pgsql/V047__blob_keys_and_search_outbox.sql
deleted file mode 100644
index d80dbce..0000000
--- a/migrations/pgsql/V047__blob_keys_and_search_outbox.sql
+++ /dev/null
@@ -1,20 +0,0 @@
-ALTER TABLE {{p}}draft_attachments ADD COLUMN checksum_sha256 TEXT;
-
-CREATE TABLE {{p}}search_outbox (
- id TEXT PRIMARY KEY,
- generation TEXT NOT NULL,
- event_type TEXT NOT NULL,
- message_uid BIGINT NOT NULL,
- folder TEXT NOT NULL,
- payload TEXT,
- attempts BIGINT NOT NULL DEFAULT 0,
- available_at BIGINT NOT NULL DEFAULT 0,
- claimed_until BIGINT NOT NULL DEFAULT 0,
- dead_letter BIGINT NOT NULL DEFAULT 0,
- last_error TEXT,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL
-);
-
-CREATE INDEX {{p}}idx_search_outbox_ready
- ON {{p}}search_outbox(dead_letter, available_at, claimed_until, updated_at);
diff --git a/migrations/pgsql/V048__folder_counter_generation.sql b/migrations/pgsql/V048__folder_counter_generation.sql
deleted file mode 100644
index 8b6e516..0000000
--- a/migrations/pgsql/V048__folder_counter_generation.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE {{p}}folders ADD COLUMN counter_generation BIGINT NOT NULL DEFAULT 0;
diff --git a/migrations/pgsql/V049__blob_delete_outbox.sql b/migrations/pgsql/V049__blob_delete_outbox.sql
deleted file mode 100644
index 194ca8a..0000000
--- a/migrations/pgsql/V049__blob_delete_outbox.sql
+++ /dev/null
@@ -1,13 +0,0 @@
-CREATE TABLE {{p}}blob_delete_outbox (
- id TEXT PRIMARY KEY,
- storage_key TEXT NOT NULL,
- attempts BIGINT NOT NULL DEFAULT 0,
- available_at BIGINT NOT NULL DEFAULT 0,
- claimed_until BIGINT NOT NULL DEFAULT 0,
- last_error TEXT,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL
-);
-
-CREATE INDEX {{p}}idx_blob_delete_outbox_ready
- ON {{p}}blob_delete_outbox(available_at, claimed_until, updated_at);
diff --git a/migrations/pgsql/V050__compact_rows_and_signature_mode.sql b/migrations/pgsql/V050__compact_rows_and_signature_mode.sql
index 55d7e02..436e705 100644
--- a/migrations/pgsql/V050__compact_rows_and_signature_mode.sql
+++ b/migrations/pgsql/V050__compact_rows_and_signature_mode.sql
@@ -1,3 +1,9 @@
+ALTER TABLE {{p}}display_preferences
+ ADD COLUMN adaptive_email_colors BIGINT NOT NULL DEFAULT 0;
+
+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;
diff --git a/migrations/pgsql/schema.sql b/migrations/pgsql/schema.sql
index 93deb25..a5e951b 100644
--- a/migrations/pgsql/schema.sql
+++ b/migrations/pgsql/schema.sql
@@ -1,13 +1,13 @@
-- 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: V046. Current runtime schema: V050.
+-- Squashed compatibility baseline: V049. Current schema: V050.
-- RUSTBOX:GLOBAL-BEGIN
CREATE TABLE IF NOT EXISTS rustbox_accounts (
user_hash VARCHAR(128) PRIMARY KEY,
table_prefix VARCHAR(64) NOT NULL UNIQUE,
- schema_version BIGINT NOT NULL DEFAULT 46,
+ schema_version BIGINT NOT NULL DEFAULT 50,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
@@ -176,7 +176,8 @@ CREATE TABLE {{p}}display_preferences (
forward_mode TEXT NOT NULL DEFAULT 'inline',
spellcheck_enabled BIGINT NOT NULL DEFAULT 1,
interface_skin TEXT NOT NULL DEFAULT 'rustbox',
- preset_scale_unlocked BIGINT NOT NULL DEFAULT 0
+ preset_scale_unlocked BIGINT NOT NULL DEFAULT 0,
+ adaptive_email_colors BIGINT NOT NULL DEFAULT 0
);
CREATE TABLE {{p}}draft_attachments (
@@ -298,6 +299,7 @@ CREATE TABLE {{p}}known_addresses (
CREATE TABLE {{p}}mail_preferences (
id BIGINT PRIMARY KEY,
messages_per_page BIGINT NOT NULL DEFAULT 300,
+ message_loading_mode TEXT NOT NULL DEFAULT 'pagination',
mark_read_delay_ms BIGINT NOT NULL DEFAULT 0,
remote_images TEXT NOT NULL DEFAULT 'ask',
preferred_body TEXT NOT NULL DEFAULT 'auto',
diff --git a/migrations/sqlite/V047__blob_keys_and_search_outbox.sql b/migrations/sqlite/V047__blob_keys_and_search_outbox.sql
deleted file mode 100644
index c648cd6..0000000
--- a/migrations/sqlite/V047__blob_keys_and_search_outbox.sql
+++ /dev/null
@@ -1,20 +0,0 @@
-ALTER TABLE {{p}}draft_attachments ADD COLUMN checksum_sha256 TEXT;
-
-CREATE TABLE {{p}}search_outbox (
- id TEXT PRIMARY KEY,
- generation TEXT NOT NULL,
- event_type TEXT NOT NULL,
- message_uid INTEGER NOT NULL,
- folder TEXT NOT NULL,
- payload TEXT,
- attempts INTEGER NOT NULL DEFAULT 0,
- available_at INTEGER NOT NULL DEFAULT 0,
- claimed_until INTEGER NOT NULL DEFAULT 0,
- dead_letter INTEGER NOT NULL DEFAULT 0,
- last_error TEXT,
- created_at INTEGER NOT NULL,
- updated_at INTEGER NOT NULL
-);
-
-CREATE INDEX {{p}}idx_search_outbox_ready
- ON {{p}}search_outbox(dead_letter, available_at, claimed_until, updated_at);
diff --git a/migrations/sqlite/V048__folder_counter_generation.sql b/migrations/sqlite/V048__folder_counter_generation.sql
deleted file mode 100644
index 0b92a5a..0000000
--- a/migrations/sqlite/V048__folder_counter_generation.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE {{p}}folders ADD COLUMN counter_generation INTEGER NOT NULL DEFAULT 0;
diff --git a/migrations/sqlite/V049__blob_delete_outbox.sql b/migrations/sqlite/V049__blob_delete_outbox.sql
deleted file mode 100644
index 816e264..0000000
--- a/migrations/sqlite/V049__blob_delete_outbox.sql
+++ /dev/null
@@ -1,13 +0,0 @@
-CREATE TABLE {{p}}blob_delete_outbox (
- id TEXT PRIMARY KEY,
- storage_key TEXT NOT NULL,
- attempts INTEGER NOT NULL DEFAULT 0,
- available_at INTEGER NOT NULL DEFAULT 0,
- claimed_until INTEGER NOT NULL DEFAULT 0,
- last_error TEXT,
- created_at INTEGER NOT NULL,
- updated_at INTEGER NOT NULL
-);
-
-CREATE INDEX {{p}}idx_blob_delete_outbox_ready
- ON {{p}}blob_delete_outbox(available_at, claimed_until, updated_at);
diff --git a/migrations/sqlite/V050__compact_rows_and_signature_mode.sql b/migrations/sqlite/V050__compact_rows_and_signature_mode.sql
index 259ad58..3d8aa6a 100644
--- a/migrations/sqlite/V050__compact_rows_and_signature_mode.sql
+++ b/migrations/sqlite/V050__compact_rows_and_signature_mode.sql
@@ -1,3 +1,9 @@
+ALTER TABLE {{p}}display_preferences
+ ADD COLUMN adaptive_email_colors INTEGER NOT NULL DEFAULT 0;
+
+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;
diff --git a/migrations/sqlite/schema.sql b/migrations/sqlite/schema.sql
index 7bd4f84..c47002b 100644
--- a/migrations/sqlite/schema.sql
+++ b/migrations/sqlite/schema.sql
@@ -1,13 +1,13 @@
-- 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: V046. Current runtime schema: V050.
+-- Squashed compatibility baseline: V049. Current schema: V050.
-- RUSTBOX:GLOBAL-BEGIN
CREATE TABLE IF NOT EXISTS rustbox_accounts (
user_hash TEXT PRIMARY KEY NOT NULL,
table_prefix TEXT NOT NULL UNIQUE,
- schema_version INTEGER NOT NULL DEFAULT 46,
+ schema_version INTEGER NOT NULL DEFAULT 50,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -122,7 +122,7 @@ CREATE TABLE {{p}}display_preferences (
theme TEXT NOT NULL DEFAULT 'system',
language TEXT NOT NULL DEFAULT 'en',
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
- compose_format TEXT NOT NULL DEFAULT 'html', deep_index BOOLEAN NOT NULL DEFAULT 0, animation_mode TEXT NULL, preview_pane TEXT NOT NULL DEFAULT 'right', ui_scale TEXT NOT NULL DEFAULT 'normal', color_preset TEXT NOT NULL DEFAULT 'sage', accent_color TEXT NOT NULL DEFAULT '#6f9c86', font_family TEXT NOT NULL DEFAULT 'system', font_size INTEGER NOT NULL DEFAULT 13, row_height INTEGER NOT NULL DEFAULT 58, folder_pane_width INTEGER NOT NULL DEFAULT 228, message_pane_width INTEGER NOT NULL DEFAULT 420, border_radius INTEGER NOT NULL DEFAULT 4, contrast TEXT NOT NULL DEFAULT 'normal', show_avatars INTEGER NOT NULL DEFAULT 1, show_attachment_icons INTEGER NOT NULL DEFAULT 1, show_flags INTEGER NOT NULL DEFAULT 1, reduce_motion INTEGER NOT NULL DEFAULT 0, date_format TEXT NOT NULL DEFAULT 'locale', time_format TEXT NOT NULL DEFAULT 'locale', background_color TEXT NOT NULL DEFAULT '#121513', panel_color TEXT NOT NULL DEFAULT '#181c19', text_color TEXT NOT NULL DEFAULT '#edf0ec', compose_autosave_ms INTEGER NOT NULL DEFAULT 3000, default_priority TEXT NOT NULL DEFAULT 'normal', request_mdn_default INTEGER NOT NULL DEFAULT 0, attachment_reminder INTEGER NOT NULL DEFAULT 1, icon_size INTEGER NOT NULL DEFAULT 18, toolbar_density TEXT NOT NULL DEFAULT 'normal', sidebar_item_height INTEGER NOT NULL DEFAULT 29, reader_max_width INTEGER NOT NULL DEFAULT 0, composer_width INTEGER NOT NULL DEFAULT 900, selection_color TEXT NOT NULL DEFAULT '#28352e', unread_font_weight INTEGER NOT NULL DEFAULT 730, unread_badge_style TEXT NOT NULL DEFAULT 'dot', compact_headers INTEGER NOT NULL DEFAULT 0, show_borders INTEGER NOT NULL DEFAULT 1, scrollbar_density TEXT NOT NULL DEFAULT 'thin', monochrome INTEGER NOT NULL DEFAULT 0, folder_background TEXT NOT NULL DEFAULT '', message_background TEXT NOT NULL DEFAULT '', reader_background TEXT NOT NULL DEFAULT '', compose_font_family TEXT NOT NULL DEFAULT 'system', reply_position TEXT NOT NULL DEFAULT 'above', quote_style TEXT NOT NULL DEFAULT 'standard', forward_mode TEXT NOT NULL DEFAULT 'inline', spellcheck_enabled INTEGER NOT NULL DEFAULT 1, interface_skin TEXT NOT NULL DEFAULT 'rustbox', preset_scale_unlocked INTEGER NOT NULL DEFAULT 0);
+ compose_format TEXT NOT NULL DEFAULT 'html', deep_index BOOLEAN NOT NULL DEFAULT 0, animation_mode TEXT NULL, preview_pane TEXT NOT NULL DEFAULT 'right', ui_scale TEXT NOT NULL DEFAULT 'normal', color_preset TEXT NOT NULL DEFAULT 'sage', accent_color TEXT NOT NULL DEFAULT '#6f9c86', font_family TEXT NOT NULL DEFAULT 'system', font_size INTEGER NOT NULL DEFAULT 13, row_height INTEGER NOT NULL DEFAULT 58, folder_pane_width INTEGER NOT NULL DEFAULT 228, message_pane_width INTEGER NOT NULL DEFAULT 420, border_radius INTEGER NOT NULL DEFAULT 4, contrast TEXT NOT NULL DEFAULT 'normal', show_avatars INTEGER NOT NULL DEFAULT 1, show_attachment_icons INTEGER NOT NULL DEFAULT 1, show_flags INTEGER NOT NULL DEFAULT 1, reduce_motion INTEGER NOT NULL DEFAULT 0, date_format TEXT NOT NULL DEFAULT 'locale', time_format TEXT NOT NULL DEFAULT 'locale', background_color TEXT NOT NULL DEFAULT '#121513', panel_color TEXT NOT NULL DEFAULT '#181c19', text_color TEXT NOT NULL DEFAULT '#edf0ec', compose_autosave_ms INTEGER NOT NULL DEFAULT 3000, default_priority TEXT NOT NULL DEFAULT 'normal', request_mdn_default INTEGER NOT NULL DEFAULT 0, attachment_reminder INTEGER NOT NULL DEFAULT 1, icon_size INTEGER NOT NULL DEFAULT 18, toolbar_density TEXT NOT NULL DEFAULT 'normal', sidebar_item_height INTEGER NOT NULL DEFAULT 29, reader_max_width INTEGER NOT NULL DEFAULT 0, composer_width INTEGER NOT NULL DEFAULT 900, selection_color TEXT NOT NULL DEFAULT '#28352e', unread_font_weight INTEGER NOT NULL DEFAULT 730, unread_badge_style TEXT NOT NULL DEFAULT 'dot', compact_headers INTEGER NOT NULL DEFAULT 0, show_borders INTEGER NOT NULL DEFAULT 1, scrollbar_density TEXT NOT NULL DEFAULT 'thin', monochrome INTEGER NOT NULL DEFAULT 0, folder_background TEXT NOT NULL DEFAULT '', message_background TEXT NOT NULL DEFAULT '', reader_background TEXT NOT NULL DEFAULT '', compose_font_family TEXT NOT NULL DEFAULT 'system', reply_position TEXT NOT NULL DEFAULT 'above', quote_style TEXT NOT NULL DEFAULT 'standard', forward_mode TEXT NOT NULL DEFAULT 'inline', spellcheck_enabled INTEGER NOT NULL DEFAULT 1, interface_skin TEXT NOT NULL DEFAULT 'rustbox', preset_scale_unlocked INTEGER NOT NULL DEFAULT 0, adaptive_email_colors INTEGER NOT NULL DEFAULT 0);
CREATE TABLE {{p}}draft_attachments (
id TEXT PRIMARY KEY,
@@ -221,6 +221,7 @@ CREATE TABLE {{p}}known_addresses (
CREATE TABLE {{p}}mail_preferences (
id INTEGER PRIMARY KEY CHECK (id = 1),
messages_per_page INTEGER NOT NULL DEFAULT 300,
+ message_loading_mode TEXT NOT NULL DEFAULT 'pagination',
mark_read_delay_ms INTEGER NOT NULL DEFAULT 0,
remote_images TEXT NOT NULL DEFAULT 'ask',
preferred_body TEXT NOT NULL DEFAULT 'auto',
diff --git a/scripts/migrate_roundcube.py b/scripts/migrate_roundcube.py
index 49a342f..cfad6e6 100755
--- a/scripts/migrate_roundcube.py
+++ b/scripts/migrate_roundcube.py
@@ -453,7 +453,7 @@ def sqlite_tenant_schema() -> tuple[str, int]:
raise ValueError(f"invalid RustBox tenant schema markers in {schema_path}")
tenant_sql = schema.split(begin, 1)[1].split(end, 1)[0]
- versions = [46]
+ versions = [50]
for path in root.glob("V[0-9][0-9][0-9]__*.sql"):
try:
versions.append(int(path.name[1:4]))
diff --git a/scripts/validate-release-static.py b/scripts/validate-release-static.py
index 6e22703..460fb8c 100755
--- a/scripts/validate-release-static.py
+++ b/scripts/validate-release-static.py
@@ -84,10 +84,10 @@ reader_js = read("src/web/js/40-messages-reader.js")
compose_js = read("src/web/js/50-compose.js")
compose_css = read("src/web/css/40-compose.css")
mail_css = read("src/web/css/30-mail.css")
-require("perPage: 300" in bootstrap_js and "messages_per_page: 300" in bootstrap_js, "browser mailbox default is 300 messages")
+require("perPage: 300" in bootstrap_js and "messages_per_page: 300" in bootstrap_js and 'message_loading_mode: "pagination"' in bootstrap_js, "browser mailbox defaults to 300-message pagination")
require(re.search(r"default_per_page\(\)\s*->\s*u32\s*\{\s*300\s*\}", read("src/routes/messages/types.rs")) is not None, "folder message API default page size is 300")
require('1..=300' in read("src/routes/messages/mod.rs"), "folder message API accepts up to 300 messages per page")
-require('20..=300' in read("src/db/mail_preferences.rs"), "mailbox preference accepts 300 messages per page")
+require('20..=300' in read("src/db/mail_preferences.rs") and '"pagination" | "progressive" | "all"' in read("src/db/mail_preferences.rs"), "mailbox preferences accept 300-message pages and the three loading modes")
require('const appShortcut = keyboardIdle && !event.ctrlKey && !event.metaKey && !event.altKey;' in bindings_js, "single-key shortcuts preserve Ctrl/Cmd/Alt browser shortcuts")
require('realtimeNotificationsReady' in realtime_js and 'suppressRealtimeNotificationsForFolder' in realtime_js, "realtime new-mail notification gating is present")
require('realtimeFolderIsDrafts' in realtime_js and 'if (realtimeFolderIsDrafts(data?.folder)) return true;' in realtime_js, "realtime new-mail notifications are suppressed for Drafts folders")
@@ -105,12 +105,13 @@ require('clipboardData' in bindings_js and 'uploadInlineImage(images, range)' in
require('.compose-attachment-kind' in compose_css and 'Image attachment' in compose_js and 'Inline image' in compose_js, "composer distinguishes embedded images from regular attachments")
require("function mergeComposeAttachments" in compose_js and "await refreshComposeAttachmentsFromServer(id);" in compose_js and "attachments: mergeComposeAttachments(local.attachments, server.attachments)" in compose_js, "composer preserves multiple attachments and draft recovery merges authoritative server attachment metadata")
require("const attachments = mergeComposeAttachments(state.composeAttachments);" in compose_js and "Inline link" in compose_js and "detachComposeAttachmentFromBody" in compose_js, "composer attachment strip keeps inline and regular attachments visible together")
-require('grid-template-columns: repeat(2, minmax(0, 1fr));' in mail_css and '.load-more {' in mail_css and 'width: 100%;' in mail_css.split('.load-more {', 1)[1].split('}', 1)[0], "message load actions share the full available list width")
+require('.message-pagination {' in mail_css and 'grid-template-columns: 30px auto 30px;' in mail_css and '.pagination-arrow {' in mail_css and '.load-more {' in mail_css and 'width: 100%;' in mail_css.split('.load-more {', 1)[1].split('}', 1)[0], "message pagination uses compact SVG arrows while progressive-load controls span the message list width")
require('id="compose-fields-toggle"' in read("src/web/index.html") and 'function setComposeFieldsCollapsed' in compose_js and 'function toggleComposeFields' in compose_js and '.compose-dialog.compose-fields-collapsed .compose-addresses' in compose_css and 'setComposeFieldsCollapsed(false)' in bindings_js, "composer addressing fields can collapse to maximize editor space and reopen on invalid recipient input")
require('id="compose-save-draft"' in read("src/web/index.html") and '$("#compose-save-draft").addEventListener("click", saveComposeDraftToImap);' in bindings_js and 'saveCurrentDraft(false, true, true)' in compose_js, "composer exposes an explicit manual IMAP draft save before cancel")
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('
Mailbox storage' in index_html and 'RustBox__RUSTBOX_VERSION__' in index_html, "mailbox storage and footer labels stay protocol-neutral")
require('
HTML signature / footer' in index_html and 'aria-labelledby="identity-signature-label"' in index_html and '