1 parent
fb9eab87eb
commit
d91212d24c
40 files changed
+585
-380
No files matched your search
+4
-1
@@ -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.
|
- 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.
|
- 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.
|
- 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.
|
- Bumped the application/source release to 0.2.15.
|
||||||
|
|
||||||
### 0.2.14
|
### 0.2.14
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ rustbox search reindex
|
|||||||
|
|
||||||
Ready Docker examples are in `docker/`: standalone SQLite, PostgreSQL + SILO (S3-compatible) + OpenSearch, and MySQL + NFS + OpenSearch.
|
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
|
## Development
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ RustBox 0.2.15 release manifest
|
|||||||
- labels embedded images separately from regular/image attachment chips
|
- labels embedded images separately from regular/image attachment chips
|
||||||
- keeps attachment re-embed actions compact and allows separate same-name/same-size files
|
- keeps attachment re-embed actions compact and allows separate same-name/same-size files
|
||||||
- compact recipient address summaries with expandable full recipient lists
|
- 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
|
- 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
|
- 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
|
- 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
|
- 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
|
- 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
|
- 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
|
- 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:
|
Architecture:
|
||||||
- database_backend = sqlite | mysql | pgsql selects authoritative SQL only
|
- database_backend = sqlite | mysql | pgsql selects authoritative SQL only
|
||||||
- blob storage and search are independent from the SQL engine
|
- blob storage and search are independent from the SQL engine
|
||||||
@@ -55,11 +54,9 @@ Search:
|
|||||||
|
|
||||||
Database/schema:
|
Database/schema:
|
||||||
- migrations/{sqlite,mysql,pgsql}/schema.sql contain the current fresh schema
|
- migrations/{sqlite,mysql,pgsql}/schema.sql contain the current fresh schema
|
||||||
- compatibility baseline remains V046
|
- compatibility baseline is V049; former V047-V049 changes are consolidated into `schema.sql`, while V050 remains the single runtime migration
|
||||||
- runtime migration V047 adds attachment checksum metadata and search_outbox
|
- databases older than V049 must be upgraded to schema V049 before using this build
|
||||||
- runtime migration V048 adds `folders.counter_generation` for race-safe authoritative IMAP counter writes
|
- 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
|
||||||
- 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
|
|
||||||
- next migration number: V051
|
- next migration number: V051
|
||||||
|
|
||||||
Docker examples:
|
Docker examples:
|
||||||
|
|||||||
@@ -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
|
- 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
|
- multiple inline images/attachments append independently and do not replace the active selection
|
||||||
- same-name/same-size files are no longer silently de-duplicated
|
- 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
|
- 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
|
- 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
|
- 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
|
- PDF attachment preview uses the authenticated inline URL for Safari/iPhone compatibility
|
||||||
- Composing shows only compact attachment/message size-limit values on one line
|
- 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
|
- 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
|
- version metadata updated to 0.2.15
|
||||||
|
|
||||||
Completed in this environment:
|
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`
|
- browser JavaScript assembled in manifest order passes `node --check`
|
||||||
- all JSON files parse successfully
|
- all JSON files parse successfully
|
||||||
- Python helper scripts compile successfully
|
- Python helper scripts compile successfully
|
||||||
- shell scripts pass `bash -n`
|
- shell scripts pass `bash -n`
|
||||||
- release metadata consistently identifies 0.2.15
|
- 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:
|
Not executable in this packaging environment:
|
||||||
|
|||||||
@@ -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.
|
- 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.
|
- 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.
|
- 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 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 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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
|
## 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
+11
-26
@@ -1,6 +1,6 @@
|
|||||||
# Database schemas and migrations
|
# 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
|
```text
|
||||||
database_backend = sqlite | mysql | pgsql
|
database_backend = sqlite | mysql | pgsql
|
||||||
@@ -9,41 +9,26 @@ storage.backend = filesystem | s3
|
|||||||
search.backend = disabled | local_tantivy | opensearch
|
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
|
## Layout
|
||||||
|
|
||||||
```text
|
```text
|
||||||
migrations/
|
migrations/
|
||||||
├── sqlite/
|
├── sqlite/schema.sql
|
||||||
│ ├── schema.sql
|
├── mysql/schema.sql
|
||||||
│ ├── V047__blob_keys_and_search_outbox.sql
|
└── pgsql/schema.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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
## 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.
|
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`.
|
||||||
|
|
||||||
RustBox 0.2.14 adds runtime migration **V050** on SQLite, MySQL and PostgreSQL. **V051** is the next migration number.
|
|
||||||
|
|
||||||
Redis and Redis Sentinel do not use SQL migrations.
|
Redis and Redis Sentinel do not use SQL migrations.
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE {{p}}folders ADD COLUMN counter_generation BIGINT NOT NULL DEFAULT 0;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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
|
ALTER TABLE {{p}}mail_preferences
|
||||||
ADD COLUMN compact_wide_rows BIGINT NOT NULL DEFAULT 1;
|
ADD COLUMN compact_wide_rows BIGINT NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
-- RustBox 0.2.0 MySQL schema.
|
-- RustBox 0.2.0 MySQL schema.
|
||||||
-- One physical database contains global sessions plus isolated per-account tables.
|
-- One physical database contains global sessions plus isolated per-account tables.
|
||||||
-- Application tables are materialized by replacing {{p}} with a collision-checked prefix.
|
-- 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
|
-- RUSTBOX:GLOBAL-BEGIN
|
||||||
CREATE TABLE IF NOT EXISTS rustbox_accounts (
|
CREATE TABLE IF NOT EXISTS rustbox_accounts (
|
||||||
user_hash VARCHAR(128) PRIMARY KEY,
|
user_hash VARCHAR(128) PRIMARY KEY,
|
||||||
table_prefix VARCHAR(64) NOT NULL UNIQUE,
|
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)),
|
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
|
||||||
updated_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;
|
) 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'),
|
forward_mode TEXT NOT NULL DEFAULT ('inline'),
|
||||||
spellcheck_enabled BIGINT NOT NULL DEFAULT 1,
|
spellcheck_enabled BIGINT NOT NULL DEFAULT 1,
|
||||||
interface_skin TEXT NOT NULL DEFAULT ('rustbox'),
|
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;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE {{p}}draft_attachments (
|
CREATE TABLE {{p}}draft_attachments (
|
||||||
@@ -296,6 +297,7 @@ CREATE TABLE {{p}}known_addresses (
|
|||||||
CREATE TABLE {{p}}mail_preferences (
|
CREATE TABLE {{p}}mail_preferences (
|
||||||
id BIGINT PRIMARY KEY,
|
id BIGINT PRIMARY KEY,
|
||||||
messages_per_page BIGINT NOT NULL DEFAULT 300,
|
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,
|
mark_read_delay_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
remote_images TEXT NOT NULL DEFAULT ('ask'),
|
remote_images TEXT NOT NULL DEFAULT ('ask'),
|
||||||
preferred_body TEXT NOT NULL DEFAULT ('auto'),
|
preferred_body TEXT NOT NULL DEFAULT ('auto'),
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE {{p}}folders ADD COLUMN counter_generation BIGINT NOT NULL DEFAULT 0;
|
|
||||||
@@ -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);
|
|
||||||
@@ -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
|
ALTER TABLE {{p}}mail_preferences
|
||||||
ADD COLUMN compact_wide_rows BIGINT NOT NULL DEFAULT 1;
|
ADD COLUMN compact_wide_rows BIGINT NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
-- RustBox 0.2.0 PostgreSQL schema.
|
-- RustBox 0.2.0 PostgreSQL schema.
|
||||||
-- One physical database contains global sessions plus isolated per-account tables.
|
-- One physical database contains global sessions plus isolated per-account tables.
|
||||||
-- Application tables are materialized by replacing {{p}} with a collision-checked prefix.
|
-- 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
|
-- RUSTBOX:GLOBAL-BEGIN
|
||||||
CREATE TABLE IF NOT EXISTS rustbox_accounts (
|
CREATE TABLE IF NOT EXISTS rustbox_accounts (
|
||||||
user_hash VARCHAR(128) PRIMARY KEY,
|
user_hash VARCHAR(128) PRIMARY KEY,
|
||||||
table_prefix VARCHAR(64) NOT NULL UNIQUE,
|
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),
|
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
|
||||||
updated_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',
|
forward_mode TEXT NOT NULL DEFAULT 'inline',
|
||||||
spellcheck_enabled BIGINT NOT NULL DEFAULT 1,
|
spellcheck_enabled BIGINT NOT NULL DEFAULT 1,
|
||||||
interface_skin TEXT NOT NULL DEFAULT 'rustbox',
|
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 (
|
CREATE TABLE {{p}}draft_attachments (
|
||||||
@@ -298,6 +299,7 @@ CREATE TABLE {{p}}known_addresses (
|
|||||||
CREATE TABLE {{p}}mail_preferences (
|
CREATE TABLE {{p}}mail_preferences (
|
||||||
id BIGINT PRIMARY KEY,
|
id BIGINT PRIMARY KEY,
|
||||||
messages_per_page BIGINT NOT NULL DEFAULT 300,
|
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,
|
mark_read_delay_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
remote_images TEXT NOT NULL DEFAULT 'ask',
|
remote_images TEXT NOT NULL DEFAULT 'ask',
|
||||||
preferred_body TEXT NOT NULL DEFAULT 'auto',
|
preferred_body TEXT NOT NULL DEFAULT 'auto',
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE {{p}}folders ADD COLUMN counter_generation INTEGER NOT NULL DEFAULT 0;
|
|
||||||
@@ -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);
|
|
||||||
@@ -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
|
ALTER TABLE {{p}}mail_preferences
|
||||||
ADD COLUMN compact_wide_rows INTEGER NOT NULL DEFAULT 1;
|
ADD COLUMN compact_wide_rows INTEGER NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
-- RustBox 0.2.0 SQLite schema.
|
-- RustBox 0.2.0 SQLite schema.
|
||||||
-- One physical database contains global sessions plus isolated per-account tables.
|
-- One physical database contains global sessions plus isolated per-account tables.
|
||||||
-- Application tables are materialized from the tenant section by replacing {{p}}.
|
-- 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
|
-- RUSTBOX:GLOBAL-BEGIN
|
||||||
CREATE TABLE IF NOT EXISTS rustbox_accounts (
|
CREATE TABLE IF NOT EXISTS rustbox_accounts (
|
||||||
user_hash TEXT PRIMARY KEY NOT NULL,
|
user_hash TEXT PRIMARY KEY NOT NULL,
|
||||||
table_prefix TEXT NOT NULL UNIQUE,
|
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')),
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
updated_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',
|
theme TEXT NOT NULL DEFAULT 'system',
|
||||||
language TEXT NOT NULL DEFAULT 'en',
|
language TEXT NOT NULL DEFAULT 'en',
|
||||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
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 (
|
CREATE TABLE {{p}}draft_attachments (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -221,6 +221,7 @@ CREATE TABLE {{p}}known_addresses (
|
|||||||
CREATE TABLE {{p}}mail_preferences (
|
CREATE TABLE {{p}}mail_preferences (
|
||||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
messages_per_page INTEGER NOT NULL DEFAULT 300,
|
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,
|
mark_read_delay_ms INTEGER NOT NULL DEFAULT 0,
|
||||||
remote_images TEXT NOT NULL DEFAULT 'ask',
|
remote_images TEXT NOT NULL DEFAULT 'ask',
|
||||||
preferred_body TEXT NOT NULL DEFAULT 'auto',
|
preferred_body TEXT NOT NULL DEFAULT 'auto',
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ def sqlite_tenant_schema() -> tuple[str, int]:
|
|||||||
raise ValueError(f"invalid RustBox tenant schema markers in {schema_path}")
|
raise ValueError(f"invalid RustBox tenant schema markers in {schema_path}")
|
||||||
tenant_sql = schema.split(begin, 1)[1].split(end, 1)[0]
|
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"):
|
for path in root.glob("V[0-9][0-9][0-9]__*.sql"):
|
||||||
try:
|
try:
|
||||||
versions.append(int(path.name[1:4]))
|
versions.append(int(path.name[1:4]))
|
||||||
|
|||||||
@@ -84,10 +84,10 @@ reader_js = read("src/web/js/40-messages-reader.js")
|
|||||||
compose_js = read("src/web/js/50-compose.js")
|
compose_js = read("src/web/js/50-compose.js")
|
||||||
compose_css = read("src/web/css/40-compose.css")
|
compose_css = read("src/web/css/40-compose.css")
|
||||||
mail_css = read("src/web/css/30-mail.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(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('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('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('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")
|
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('.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("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("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-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('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")
|
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")
|
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('<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('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")
|
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")
|
||||||
@@ -121,6 +122,8 @@ require('bindPaneResizer("#message-resizer", "messagePaneWidth", 300, 760)' in b
|
|||||||
require('bindPaneResizer("#folder-resizer", "folderPaneWidth", 180, 420)' in bindings_js and 'value.clamp(180, 420).to_db_value()' in display_prefs_rs and 'id="pref-folder-width" type="range"\n min="180" max="420"' in index_html, "folder pane resize limits are aligned at 180..420 px and normalized server-side")
|
require('bindPaneResizer("#folder-resizer", "folderPaneWidth", 180, 420)' in bindings_js and 'value.clamp(180, 420).to_db_value()' in display_prefs_rs and 'id="pref-folder-width" type="range"\n min="180" max="420"' in index_html, "folder pane resize limits are aligned at 180..420 px and normalized server-side")
|
||||||
require('suppressPaneWidthValidationError' in auth_js and 'temporarilyDisablePanePreference' in auth_js and 'data.temporarilyDisabled' not in auth_js and 'handle.dataset.temporarilyDisabled = "true"' in auth_js and 'if (!suppressPaneWidthValidationError(error)) toast(error.message, "error")' in auth_js, "pane-width validation incompatibilities disable the in-view resizer without a user-facing error")
|
require('suppressPaneWidthValidationError' in auth_js and 'temporarilyDisablePanePreference' in auth_js and 'data.temporarilyDisabled' not in auth_js and 'handle.dataset.temporarilyDisabled = "true"' in auth_js and 'if (!suppressPaneWidthValidationError(error)) toast(error.message, "error")' in auth_js, "pane-width validation incompatibilities disable the in-view resizer without a user-facing error")
|
||||||
require('id="identity-signature-mode"' in read("src/web/index.html") and 'value="silent"' in read("src/web/index.html") and 'value="visible"' in read("src/web/index.html") and 'signature_insert_mode' in read("src/web/js/72-openpgp-identities.js"), "identity UI exposes silent and visible signature insertion modes")
|
require('id="identity-signature-mode"' in read("src/web/index.html") and 'value="silent"' in read("src/web/index.html") and 'value="visible"' in read("src/web/index.html") and 'signature_insert_mode' in read("src/web/js/72-openpgp-identities.js"), "identity UI exposes silent and visible signature insertion modes")
|
||||||
|
require('id="pref-adaptive-email-colors"' in read("src/web/index.html") and 'adaptive_email_colors' in display_prefs_rs and 'adaptiveEmailColors' in auth_js and 'adaptMessagePreviewColors' in reader_js, "Appearance exposes persisted adaptive HTML-mail colors and the reader applies them")
|
||||||
|
require('id="mail-pref-loading-mode"' in index_html and 'id="message-pagination"' in index_html and 'function changeMailboxPage(delta)' in reader_js and 'function loadNextProgressivePage()' in reader_js and 'function loadAllMessages()' in reader_js and 'handleMessageListScroll' in bindings_js, "Mailbox settings expose pagination, progressive loading and load-all behavior")
|
||||||
require('identity.signature_insert_mode != "visible"' in read("src/routes/send.rs") and 'data-rustbox-signature' in read("src/web/js/72-openpgp-identities.js"), "silent signatures append server-side while visible signatures remain composer-owned and de-duplicatable")
|
require('identity.signature_insert_mode != "visible"' in read("src/routes/send.rs") and 'data-rustbox-signature' in read("src/web/js/72-openpgp-identities.js"), "silent signatures append server-side while visible signatures remain composer-owned and de-duplicatable")
|
||||||
require('id="compose-delete-draft"' in read("src/web/index.html") and '$("#compose-delete-draft").addEventListener("click", deleteComposeDraft);' in bindings_js and 'function deleteComposeDraft()' in compose_js and 'imap_uid_validity' in compose_js, "composer exposes permanent draft deletion with current IMAP coordinates")
|
require('id="compose-delete-draft"' in read("src/web/index.html") and '$("#compose-delete-draft").addEventListener("click", deleteComposeDraft);' in bindings_js and 'function deleteComposeDraft()' in compose_js and 'imap_uid_validity' in compose_js, "composer exposes permanent draft deletion with current IMAP coordinates")
|
||||||
require('if req.sync_imap {' in read("src/routes/drafts.rs") and 'Failed to save draft to IMAP' in read("src/routes/drafts.rs") and 'AppError::ServiceUnavailable' in read("src/routes/drafts.rs"), "manual IMAP draft save fails visibly when the IMAP snapshot cannot be appended")
|
require('if req.sync_imap {' in read("src/routes/drafts.rs") and 'Failed to save draft to IMAP' in read("src/routes/drafts.rs") and 'AppError::ServiceUnavailable' in read("src/routes/drafts.rs"), "manual IMAP draft save fails visibly when the IMAP snapshot cannot be appended")
|
||||||
@@ -138,8 +141,9 @@ 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")
|
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")
|
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('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("V050" in read("RELEASE-MANIFEST.txt") and "V051" in read("RELEASE-MANIFEST.txt"), "release manifest documents V050 and reserves V051")
|
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("V050" in read("migrations/README.md") and "V051" in read("migrations/README.md"), "migration guide documents V050 and reserves 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("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((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")
|
require('imap_host = ""' not in read("config.dev.toml"), "development config does not override IMAP host with an invalid empty value")
|
||||||
require(not re.search(r"(?m)^IMAP_HOST=\s*$", read(".env.example")), "environment example does not set an invalid empty IMAP host")
|
require(not re.search(r"(?m)^IMAP_HOST=\s*$", read(".env.example")), "environment example does not set an invalid empty IMAP host")
|
||||||
@@ -548,114 +552,76 @@ try:
|
|||||||
require(sqlite_generation == ("2",), "SQLite header-sync UPSERT remains unambiguous and increments generation")
|
require(sqlite_generation == ("2",), "SQLite header-sync UPSERT remains unambiguous and increments generation")
|
||||||
account_default = conn.execute("PRAGMA table_info(rustbox_accounts)").fetchall()
|
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)
|
version_default = next((row[4] for row in account_default if row[1] == "schema_version"), None)
|
||||||
require(str(version_default).strip("'\"") == "46", "fresh global compatibility baseline remains V046 before tenant registration")
|
require(str(version_default).strip("'\"") == "50", "fresh account schema version defaults to current V050")
|
||||||
|
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")
|
||||||
|
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")
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors.append(f"fresh SQLite schema execution failed: {exc}")
|
errors.append(f"fresh SQLite schema execution failed: {exc}")
|
||||||
|
|
||||||
# V047 upgrade execution from a minimal V046 attachment table, including data preservation.
|
# V047-V049 are squashed into the V049 compatibility baseline. V050 is the single runtime migration.
|
||||||
try:
|
# V050 upgrade execution and migration/query parity across SQL engines.
|
||||||
conn = sqlite3.connect(":memory:")
|
# V050 upgrades a V049-compatible account schema with all unreleased 0.2.15 preferences.
|
||||||
conn.executescript(
|
|
||||||
"CREATE TABLE u_test__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);"
|
|
||||||
"INSERT INTO u_test__draft_attachments "
|
|
||||||
"(id, draft_id, filename, content_type, size, file_path, created_at) VALUES "
|
|
||||||
"('att-1', 'draft-1', 'report.pdf', 'application/pdf', 1234, '/legacy/report.pdf', '2026-01-01T00:00:00Z');"
|
|
||||||
)
|
|
||||||
migration = read("migrations/sqlite/V047__blob_keys_and_search_outbox.sql").replace("{{p}}", "u_test__")
|
|
||||||
conn.executescript(migration)
|
|
||||||
columns = {row[1] for row in conn.execute("PRAGMA table_info(u_test__draft_attachments)")}
|
|
||||||
require("checksum_sha256" in columns, "V047 adds checksum_sha256 to SQLite attachments")
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT id, draft_id, filename, content_type, size, file_path, created_at, checksum_sha256 "
|
|
||||||
"FROM u_test__draft_attachments WHERE id='att-1'"
|
|
||||||
).fetchone()
|
|
||||||
require(
|
|
||||||
row == ('att-1', 'draft-1', 'report.pdf', 'application/pdf', 1234, '/legacy/report.pdf', '2026-01-01T00:00:00Z', None),
|
|
||||||
"V047 preserves existing attachment rows and initializes checksum as NULL",
|
|
||||||
)
|
|
||||||
require(conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='u_test__search_outbox'").fetchone() is not None, "V047 creates SQLite search_outbox")
|
|
||||||
outbox_columns = {row[1] for row in conn.execute("PRAGMA table_info(u_test__search_outbox)")}
|
|
||||||
require("claimed_until" in outbox_columns, "V047 creates an independent SQLite outbox lease column")
|
|
||||||
conn.close()
|
|
||||||
except Exception as exc:
|
|
||||||
errors.append(f"SQLite V047 migration execution failed: {exc}")
|
|
||||||
|
|
||||||
# V048 upgrade execution from a V047-style folders table, preserving existing counters.
|
|
||||||
try:
|
|
||||||
conn = sqlite3.connect(":memory:")
|
|
||||||
conn.executescript(
|
|
||||||
"CREATE TABLE u_test__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, messages_updated_at TEXT);"
|
|
||||||
"INSERT INTO u_test__folders "
|
|
||||||
"(name, total_count, unread_count, uid_validity, highest_modseq, updated_at) VALUES "
|
|
||||||
"('INBOX', 3444, 12, 77, 9001, '2026-11-01T00:00:00Z');"
|
|
||||||
)
|
|
||||||
migration = read("migrations/sqlite/V048__folder_counter_generation.sql").replace("{{p}}", "u_test__")
|
|
||||||
conn.executescript(migration)
|
|
||||||
columns = {row[1] for row in conn.execute("PRAGMA table_info(u_test__folders)")}
|
|
||||||
require("counter_generation" in columns, "V048 adds counter_generation to SQLite folders")
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT name, total_count, unread_count, uid_validity, highest_modseq, counter_generation "
|
|
||||||
"FROM u_test__folders WHERE name='INBOX'"
|
|
||||||
).fetchone()
|
|
||||||
require(
|
|
||||||
row == ('INBOX', 3444, 12, 77, 9001, 0),
|
|
||||||
"V048 preserves existing folder counters and initializes generation at zero",
|
|
||||||
)
|
|
||||||
conn.close()
|
|
||||||
except Exception as exc:
|
|
||||||
errors.append(f"SQLite V048 migration execution failed: {exc}")
|
|
||||||
|
|
||||||
# V049 creates the durable blob deletion outbox.
|
|
||||||
try:
|
|
||||||
conn = sqlite3.connect(":memory:")
|
|
||||||
migration = read("migrations/sqlite/V049__blob_delete_outbox.sql").replace("{{p}}", "u_test__")
|
|
||||||
conn.executescript(migration)
|
|
||||||
columns = {row[1] for row in conn.execute("PRAGMA table_info(u_test__blob_delete_outbox)")}
|
|
||||||
require({"id", "storage_key", "attempts", "available_at", "claimed_until", "last_error", "created_at", "updated_at"}.issubset(columns), "V049 creates SQLite blob_delete_outbox with durable retry state")
|
|
||||||
indexes = {row[1] for row in conn.execute("PRAGMA index_list(u_test__blob_delete_outbox)")}
|
|
||||||
require(any("blob_delete_outbox_ready" in name for name in indexes), "V049 creates SQLite blob cleanup readiness index")
|
|
||||||
conn.close()
|
|
||||||
except Exception as exc:
|
|
||||||
errors.append(f"SQLite V049 migration execution failed: {exc}")
|
|
||||||
|
|
||||||
# V050 adds compact-wide-row and signature insertion preferences.
|
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(":memory:")
|
conn = sqlite3.connect(":memory:")
|
||||||
conn.executescript(
|
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__mail_preferences (id INTEGER PRIMARY KEY);"
|
||||||
"CREATE TABLE u_test__identities (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__")
|
migration = read("migrations/sqlite/V050__compact_rows_and_signature_mode.sql").replace("{{p}}", "u_test__")
|
||||||
conn.executescript(migration)
|
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)")}
|
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)")}
|
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("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")
|
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__mail_preferences(id) VALUES (1)")
|
||||||
conn.execute("INSERT INTO u_test__identities(id) VALUES (1)")
|
conn.execute("INSERT INTO u_test__identities(id) VALUES (1)")
|
||||||
require(conn.execute("SELECT compact_wide_rows FROM u_test__mail_preferences WHERE id=1").fetchone()[0] == 1, "V050 applies compact-row default to existing/new SQLite rows")
|
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 signature_insert_mode FROM u_test__identities WHERE id=1").fetchone()[0] == "silent", "V050 applies silent-signature default to existing/new 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()
|
conn.close()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors.append(f"SQLite V050 migration execution failed: {exc}")
|
errors.append(f"SQLite V050 migration execution failed: {exc}")
|
||||||
|
|
||||||
# Migration/query parity across SQL engines.
|
|
||||||
for dialect in ("sqlite", "mysql", "pgsql"):
|
for dialect in ("sqlite", "mysql", "pgsql"):
|
||||||
require((ROOT / f"migrations/{dialect}/V047__blob_keys_and_search_outbox.sql").is_file(), f"V047 migration exists for {dialect}")
|
for retired in (
|
||||||
require((ROOT / f"migrations/{dialect}/V048__folder_counter_generation.sql").is_file(), f"V048 migration exists for {dialect}")
|
"V047__blob_keys_and_search_outbox.sql",
|
||||||
require((ROOT / f"migrations/{dialect}/V049__blob_delete_outbox.sql").is_file(), f"V049 migration exists for {dialect}")
|
"V048__folder_counter_generation.sql",
|
||||||
require((ROOT / f"migrations/{dialect}/V050__compact_rows_and_signature_mode.sql").is_file(), f"V050 migration exists for {dialect}")
|
"V049__blob_delete_outbox.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}",
|
||||||
|
)
|
||||||
schema_text = read(f"migrations/{dialect}/schema.sql")
|
schema_text = read(f"migrations/{dialect}/schema.sql")
|
||||||
require("Current runtime schema: V050" in schema_text and "counter_generation" in schema_text and "blob_delete_outbox" in schema_text and "compact_wide_rows" in schema_text and "signature_insert_mode" in schema_text, f"fresh {dialect} schema includes V050 compact-row/signature preferences")
|
require(
|
||||||
|
"Squashed compatibility baseline: V049. Current schema: V050." in schema_text
|
||||||
|
and "counter_generation" in schema_text
|
||||||
|
and "blob_delete_outbox" in schema_text
|
||||||
|
and "compact_wide_rows" in schema_text
|
||||||
|
and "signature_insert_mode" in schema_text
|
||||||
|
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",
|
||||||
|
)
|
||||||
queries = read(f"src/queries/{dialect}/queries.rs")
|
queries = read(f"src/queries/{dialect}/queries.rs")
|
||||||
|
require("adaptive_email_colors" in queries, f"display-preference query includes adaptive_email_colors for {dialect}")
|
||||||
|
require("message_loading_mode" in queries, f"mail-preference query includes message_loading_mode for {dialect}")
|
||||||
folder_block = re.search(r"pub mod db_folders\s*\{(.*?)\n\}", queries, re.S)
|
folder_block = re.search(r"pub mod db_folders\s*\{(.*?)\n\}", queries, re.S)
|
||||||
require(folder_block is not None and "Q_032" in folder_block.group(1) and "counter_generation = counter_generation + 1" in folder_block.group(1), f"folder counter CAS increments generation for {dialect}")
|
require(folder_block is not None and "Q_032" in folder_block.group(1) and "counter_generation = counter_generation + 1" in folder_block.group(1), f"folder counter CAS increments generation for {dialect}")
|
||||||
block_match = re.search(r"pub mod db_search_outbox\s*\{(.*?)\n\}", queries, re.S)
|
block_match = re.search(r"pub mod db_search_outbox\s*\{(.*?)\n\}", queries, re.S)
|
||||||
@@ -671,16 +637,14 @@ for dialect in ("sqlite", "mysql", "pgsql"):
|
|||||||
|
|
||||||
pool = read("src/db/pool.rs")
|
pool = read("src/db/pool.rs")
|
||||||
for dialect in ("SQLITE", "MYSQL", "PGSQL"):
|
for dialect in ("SQLITE", "MYSQL", "PGSQL"):
|
||||||
require(f"{dialect}_MIGRATIONS" in pool and "V047__blob_keys_and_search_outbox.sql" in pool, f"runtime migration catalog includes V047 ({dialect.lower()})")
|
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(f"{dialect}_MIGRATIONS" in pool and "V048__folder_counter_generation.sql" in pool, f"runtime migration catalog includes V048 ({dialect.lower()})")
|
require("pub const BASE_SCHEMA_VERSION: u32 = 49" in pool, "consolidated squashed baseline is V049")
|
||||||
require(f"{dialect}_MIGRATIONS" in pool and "V049__blob_delete_outbox.sql" in pool, f"runtime migration catalog includes V049 ({dialect.lower()})")
|
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")
|
||||||
require(f"{dialect}_MIGRATIONS" in pool and "V050__compact_rows_and_signature_mode.sql" in pool, f"runtime migration catalog includes V050 ({dialect.lower()})")
|
|
||||||
require("const BASE_SCHEMA_VERSION: u32 = 46" in pool, "legacy squashed baseline stays V046")
|
|
||||||
require("latest_schema_version" in pool, "runtime schema version is computed from migrations")
|
require("latest_schema_version" in pool, "runtime schema version is computed from migrations")
|
||||||
require(
|
require(
|
||||||
"latest_supported_sqlite = SQLITE_MIGRATIONS" in pool
|
"latest_supported_sqlite = SQLITE_MIGRATIONS" in pool
|
||||||
and "if version > latest_supported_sqlite" in pool,
|
and "if version > latest_supported_sqlite" in pool,
|
||||||
"legacy SQLite import accepts runtime migrations up to this build instead of stopping at V046",
|
"legacy SQLite import accepts the V049 baseline, applies V050 and rejects schemas newer than this build",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Tenant/import table parity and transaction-bound mutation helpers.
|
# Tenant/import table parity and transaction-bound mutation helpers.
|
||||||
@@ -777,11 +741,11 @@ require('json!({"term": {"user_hash": user_hash}})' in search_rs, "OpenSearch qu
|
|||||||
require("process_search_outbox" in search_rs and "claim(&conn" in search_rs, "OpenSearch worker uses durable claimed outbox events")
|
require("process_search_outbox" in search_rs and "claim(&conn" in search_rs, "OpenSearch worker uses durable claimed outbox events")
|
||||||
require("fn event_lease_secs(&self) -> u64" in search_rs and "let lease_secs = backend.event_lease_secs();" in search_rs and "claim(&conn, &event, 60)" not in search_rs, "OpenSearch outbox lease is derived from HTTP timeout/failover budget instead of a fixed 60 seconds")
|
require("fn event_lease_secs(&self) -> u64" in search_rs and "let lease_secs = backend.event_lease_secs();" in search_rs and "claim(&conn, &event, 60)" not in search_rs, "OpenSearch outbox lease is derived from HTTP timeout/failover budget instead of a fixed 60 seconds")
|
||||||
require("opensearch_event_lease_covers_timeout_and_failover_budget" in search_rs, "OpenSearch lease budget has a source-level unit regression test")
|
require("opensearch_event_lease_covers_timeout_and_failover_budget" in search_rs, "OpenSearch lease budget has a source-level unit regression test")
|
||||||
require("claimed_until" in read("migrations/sqlite/V047__blob_keys_and_search_outbox.sql") and "reenqueue_preserves_active_claim_lease" in read("src/db/search_outbox.rs"), "outbox re-enqueue cannot steal an active worker lease")
|
require("claimed_until" in read("migrations/sqlite/schema.sql") and "reenqueue_preserves_active_claim_lease" in read("src/db/search_outbox.rs"), "outbox re-enqueue cannot steal an active worker lease")
|
||||||
blob_delete_rs = read("src/db/blob_delete_outbox.rs")
|
blob_delete_rs = read("src/db/blob_delete_outbox.rs")
|
||||||
attachments_rs = read("src/routes/attachments.rs")
|
attachments_rs = read("src/routes/attachments.rs")
|
||||||
send_rs = read("src/routes/send.rs")
|
send_rs = read("src/routes/send.rs")
|
||||||
require("mark_failure" in blob_delete_rs and "min(3600)" in blob_delete_rs and "max_attempts" not in blob_delete_rs and "dead_letter" not in read("migrations/sqlite/V049__blob_delete_outbox.sql"), "blob deletion outbox retries indefinitely with capped backoff and no permanent dead-letter")
|
require("mark_failure" in blob_delete_rs and "min(3600)" in blob_delete_rs and "max_attempts" not in blob_delete_rs and "dead_letter" not in read("migrations/sqlite/schema.sql").split("CREATE TABLE {{p}}blob_delete_outbox", 1)[1].split("CREATE TABLE {{p}}search_outbox", 1)[0], "blob deletion outbox retries indefinitely with capped backoff and no permanent dead-letter")
|
||||||
require("unchecked_transaction()" in attachments_rs and "blob_delete_outbox::enqueue(&transaction" in attachments_rs and "delete_draft_attachment(&transaction" in attachments_rs, "single attachment deletion atomically queues blob cleanup with metadata deletion")
|
require("unchecked_transaction()" in attachments_rs and "blob_delete_outbox::enqueue(&transaction" in attachments_rs and "delete_draft_attachment(&transaction" in attachments_rs, "single attachment deletion atomically queues blob cleanup with metadata deletion")
|
||||||
require("blob_delete_outbox::enqueue(&transaction" in drafts_rs and "delete_draft(&transaction" in drafts_rs, "whole-draft deletion atomically queues every attachment blob before metadata cascade")
|
require("blob_delete_outbox::enqueue(&transaction" in drafts_rs and "delete_draft(&transaction" in drafts_rs, "whole-draft deletion atomically queues every attachment blob before metadata cascade")
|
||||||
require("blob_delete_outbox::enqueue(&transaction" in send_rs and "delete_draft(&transaction" in send_rs, "post-send draft cleanup atomically queues attachment blobs")
|
require("blob_delete_outbox::enqueue(&transaction" in send_rs and "delete_draft(&transaction" in send_rs, "post-send draft cleanup atomically queues attachment blobs")
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pub struct DisplayPreferences {
|
|||||||
pub spellcheck_enabled: bool,
|
pub spellcheck_enabled: bool,
|
||||||
pub interface_skin: String,
|
pub interface_skin: String,
|
||||||
pub preset_scale_unlocked: bool,
|
pub preset_scale_unlocked: bool,
|
||||||
|
pub adaptive_email_colors: bool,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +114,7 @@ pub struct UpdateDisplayPreferences {
|
|||||||
pub spellcheck_enabled: Option<bool>,
|
pub spellcheck_enabled: Option<bool>,
|
||||||
pub interface_skin: Option<String>,
|
pub interface_skin: Option<String>,
|
||||||
pub preset_scale_unlocked: Option<bool>,
|
pub preset_scale_unlocked: Option<bool>,
|
||||||
|
pub adaptive_email_colors: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_animation_mode_field<'de, D>(
|
fn deserialize_animation_mode_field<'de, D>(
|
||||||
@@ -179,6 +181,7 @@ fn defaults() -> DisplayPreferences {
|
|||||||
spellcheck_enabled: true,
|
spellcheck_enabled: true,
|
||||||
interface_skin: "default".into(),
|
interface_skin: "default".into(),
|
||||||
preset_scale_unlocked: false,
|
preset_scale_unlocked: false,
|
||||||
|
adaptive_email_colors: false,
|
||||||
updated_at: String::new(),
|
updated_at: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,7 +241,8 @@ pub fn get_preferences(conn: &Connection) -> Result<DisplayPreferences, String>
|
|||||||
spellcheck_enabled: row.get::<_, i32>(49)? != 0,
|
spellcheck_enabled: row.get::<_, i32>(49)? != 0,
|
||||||
interface_skin: row.get(50)?,
|
interface_skin: row.get(50)?,
|
||||||
preset_scale_unlocked: row.get::<_, i32>(51)? != 0,
|
preset_scale_unlocked: row.get::<_, i32>(51)? != 0,
|
||||||
updated_at: row.get(52)?,
|
adaptive_email_colors: row.get::<_, i32>(52)? != 0,
|
||||||
|
updated_at: row.get(53)?,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
match result {
|
match result {
|
||||||
@@ -539,6 +543,7 @@ pub fn update_preferences(
|
|||||||
set_bool!(spellcheck_enabled, "spellcheck_enabled");
|
set_bool!(spellcheck_enabled, "spellcheck_enabled");
|
||||||
set_text!(interface_skin, "interface_skin");
|
set_text!(interface_skin, "interface_skin");
|
||||||
set_bool!(preset_scale_unlocked, "preset_scale_unlocked");
|
set_bool!(preset_scale_unlocked, "preset_scale_unlocked");
|
||||||
|
set_bool!(adaptive_email_colors, "adaptive_email_colors");
|
||||||
|
|
||||||
if sets.is_empty() {
|
if sets.is_empty() {
|
||||||
return get_preferences(conn);
|
return get_preferences(conn);
|
||||||
@@ -568,7 +573,7 @@ mod tests {
|
|||||||
let update: UpdateDisplayPreferences = serde_json::from_value(serde_json::json!({
|
let update: UpdateDisplayPreferences = serde_json::from_value(serde_json::json!({
|
||||||
"theme":"dark", "language":"pl", "color_preset":"blue", "accent_color":"#336699",
|
"theme":"dark", "language":"pl", "color_preset":"blue", "accent_color":"#336699",
|
||||||
"font_size":15, "row_height":64, "folder_pane_width":250, "message_pane_width":480,
|
"font_size":15, "row_height":64, "folder_pane_width":250, "message_pane_width":480,
|
||||||
"show_avatars":false, "reduce_motion":true, "interface_skin":"roundcube-elastic", "preset_scale_unlocked":true
|
"show_avatars":false, "reduce_motion":true, "interface_skin":"roundcube-elastic", "preset_scale_unlocked":true, "adaptive_email_colors":true
|
||||||
}))
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let prefs = update_preferences(&conn, &update).unwrap();
|
let prefs = update_preferences(&conn, &update).unwrap();
|
||||||
@@ -578,6 +583,7 @@ mod tests {
|
|||||||
assert!(!prefs.show_avatars);
|
assert!(!prefs.show_avatars);
|
||||||
assert_eq!(prefs.interface_skin, "roundcube-elastic");
|
assert_eq!(prefs.interface_skin, "roundcube-elastic");
|
||||||
assert!(prefs.preset_scale_unlocked);
|
assert!(prefs.preset_scale_unlocked);
|
||||||
|
assert!(prefs.adaptive_email_colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ mod tests {
|
|||||||
&conn,
|
&conn,
|
||||||
&mail_preferences::UpdateMailPreferences {
|
&mail_preferences::UpdateMailPreferences {
|
||||||
messages_per_page: None,
|
messages_per_page: None,
|
||||||
|
message_loading_mode: None,
|
||||||
mark_read_delay_ms: None,
|
mark_read_delay_ms: None,
|
||||||
remote_images: None,
|
remote_images: None,
|
||||||
preferred_body: None,
|
preferred_body: None,
|
||||||
|
|||||||
+27
-11
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct MailPreferences {
|
pub struct MailPreferences {
|
||||||
pub messages_per_page: u32,
|
pub messages_per_page: u32,
|
||||||
|
pub message_loading_mode: String,
|
||||||
pub mark_read_delay_ms: u64,
|
pub mark_read_delay_ms: u64,
|
||||||
pub remote_images: String,
|
pub remote_images: String,
|
||||||
pub preferred_body: String,
|
pub preferred_body: String,
|
||||||
@@ -20,6 +21,7 @@ pub struct MailPreferences {
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct UpdateMailPreferences {
|
pub struct UpdateMailPreferences {
|
||||||
pub messages_per_page: Option<u32>,
|
pub messages_per_page: Option<u32>,
|
||||||
|
pub message_loading_mode: Option<String>,
|
||||||
pub mark_read_delay_ms: Option<u64>,
|
pub mark_read_delay_ms: Option<u64>,
|
||||||
pub remote_images: Option<String>,
|
pub remote_images: Option<String>,
|
||||||
pub preferred_body: Option<String>,
|
pub preferred_body: Option<String>,
|
||||||
@@ -36,17 +38,18 @@ pub fn get_preferences(conn: &Connection) -> Result<MailPreferences, String> {
|
|||||||
conn.query_row(crate::dbq!(db_mail_preferences::Q_001), [], |row| {
|
conn.query_row(crate::dbq!(db_mail_preferences::Q_001), [], |row| {
|
||||||
Ok(MailPreferences {
|
Ok(MailPreferences {
|
||||||
messages_per_page: row.get::<_, u32>(0)?,
|
messages_per_page: row.get::<_, u32>(0)?,
|
||||||
mark_read_delay_ms: row.get::<_, u64>(1)?,
|
message_loading_mode: row.get(1)?,
|
||||||
remote_images: row.get(2)?,
|
mark_read_delay_ms: row.get::<_, u64>(2)?,
|
||||||
preferred_body: row.get(3)?,
|
remote_images: row.get(3)?,
|
||||||
thread_mode: row.get(4)?,
|
preferred_body: row.get(4)?,
|
||||||
collapse_quotes: row.get::<_, i32>(5)? != 0,
|
thread_mode: row.get(5)?,
|
||||||
show_snippets: row.get::<_, i32>(6)? != 0,
|
collapse_quotes: row.get::<_, i32>(6)? != 0,
|
||||||
default_sort: row.get(7)?,
|
show_snippets: row.get::<_, i32>(7)? != 0,
|
||||||
flagged_on_top: row.get::<_, i32>(8)? != 0,
|
default_sort: row.get(8)?,
|
||||||
sync_imap_keywords: row.get::<_, i32>(9)? != 0,
|
flagged_on_top: row.get::<_, i32>(9)? != 0,
|
||||||
compact_wide_rows: row.get::<_, i32>(10)? != 0,
|
sync_imap_keywords: row.get::<_, i32>(10)? != 0,
|
||||||
updated_at: row.get(11)?,
|
compact_wide_rows: row.get::<_, i32>(11)? != 0,
|
||||||
|
updated_at: row.get(12)?,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.map_err(|error| format!("Failed to read mail preferences: {error}"))
|
.map_err(|error| format!("Failed to read mail preferences: {error}"))
|
||||||
@@ -64,6 +67,11 @@ pub fn update_preferences(
|
|||||||
{
|
{
|
||||||
return Err("messages_per_page must be between 20 and 300".to_string());
|
return Err("messages_per_page must be between 20 and 300".to_string());
|
||||||
}
|
}
|
||||||
|
if let Some(ref value) = data.message_loading_mode
|
||||||
|
&& !matches!(value.as_str(), "pagination" | "progressive" | "all")
|
||||||
|
{
|
||||||
|
return Err("message_loading_mode must be pagination, progressive or all".to_string());
|
||||||
|
}
|
||||||
if let Some(value) = data.mark_read_delay_ms
|
if let Some(value) = data.mark_read_delay_ms
|
||||||
&& value > 30_000
|
&& value > 30_000
|
||||||
{
|
{
|
||||||
@@ -102,6 +110,10 @@ pub fn update_preferences(
|
|||||||
|
|
||||||
let current = get_preferences(conn)?;
|
let current = get_preferences(conn)?;
|
||||||
let messages_per_page = data.messages_per_page.unwrap_or(current.messages_per_page);
|
let messages_per_page = data.messages_per_page.unwrap_or(current.messages_per_page);
|
||||||
|
let message_loading_mode = data
|
||||||
|
.message_loading_mode
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(¤t.message_loading_mode);
|
||||||
let mark_read_delay_ms = data
|
let mark_read_delay_ms = data
|
||||||
.mark_read_delay_ms
|
.mark_read_delay_ms
|
||||||
.unwrap_or(current.mark_read_delay_ms);
|
.unwrap_or(current.mark_read_delay_ms);
|
||||||
@@ -130,6 +142,7 @@ pub fn update_preferences(
|
|||||||
crate::dbq!(db_mail_preferences::Q_003),
|
crate::dbq!(db_mail_preferences::Q_003),
|
||||||
params![
|
params![
|
||||||
messages_per_page,
|
messages_per_page,
|
||||||
|
message_loading_mode,
|
||||||
mark_read_delay_ms,
|
mark_read_delay_ms,
|
||||||
remote_images,
|
remote_images,
|
||||||
preferred_body,
|
preferred_body,
|
||||||
@@ -157,12 +170,14 @@ mod tests {
|
|||||||
let conn = open_test_db();
|
let conn = open_test_db();
|
||||||
let defaults = get_preferences(&conn).unwrap();
|
let defaults = get_preferences(&conn).unwrap();
|
||||||
assert_eq!(defaults.messages_per_page, 300);
|
assert_eq!(defaults.messages_per_page, 300);
|
||||||
|
assert_eq!(defaults.message_loading_mode, "pagination");
|
||||||
assert_eq!(defaults.remote_images, "ask");
|
assert_eq!(defaults.remote_images, "ask");
|
||||||
|
|
||||||
let updated = update_preferences(
|
let updated = update_preferences(
|
||||||
&conn,
|
&conn,
|
||||||
&UpdateMailPreferences {
|
&UpdateMailPreferences {
|
||||||
messages_per_page: Some(100),
|
messages_per_page: Some(100),
|
||||||
|
message_loading_mode: Some("progressive".to_string()),
|
||||||
mark_read_delay_ms: Some(1500),
|
mark_read_delay_ms: Some(1500),
|
||||||
remote_images: Some("never".to_string()),
|
remote_images: Some("never".to_string()),
|
||||||
preferred_body: Some("text".to_string()),
|
preferred_body: Some("text".to_string()),
|
||||||
@@ -177,6 +192,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(updated.messages_per_page, 100);
|
assert_eq!(updated.messages_per_page, 100);
|
||||||
|
assert_eq!(updated.message_loading_mode, "progressive");
|
||||||
assert_eq!(updated.mark_read_delay_ms, 1500);
|
assert_eq!(updated.mark_read_delay_ms, 1500);
|
||||||
assert_eq!(updated.thread_mode, "flat");
|
assert_eq!(updated.thread_mode, "flat");
|
||||||
assert!(updated.flagged_on_top);
|
assert!(updated.flagged_on_top);
|
||||||
|
|||||||
+17
-59
@@ -16,65 +16,23 @@ use tokio::sync::Mutex as AsyncMutex;
|
|||||||
|
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
|
|
||||||
pub const BASE_SCHEMA_VERSION: u32 = 46;
|
pub const BASE_SCHEMA_VERSION: u32 = 49;
|
||||||
const SQLITE_SCHEMA: &str = include_str!("../../migrations/sqlite/schema.sql");
|
const SQLITE_SCHEMA: &str = include_str!("../../migrations/sqlite/schema.sql");
|
||||||
const MYSQL_SCHEMA: &str = include_str!("../../migrations/mysql/schema.sql");
|
const MYSQL_SCHEMA: &str = include_str!("../../migrations/mysql/schema.sql");
|
||||||
const PGSQL_SCHEMA: &str = include_str!("../../migrations/pgsql/schema.sql");
|
const PGSQL_SCHEMA: &str = include_str!("../../migrations/pgsql/schema.sql");
|
||||||
|
|
||||||
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[
|
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[(
|
||||||
(
|
50,
|
||||||
47,
|
include_str!("../../migrations/sqlite/V050__compact_rows_and_signature_mode.sql"),
|
||||||
include_str!("../../migrations/sqlite/V047__blob_keys_and_search_outbox.sql"),
|
)];
|
||||||
),
|
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[(
|
||||||
(
|
50,
|
||||||
48,
|
include_str!("../../migrations/mysql/V050__compact_rows_and_signature_mode.sql"),
|
||||||
include_str!("../../migrations/sqlite/V048__folder_counter_generation.sql"),
|
)];
|
||||||
),
|
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[(
|
||||||
(
|
50,
|
||||||
49,
|
include_str!("../../migrations/pgsql/V050__compact_rows_and_signature_mode.sql"),
|
||||||
include_str!("../../migrations/sqlite/V049__blob_delete_outbox.sql"),
|
)];
|
||||||
),
|
|
||||||
(
|
|
||||||
50,
|
|
||||||
include_str!("../../migrations/sqlite/V050__compact_rows_and_signature_mode.sql"),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[
|
|
||||||
(
|
|
||||||
47,
|
|
||||||
include_str!("../../migrations/mysql/V047__blob_keys_and_search_outbox.sql"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
48,
|
|
||||||
include_str!("../../migrations/mysql/V048__folder_counter_generation.sql"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
49,
|
|
||||||
include_str!("../../migrations/mysql/V049__blob_delete_outbox.sql"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
50,
|
|
||||||
include_str!("../../migrations/mysql/V050__compact_rows_and_signature_mode.sql"),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[
|
|
||||||
(
|
|
||||||
47,
|
|
||||||
include_str!("../../migrations/pgsql/V047__blob_keys_and_search_outbox.sql"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
48,
|
|
||||||
include_str!("../../migrations/pgsql/V048__folder_counter_generation.sql"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
49,
|
|
||||||
include_str!("../../migrations/pgsql/V049__blob_delete_outbox.sql"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
50,
|
|
||||||
include_str!("../../migrations/pgsql/V050__compact_rows_and_signature_mode.sql"),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
const TENANT_TABLES: &[&str] = &[
|
const TENANT_TABLES: &[&str] = &[
|
||||||
"blob_delete_outbox",
|
"blob_delete_outbox",
|
||||||
@@ -1861,7 +1819,7 @@ fn read_legacy_sqlite(path: &Path) -> Result<Vec<LegacyTableData>, String> {
|
|||||||
.map_err(|e| format!("failed to read legacy SQLite schema version: {e}"))?;
|
.map_err(|e| format!("failed to read legacy SQLite schema version: {e}"))?;
|
||||||
if version < BASE_SCHEMA_VERSION {
|
if version < BASE_SCHEMA_VERSION {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"legacy SQLite schema version {version} is older than the legacy RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with RustBox 0.1.0 first"
|
"legacy SQLite schema version {version} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade to schema V049 first"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let latest_supported_sqlite = SQLITE_MIGRATIONS
|
let latest_supported_sqlite = SQLITE_MIGRATIONS
|
||||||
@@ -2222,7 +2180,7 @@ fn apply_sqlite_migrations(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if current < BASE_SCHEMA_VERSION {
|
if current < BASE_SCHEMA_VERSION {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"database schema version {current} is older than the legacy RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with RustBox 0.1.0 first"
|
"database schema version {current} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade to schema V049 first"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let latest = latest_schema_version();
|
let latest = latest_schema_version();
|
||||||
@@ -2667,7 +2625,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn legacy_sqlite_v046_is_imported_and_backed_up() {
|
fn legacy_sqlite_v050_is_imported_and_backed_up() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
let data_dir = temp.path();
|
let data_dir = temp.path();
|
||||||
let user_hash = format!("{:x}", Sha256::digest(b"legacy-user"));
|
let user_hash = format!("{:x}", Sha256::digest(b"legacy-user"));
|
||||||
@@ -2683,7 +2641,7 @@ mod tests {
|
|||||||
legacy
|
legacy
|
||||||
.execute_batch(&tenant_seed_sql(DatabaseBackend::Sqlite, ""))
|
.execute_batch(&tenant_seed_sql(DatabaseBackend::Sqlite, ""))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
legacy.execute_batch("PRAGMA user_version=46;").unwrap();
|
legacy.execute_batch("PRAGMA user_version=50;").unwrap();
|
||||||
legacy.execute(
|
legacy.execute(
|
||||||
"INSERT INTO display_preferences(id, theme, accent_color) VALUES(1, 'dark', '#123456')",
|
"INSERT INTO display_preferences(id, theme, accent_color) VALUES(1, 'dark', '#123456')",
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ mod tests {
|
|||||||
&conn,
|
&conn,
|
||||||
&UpdateMailPreferences {
|
&UpdateMailPreferences {
|
||||||
messages_per_page: Some(100),
|
messages_per_page: Some(100),
|
||||||
|
message_loading_mode: None,
|
||||||
mark_read_delay_ms: None,
|
mark_read_delay_ms: None,
|
||||||
remote_images: None,
|
remote_images: None,
|
||||||
preferred_body: None,
|
preferred_body: None,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ pub mod db_contacts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_display_preferences {
|
pub mod db_display_preferences {
|
||||||
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, updated_at FROM display_preferences WHERE id = 1";
|
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, adaptive_email_colors, updated_at FROM display_preferences WHERE id = 1";
|
||||||
pub const Q_002: &str = "INSERT IGNORE INTO display_preferences (id) VALUES (1)";
|
pub const Q_002: &str = "INSERT IGNORE INTO display_preferences (id) VALUES (1)";
|
||||||
pub const FORMAT_001: &str = "UPDATE display_preferences SET {} WHERE id = ?";
|
pub const FORMAT_001: &str = "UPDATE display_preferences SET {} WHERE id = ?";
|
||||||
}
|
}
|
||||||
@@ -272,10 +272,10 @@ pub mod db_identities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_mail_preferences {
|
pub mod db_mail_preferences {
|
||||||
pub const Q_001: &str = "SELECT messages_per_page, mark_read_delay_ms, remote_images, preferred_body, thread_mode, collapse_quotes, show_snippets, default_sort, flagged_on_top, sync_imap_keywords, compact_wide_rows, updated_at FROM mail_preferences WHERE id = 1";
|
pub const Q_001: &str = "SELECT messages_per_page, message_loading_mode, mark_read_delay_ms, remote_images, preferred_body, thread_mode, collapse_quotes, show_snippets, default_sort, flagged_on_top, sync_imap_keywords, compact_wide_rows, updated_at FROM mail_preferences WHERE id = 1";
|
||||||
pub const Q_002: &str =
|
pub const Q_002: &str =
|
||||||
"INSERT IGNORE INTO mail_preferences (id, messages_per_page) VALUES (1, 300)";
|
"INSERT IGNORE INTO mail_preferences (id, messages_per_page) VALUES (1, 300)";
|
||||||
pub const Q_003: &str = "UPDATE mail_preferences SET messages_per_page = ?, mark_read_delay_ms = ?, remote_images = ?, preferred_body = ?, thread_mode = ?, collapse_quotes = ?, show_snippets = ?, default_sort = ?, flagged_on_top = ?, sync_imap_keywords = ?, compact_wide_rows = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1";
|
pub const Q_003: &str = "UPDATE mail_preferences SET messages_per_page = ?, message_loading_mode = ?, mark_read_delay_ms = ?, remote_images = ?, preferred_body = ?, thread_mode = ?, collapse_quotes = ?, show_snippets = ?, default_sort = ?, flagged_on_top = ?, sync_imap_keywords = ?, compact_wide_rows = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_messages {
|
pub mod db_messages {
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ pub mod db_contacts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_display_preferences {
|
pub mod db_display_preferences {
|
||||||
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, updated_at FROM display_preferences WHERE id = 1";
|
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, adaptive_email_colors, updated_at FROM display_preferences WHERE id = 1";
|
||||||
pub const Q_002: &str =
|
pub const Q_002: &str =
|
||||||
"INSERT INTO display_preferences (id) VALUES (1) ON CONFLICT (id) DO NOTHING";
|
"INSERT INTO display_preferences (id) VALUES (1) ON CONFLICT (id) DO NOTHING";
|
||||||
pub const FORMAT_001: &str = "UPDATE display_preferences SET {} WHERE id = ${}";
|
pub const FORMAT_001: &str = "UPDATE display_preferences SET {} WHERE id = ${}";
|
||||||
@@ -272,9 +272,9 @@ pub mod db_identities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_mail_preferences {
|
pub mod db_mail_preferences {
|
||||||
pub const Q_001: &str = "SELECT messages_per_page, mark_read_delay_ms, remote_images, preferred_body, thread_mode, collapse_quotes, show_snippets, default_sort, flagged_on_top, sync_imap_keywords, compact_wide_rows, updated_at FROM mail_preferences WHERE id = 1";
|
pub const Q_001: &str = "SELECT messages_per_page, message_loading_mode, mark_read_delay_ms, remote_images, preferred_body, thread_mode, collapse_quotes, show_snippets, default_sort, flagged_on_top, sync_imap_keywords, compact_wide_rows, updated_at FROM mail_preferences WHERE id = 1";
|
||||||
pub const Q_002: &str = "INSERT INTO mail_preferences (id, messages_per_page) VALUES (1, 300) ON CONFLICT (id) DO NOTHING";
|
pub const Q_002: &str = "INSERT INTO mail_preferences (id, messages_per_page) VALUES (1, 300) ON CONFLICT (id) DO NOTHING";
|
||||||
pub const Q_003: &str = "UPDATE mail_preferences SET messages_per_page = $1, mark_read_delay_ms = $2, remote_images = $3, preferred_body = $4, thread_mode = $5, collapse_quotes = $6, show_snippets = $7, default_sort = $8, flagged_on_top = $9, sync_imap_keywords = $10, compact_wide_rows = $11, updated_at = CURRENT_TIMESTAMP::text WHERE id = 1";
|
pub const Q_003: &str = "UPDATE mail_preferences SET messages_per_page = $1, message_loading_mode = $2, mark_read_delay_ms = $3, remote_images = $4, preferred_body = $5, thread_mode = $6, collapse_quotes = $7, show_snippets = $8, default_sort = $9, flagged_on_top = $10, sync_imap_keywords = $11, compact_wide_rows = $12, updated_at = CURRENT_TIMESTAMP::text WHERE id = 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_messages {
|
pub mod db_messages {
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ pub mod db_contacts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_display_preferences {
|
pub mod db_display_preferences {
|
||||||
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, updated_at FROM display_preferences WHERE id = 1";
|
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, adaptive_email_colors, updated_at FROM display_preferences WHERE id = 1";
|
||||||
pub const Q_002: &str = "INSERT OR IGNORE INTO display_preferences (id) VALUES (1)";
|
pub const Q_002: &str = "INSERT OR IGNORE INTO display_preferences (id) VALUES (1)";
|
||||||
pub const FORMAT_001: &str = "UPDATE display_preferences SET {} WHERE id = ?{}";
|
pub const FORMAT_001: &str = "UPDATE display_preferences SET {} WHERE id = ?{}";
|
||||||
}
|
}
|
||||||
@@ -271,10 +271,10 @@ pub mod db_identities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_mail_preferences {
|
pub mod db_mail_preferences {
|
||||||
pub const Q_001: &str = "SELECT messages_per_page, mark_read_delay_ms, remote_images, preferred_body, thread_mode, collapse_quotes, show_snippets, default_sort, flagged_on_top, sync_imap_keywords, compact_wide_rows, updated_at FROM mail_preferences WHERE id = 1";
|
pub const Q_001: &str = "SELECT messages_per_page, message_loading_mode, mark_read_delay_ms, remote_images, preferred_body, thread_mode, collapse_quotes, show_snippets, default_sort, flagged_on_top, sync_imap_keywords, compact_wide_rows, updated_at FROM mail_preferences WHERE id = 1";
|
||||||
pub const Q_002: &str =
|
pub const Q_002: &str =
|
||||||
"INSERT OR IGNORE INTO mail_preferences (id, messages_per_page) VALUES (1, 300)";
|
"INSERT OR IGNORE INTO mail_preferences (id, messages_per_page) VALUES (1, 300)";
|
||||||
pub const Q_003: &str = "UPDATE mail_preferences SET messages_per_page = ?1, mark_read_delay_ms = ?2, remote_images = ?3, preferred_body = ?4, thread_mode = ?5, collapse_quotes = ?6, show_snippets = ?7, default_sort = ?8, flagged_on_top = ?9, sync_imap_keywords = ?10, compact_wide_rows = ?11, updated_at = datetime('now') WHERE id = 1";
|
pub const Q_003: &str = "UPDATE mail_preferences SET messages_per_page = ?1, message_loading_mode = ?2, mark_read_delay_ms = ?3, remote_images = ?4, preferred_body = ?5, thread_mode = ?6, collapse_quotes = ?7, show_snippets = ?8, default_sort = ?9, flagged_on_top = ?10, sync_imap_keywords = ?11, compact_wide_rows = ?12, updated_at = datetime('now') WHERE id = 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_messages {
|
pub mod db_messages {
|
||||||
|
|||||||
+37
-3
@@ -244,7 +244,7 @@
|
|||||||
.message-load-actions {
|
.message-load-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -254,6 +254,40 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-pagination {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 30px auto 30px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 7px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-pagination.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-arrow {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-arrow svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-page-status {
|
||||||
|
min-width: 88px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.load-more {
|
.load-more {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -1355,6 +1389,7 @@ body.reader-fullscreen-open {
|
|||||||
min-width: max-content;
|
min-width: max-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optional wide-pane single-line message rows. The preference is on by default,
|
/* Optional wide-pane single-line message rows. The preference is on by default,
|
||||||
but the layout only activates once the message pane has enough horizontal room. */
|
but the layout only activates once the message pane has enough horizontal room. */
|
||||||
@container message-pane (min-width: 520px) {
|
@container message-pane (min-width: 520px) {
|
||||||
@@ -1443,5 +1478,4 @@ body.reader-fullscreen-open {
|
|||||||
html[data-compact-wide-rows="true"] .message-list.selection-mode .message-row .message-icons {
|
html[data-compact-wide-rows="true"] .message-list.selection-mode .message-row .message-icons {
|
||||||
grid-column: 6;
|
grid-column: 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +274,22 @@
|
|||||||
max-width: 760px;
|
max-width: 760px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.appearance-toggle-described {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-toggle-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-toggle-copy small {
|
||||||
|
display: block;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
.compose-limit-summary {
|
.compose-limit-summary {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"loading_quota": "Loading quota…",
|
"loading_quota": "Loading quota…",
|
||||||
"of": "of",
|
"of": "of",
|
||||||
|
"page": "Page",
|
||||||
"used": "used",
|
"used": "used",
|
||||||
"server_limit_unavailable": "server limit unavailable",
|
"server_limit_unavailable": "server limit unavailable",
|
||||||
"quota_unavailable": "Quota unavailable",
|
"quota_unavailable": "Quota unavailable",
|
||||||
@@ -56,5 +57,7 @@
|
|||||||
"move_copy": "Move / copy",
|
"move_copy": "Move / copy",
|
||||||
"move_to": "Move to…",
|
"move_to": "Move to…",
|
||||||
"copy_to": "Copy to…",
|
"copy_to": "Copy to…",
|
||||||
"login_success": "Successfully logged in"
|
"login_success": "Successfully logged in",
|
||||||
|
"adaptive_email_colors": "Adapt HTML mail colors to current theme",
|
||||||
|
"adaptive_email_colors_note": "Adjusts neutral backgrounds and low-contrast text while preserving layout, images and brand colors."
|
||||||
}
|
}
|
||||||
+12
-1
@@ -40,6 +40,7 @@
|
|||||||
"loading": "Ładowanie…",
|
"loading": "Ładowanie…",
|
||||||
"loading_quota": "Ładowanie limitu…",
|
"loading_quota": "Ładowanie limitu…",
|
||||||
"of": "z",
|
"of": "z",
|
||||||
|
"page": "Strona",
|
||||||
"used": "wykorzystano",
|
"used": "wykorzystano",
|
||||||
"server_limit_unavailable": "limit serwera niedostępny",
|
"server_limit_unavailable": "limit serwera niedostępny",
|
||||||
"quota_unavailable": "Limit niedostępny",
|
"quota_unavailable": "Limit niedostępny",
|
||||||
@@ -189,6 +190,14 @@
|
|||||||
"Message-list background override": "Nadpisanie tła listy wiadomości",
|
"Message-list background override": "Nadpisanie tła listy wiadomości",
|
||||||
"Reader background override": "Nadpisanie tła czytnika",
|
"Reader background override": "Nadpisanie tła czytnika",
|
||||||
"Control message-list and reader behavior for this account.": "Ustaw zachowanie listy wiadomości i czytnika dla tego konta.",
|
"Control message-list and reader behavior for this account.": "Ustaw zachowanie listy wiadomości i czytnika dla tego konta.",
|
||||||
|
"Message loading": "Ładowanie wiadomości",
|
||||||
|
"Pagination": "Paginacja",
|
||||||
|
"Smooth loading": "Płynne doładowywanie",
|
||||||
|
"Load all messages": "Załaduj wszystkie wiadomości",
|
||||||
|
"Mailbox pages": "Strony skrzynki",
|
||||||
|
"Previous page": "Poprzednia strona",
|
||||||
|
"Next page": "Następna strona",
|
||||||
|
"Page 1 of 1": "Strona 1 z 1",
|
||||||
"Messages per page": "Wiadomości na stronę",
|
"Messages per page": "Wiadomości na stronę",
|
||||||
"Mark as read": "Oznacz jako przeczytane",
|
"Mark as read": "Oznacz jako przeczytane",
|
||||||
"Immediately": "Natychmiast",
|
"Immediately": "Natychmiast",
|
||||||
@@ -655,5 +664,7 @@
|
|||||||
"move_copy": "Przenieś / kopiuj",
|
"move_copy": "Przenieś / kopiuj",
|
||||||
"move_to": "Przenieś do…",
|
"move_to": "Przenieś do…",
|
||||||
"copy_to": "Kopiuj do…",
|
"copy_to": "Kopiuj do…",
|
||||||
"login_success": "Pomyślnie zalogowano"
|
"login_success": "Pomyślnie zalogowano",
|
||||||
|
"adaptive_email_colors": "Dopasuj kolory maili HTML do bieżącego motywu",
|
||||||
|
"adaptive_email_colors_note": "Dostosowuje neutralne tła i tekst o niskim kontraście bez zmiany układu, obrazów i kolorów firmowych."
|
||||||
}
|
}
|
||||||
+30
-9
@@ -116,10 +116,10 @@
|
|||||||
</svg></button></div>
|
</svg></button></div>
|
||||||
<nav id="tag-list" class="folder-list tag-list" aria-label="Message labels"></nav>
|
<nav id="tag-list" class="folder-list tag-list" aria-label="Message labels"></nav>
|
||||||
<div id="folder-quota" class="folder-quota" aria-live="polite">
|
<div id="folder-quota" class="folder-quota" aria-live="polite">
|
||||||
<div class="folder-quota-label"><span>IMAP storage</span><span id="folder-quota-text">Loading…</span></div>
|
<div class="folder-quota-label"><span>Mailbox storage</span><span id="folder-quota-text">Loading…</span></div>
|
||||||
<div class="folder-quota-track"><span id="folder-quota-bar"></span></div>
|
<div class="folder-quota-track"><span id="folder-quota-bar"></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="folder-pane-footer"><span id="mail-server-label">IMAP</span><span>RustBox __RUSTBOX_VERSION__</span>
|
<div class="folder-pane-footer"><span id="mail-server-label">RustBox</span><span>__RUSTBOX_VERSION__</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="folder-resizer" class="pane-resizer" role="separator" aria-orientation="vertical"
|
<div id="folder-resizer" class="pane-resizer" role="separator" aria-orientation="vertical"
|
||||||
aria-label="Resize folder pane"></div>
|
aria-label="Resize folder pane"></div>
|
||||||
@@ -180,9 +180,19 @@
|
|||||||
<button id="selection-done" class="button quiet" type="button">Done</button>
|
<button id="selection-done" class="button quiet" type="button">Done</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="message-list" class="message-list" aria-live="polite"></div>
|
<div id="message-list" class="message-list" aria-live="polite"></div>
|
||||||
|
<div id="message-pagination" class="message-pagination hidden" aria-label="Mailbox pages">
|
||||||
|
<button id="page-prev" class="icon-button pagination-arrow" type="button" title="Previous page"
|
||||||
|
aria-label="Previous page"><svg aria-hidden="true">
|
||||||
|
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-arrow-left"></use>
|
||||||
|
</svg></button>
|
||||||
|
<span id="page-status" class="message-page-status">Page 1 of 1</span>
|
||||||
|
<button id="page-next" class="icon-button pagination-arrow" type="button" title="Next page"
|
||||||
|
aria-label="Next page"><svg aria-hidden="true">
|
||||||
|
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-chevron-right"></use>
|
||||||
|
</svg></button>
|
||||||
|
</div>
|
||||||
<div id="message-load-actions" class="message-load-actions hidden">
|
<div id="message-load-actions" class="message-load-actions hidden">
|
||||||
<button id="load-more" class="button quiet load-more" type="button">Load more</button>
|
<button id="load-more" class="button quiet load-more" type="button">Load more</button>
|
||||||
<button id="load-all" class="button quiet load-more" type="button">Load all</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="message-resizer" class="pane-resizer" role="separator" aria-orientation="vertical"
|
<div id="message-resizer" class="pane-resizer" role="separator" aria-orientation="vertical"
|
||||||
aria-label="Resize message list"></div>
|
aria-label="Resize message list"></div>
|
||||||
@@ -311,11 +321,11 @@
|
|||||||
<form id="compose-form" class="dialog-card compose-card" method="dialog">
|
<form id="compose-form" class="dialog-card compose-card" method="dialog">
|
||||||
<header class="dialog-header"><strong id="compose-title">New message</strong>
|
<header class="dialog-header"><strong id="compose-title">New message</strong>
|
||||||
<div class="compose-header-actions"><button type="button" id="compose-fields-toggle" class="button quiet small"
|
<div class="compose-header-actions"><button type="button" id="compose-fields-toggle" class="button quiet small"
|
||||||
aria-expanded="true" title="Hide addressing fields">Hide fields</button><button type="button" id="compose-recover" class="button quiet small"
|
aria-expanded="true" title="Hide addressing fields">Hide fields</button><button type="button"
|
||||||
title="Recover a saved draft">Recover draft</button><button type="button" id="compose-fullscreen"
|
id="compose-recover" class="button quiet small" title="Recover a saved draft">Recover draft</button><button
|
||||||
class="icon-button compose-fullscreen-toggle" aria-label="Toggle full screen composer" aria-pressed="false"
|
type="button" id="compose-fullscreen" class="icon-button compose-fullscreen-toggle"
|
||||||
title="Full screen">⛶</button><button type="button" class="icon-button close-dialog"
|
aria-label="Toggle full screen composer" aria-pressed="false" title="Full screen">⛶</button><button
|
||||||
aria-label="Close"><svg>
|
type="button" class="icon-button close-dialog" aria-label="Close"><svg>
|
||||||
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-x"></use>
|
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-x"></use>
|
||||||
</svg></button></div>
|
</svg></button></div>
|
||||||
</header>
|
</header>
|
||||||
@@ -444,7 +454,8 @@
|
|||||||
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-file-text"></use>
|
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-file-text"></use>
|
||||||
</svg></button>
|
</svg></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="compose-image-actions" class="compose-image-actions hidden" role="toolbar" aria-label="Inline image actions">
|
<div id="compose-image-actions" class="compose-image-actions hidden" role="toolbar"
|
||||||
|
aria-label="Inline image actions">
|
||||||
<span class="compose-image-actions-label">Inline image</span>
|
<span class="compose-image-actions-label">Inline image</span>
|
||||||
<button type="button" class="button quiet small" data-image-width="25">25%</button>
|
<button type="button" class="button quiet small" data-image-width="25">25%</button>
|
||||||
<button type="button" class="button quiet small" data-image-width="50">50%</button>
|
<button type="button" class="button quiet small" data-image-width="50">50%</button>
|
||||||
@@ -740,6 +751,11 @@
|
|||||||
flags</span></label>
|
flags</span></label>
|
||||||
<label class="checkbox-row"><input id="pref-reduce-motion" type="checkbox"><span
|
<label class="checkbox-row"><input id="pref-reduce-motion" type="checkbox"><span
|
||||||
data-i18n="reduce_motion">Reduce motion</span></label>
|
data-i18n="reduce_motion">Reduce motion</span></label>
|
||||||
|
<label class="checkbox-row appearance-toggle-described"><input id="pref-adaptive-email-colors"
|
||||||
|
type="checkbox"><span class="appearance-toggle-copy"><span data-i18n="adaptive_email_colors">Adapt
|
||||||
|
HTML mail colors to current theme</span><small class="muted"
|
||||||
|
data-i18n="adaptive_email_colors_note">Adjusts neutral backgrounds and low-contrast text while
|
||||||
|
preserving layout, images and brand colors.</small></span></label>
|
||||||
</div>
|
</div>
|
||||||
<details class="appearance-advanced">
|
<details class="appearance-advanced">
|
||||||
<summary>Advanced interface styling</summary>
|
<summary>Advanced interface styling</summary>
|
||||||
@@ -808,6 +824,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-grid">
|
<div class="settings-grid">
|
||||||
|
<label><span>Message loading</span><select id="mail-pref-loading-mode">
|
||||||
|
<option value="pagination">Pagination</option>
|
||||||
|
<option value="progressive">Smooth loading</option>
|
||||||
|
<option value="all">Load all messages</option>
|
||||||
|
</select></label>
|
||||||
<label><span>Messages per page</span><select id="mail-pref-per-page">
|
<label><span>Messages per page</span><select id="mail-pref-per-page">
|
||||||
<option value="20">20</option>
|
<option value="20">20</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
page: 0,
|
page: 0,
|
||||||
perPage: 300,
|
perPage: 300,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
loadingMoreMessages: false,
|
||||||
|
loadingAllMessages: false,
|
||||||
messages: [],
|
messages: [],
|
||||||
selected: null,
|
selected: null,
|
||||||
selectedMessages: new Set(),
|
selectedMessages: new Set(),
|
||||||
@@ -45,7 +47,7 @@
|
|||||||
savedSearches: [],
|
savedSearches: [],
|
||||||
responseTemplates: [],
|
responseTemplates: [],
|
||||||
notificationPrefs: { enabled: true, sound: false, folders: ["INBOX"], dnd_enabled: false, dnd_start: "22:00", dnd_end: "07:00" },
|
notificationPrefs: { enabled: true, sound: false, folders: ["INBOX"], dnd_enabled: false, dnd_start: "22:00", dnd_end: "07:00" },
|
||||||
mailPrefs: { messages_per_page: 300, mark_read_delay_ms: 0, remote_images: "ask", preferred_body: "auto", thread_mode: "threaded", collapse_quotes: true, show_snippets: true, default_sort: "date_desc", flagged_on_top: false, sync_imap_keywords: false, compact_wide_rows: true },
|
mailPrefs: { messages_per_page: 300, message_loading_mode: "pagination", mark_read_delay_ms: 0, remote_images: "ask", preferred_body: "auto", thread_mode: "threaded", collapse_quotes: true, show_snippets: true, default_sort: "date_desc", flagged_on_top: false, sync_imap_keywords: false, compact_wide_rows: true },
|
||||||
currentFolderFlaggedOnTop: false,
|
currentFolderFlaggedOnTop: false,
|
||||||
markReadTimer: null,
|
markReadTimer: null,
|
||||||
ws: null,
|
ws: null,
|
||||||
@@ -98,7 +100,7 @@
|
|||||||
colorPreset: "sage", accentColor: "#6f9c86", backgroundColor: "#121513", panelColor: "#181c19", textColor: "#edf0ec", fontFamily: "system", fontSize: 13,
|
colorPreset: "sage", accentColor: "#6f9c86", backgroundColor: "#121513", panelColor: "#181c19", textColor: "#edf0ec", fontFamily: "system", fontSize: 13,
|
||||||
rowHeight: 58, folderPaneWidth: 228, messagePaneWidth: 420, borderRadius: 4,
|
rowHeight: 58, folderPaneWidth: 228, messagePaneWidth: 420, borderRadius: 4,
|
||||||
contrast: "normal", showAvatars: true, showAttachmentIcons: true, showFlags: true, reduceMotion: false, dateFormat: "locale", timeFormat: "locale",
|
contrast: "normal", showAvatars: true, showAttachmentIcons: true, showFlags: true, reduceMotion: false, dateFormat: "locale", timeFormat: "locale",
|
||||||
iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 29, readerMaxWidth: 0, composerWidth: 900, selectionColor: "#28352e", unreadFontWeight: 730, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false, folderBackground: "", messageBackground: "", readerBackground: "",
|
iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 29, readerMaxWidth: 0, composerWidth: 900, selectionColor: "#28352e", unreadFontWeight: 730, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false, folderBackground: "", messageBackground: "", readerBackground: "", adaptiveEmailColors: false,
|
||||||
},
|
},
|
||||||
translations: {},
|
translations: {},
|
||||||
quotaWarningLevel: null,
|
quotaWarningLevel: null,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const preferenceMap = {
|
|||||||
composeAutosaveMs: "compose_autosave_ms", defaultPriority: "default_priority", requestMdnDefault: "request_mdn_default", attachmentReminder: "attachment_reminder", composeFontFamily: "compose_font_family", replyPosition: "reply_position", quoteStyle: "quote_style", forwardMode: "forward_mode", spellcheckEnabled: "spellcheck_enabled",
|
composeAutosaveMs: "compose_autosave_ms", defaultPriority: "default_priority", requestMdnDefault: "request_mdn_default", attachmentReminder: "attachment_reminder", composeFontFamily: "compose_font_family", replyPosition: "reply_position", quoteStyle: "quote_style", forwardMode: "forward_mode", spellcheckEnabled: "spellcheck_enabled",
|
||||||
dateFormat: "date_format", timeFormat: "time_format", iconSize: "icon_size", toolbarDensity: "toolbar_density", sidebarItemHeight: "sidebar_item_height",
|
dateFormat: "date_format", timeFormat: "time_format", iconSize: "icon_size", toolbarDensity: "toolbar_density", sidebarItemHeight: "sidebar_item_height",
|
||||||
readerMaxWidth: "reader_max_width", composerWidth: "composer_width", selectionColor: "selection_color", unreadFontWeight: "unread_font_weight", unreadBadgeStyle: "unread_badge_style",
|
readerMaxWidth: "reader_max_width", composerWidth: "composer_width", selectionColor: "selection_color", unreadFontWeight: "unread_font_weight", unreadBadgeStyle: "unread_badge_style",
|
||||||
compactHeaders: "compact_headers", showBorders: "show_borders", scrollbarDensity: "scrollbar_density", monochrome: "monochrome", folderBackground: "folder_background", messageBackground: "message_background", readerBackground: "reader_background",
|
compactHeaders: "compact_headers", showBorders: "show_borders", scrollbarDensity: "scrollbar_density", monochrome: "monochrome", folderBackground: "folder_background", messageBackground: "message_background", readerBackground: "reader_background", adaptiveEmailColors: "adaptive_email_colors",
|
||||||
};
|
};
|
||||||
const preferenceTimers = new Map();
|
const preferenceTimers = new Map();
|
||||||
let presetPromotionTimer = null;
|
let presetPromotionTimer = null;
|
||||||
@@ -273,7 +273,7 @@ function applyPreferences() {
|
|||||||
"#pref-selection-color": visual.selectionColor, "#pref-unread-weight": visual.unreadFontWeight, "#pref-unread-badge": visual.unreadBadgeStyle, "#pref-scrollbar-density": visual.scrollbarDensity,
|
"#pref-selection-color": visual.selectionColor, "#pref-unread-weight": visual.unreadFontWeight, "#pref-unread-badge": visual.unreadBadgeStyle, "#pref-scrollbar-density": visual.scrollbarDensity,
|
||||||
};
|
};
|
||||||
Object.entries(values).forEach(([selector, value]) => { const node = $(selector); if (node) node.value = value; });
|
Object.entries(values).forEach(([selector, value]) => { const node = $(selector); if (node) node.value = value; });
|
||||||
const checks = { "#pref-preset-scale-unlocked": p.presetScaleUnlocked, "#pref-avatars": p.showAvatars, "#pref-attachment-icons": p.showAttachmentIcons, "#pref-flags": p.showFlags, "#pref-reduce-motion": p.reduceMotion, "#compose-pref-mdn": p.requestMdnDefault, "#compose-pref-attachment-reminder": p.attachmentReminder, "#compose-pref-spellcheck": p.spellcheckEnabled, "#pref-compact-headers": visual.compactHeaders, "#pref-show-borders": visual.showBorders, "#pref-monochrome": visual.monochrome, "#pref-folder-bg-enabled": Boolean(p.folderBackground), "#pref-message-bg-enabled": Boolean(p.messageBackground), "#pref-reader-bg-enabled": Boolean(p.readerBackground) };
|
const checks = { "#pref-preset-scale-unlocked": p.presetScaleUnlocked, "#pref-adaptive-email-colors": p.adaptiveEmailColors, "#pref-avatars": p.showAvatars, "#pref-attachment-icons": p.showAttachmentIcons, "#pref-flags": p.showFlags, "#pref-reduce-motion": p.reduceMotion, "#compose-pref-mdn": p.requestMdnDefault, "#compose-pref-attachment-reminder": p.attachmentReminder, "#compose-pref-spellcheck": p.spellcheckEnabled, "#pref-compact-headers": visual.compactHeaders, "#pref-show-borders": visual.showBorders, "#pref-monochrome": visual.monochrome, "#pref-folder-bg-enabled": Boolean(p.folderBackground), "#pref-message-bg-enabled": Boolean(p.messageBackground), "#pref-reader-bg-enabled": Boolean(p.readerBackground) };
|
||||||
Object.entries(checks).forEach(([selector, value]) => { const node = $(selector); if (node) node.checked = Boolean(value); });
|
Object.entries(checks).forEach(([selector, value]) => { const node = $(selector); if (node) node.checked = Boolean(value); });
|
||||||
const outputs = { "#pref-accent-value": displayedAccent, "#pref-background-value": displayedBackground, "#pref-panel-value": displayedPanel, "#pref-text-color-value": displayedText, "#pref-font-size-value": `${visual.fontSize} px`, "#pref-row-height-value": `${visual.rowHeight} px`, "#pref-folder-width-value": `${visual.folderPaneWidth} px`, "#pref-message-width-value": `${visual.messagePaneWidth} px`, "#pref-radius-value": `${visual.borderRadius} px`, "#pref-icon-size-value": `${visual.iconSize} px`, "#pref-sidebar-height-value": `${visual.sidebarItemHeight} px`, "#pref-composer-width-value": `${visual.composerWidth} px`, "#pref-selection-color-value": visual.selectionColor, "#pref-unread-weight-value": String(visual.unreadFontWeight) };
|
const outputs = { "#pref-accent-value": displayedAccent, "#pref-background-value": displayedBackground, "#pref-panel-value": displayedPanel, "#pref-text-color-value": displayedText, "#pref-font-size-value": `${visual.fontSize} px`, "#pref-row-height-value": `${visual.rowHeight} px`, "#pref-folder-width-value": `${visual.folderPaneWidth} px`, "#pref-message-width-value": `${visual.messagePaneWidth} px`, "#pref-radius-value": `${visual.borderRadius} px`, "#pref-icon-size-value": `${visual.iconSize} px`, "#pref-sidebar-height-value": `${visual.sidebarItemHeight} px`, "#pref-composer-width-value": `${visual.composerWidth} px`, "#pref-selection-color-value": visual.selectionColor, "#pref-unread-weight-value": String(visual.unreadFontWeight) };
|
||||||
Object.entries(outputs).forEach(([selector, value]) => { const node = $(selector); if (node) node.textContent = value; });
|
Object.entries(outputs).forEach(([selector, value]) => { const node = $(selector); if (node) node.textContent = value; });
|
||||||
@@ -314,7 +314,7 @@ function normalizeServerPreferences(data) {
|
|||||||
rowHeight: data.row_height || 58, folderPaneWidth: data.folder_pane_width || 228, messagePaneWidth: data.message_pane_width || 420,
|
rowHeight: data.row_height || 58, folderPaneWidth: data.folder_pane_width || 228, messagePaneWidth: data.message_pane_width || 420,
|
||||||
borderRadius: data.border_radius ?? 4, contrast: data.contrast || "normal", showAvatars: data.show_avatars !== false,
|
borderRadius: data.border_radius ?? 4, contrast: data.contrast || "normal", showAvatars: data.show_avatars !== false,
|
||||||
showAttachmentIcons: data.show_attachment_icons !== false, showFlags: data.show_flags !== false, reduceMotion: Boolean(data.reduce_motion), dateFormat: data.date_format || "locale", timeFormat: data.time_format || "locale",
|
showAttachmentIcons: data.show_attachment_icons !== false, showFlags: data.show_flags !== false, reduceMotion: Boolean(data.reduce_motion), dateFormat: data.date_format || "locale", timeFormat: data.time_format || "locale",
|
||||||
iconSize: Number(data.icon_size || 18), toolbarDensity: data.toolbar_density || "normal", sidebarItemHeight: Number(data.sidebar_item_height || 29), readerMaxWidth: Number(data.reader_max_width || 0), composerWidth: Number(data.composer_width || 900), selectionColor: data.selection_color || "#28352e", unreadFontWeight: Number(data.unread_font_weight || 730), unreadBadgeStyle: data.unread_badge_style || "dot", compactHeaders: Boolean(data.compact_headers), showBorders: data.show_borders !== false, scrollbarDensity: data.scrollbar_density || "thin", monochrome: Boolean(data.monochrome), folderBackground: data.folder_background || "", messageBackground: data.message_background || "", readerBackground: data.reader_background || "",
|
iconSize: Number(data.icon_size || 18), toolbarDensity: data.toolbar_density || "normal", sidebarItemHeight: Number(data.sidebar_item_height || 29), readerMaxWidth: Number(data.reader_max_width || 0), composerWidth: Number(data.composer_width || 900), selectionColor: data.selection_color || "#28352e", unreadFontWeight: Number(data.unread_font_weight || 730), unreadBadgeStyle: data.unread_badge_style || "dot", compactHeaders: Boolean(data.compact_headers), showBorders: data.show_borders !== false, scrollbarDensity: data.scrollbar_density || "thin", monochrome: Boolean(data.monochrome), folderBackground: data.folder_background || "", messageBackground: data.message_background || "", readerBackground: data.reader_background || "", adaptiveEmailColors: Boolean(data.adaptive_email_colors),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,6 +331,7 @@ async function loadMailPreferences() {
|
|||||||
state.mailPrefs = { ...state.mailPrefs, ...prefs };
|
state.mailPrefs = { ...state.mailPrefs, ...prefs };
|
||||||
state.perPage = Number(state.mailPrefs.messages_per_page || 300);
|
state.perPage = Number(state.mailPrefs.messages_per_page || 300);
|
||||||
const values = {
|
const values = {
|
||||||
|
"#mail-pref-loading-mode": state.mailPrefs.message_loading_mode || "pagination",
|
||||||
"#mail-pref-per-page": String(state.perPage),
|
"#mail-pref-per-page": String(state.perPage),
|
||||||
"#mail-pref-read-delay": String(state.mailPrefs.mark_read_delay_ms || 0),
|
"#mail-pref-read-delay": String(state.mailPrefs.mark_read_delay_ms || 0),
|
||||||
"#mail-pref-remote-images": state.mailPrefs.remote_images || "ask",
|
"#mail-pref-remote-images": state.mailPrefs.remote_images || "ask",
|
||||||
@@ -374,6 +375,7 @@ async function saveCompactWideRowsAppearance(enabled) {
|
|||||||
|
|
||||||
async function saveMailPreferences() {
|
async function saveMailPreferences() {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
message_loading_mode: $("#mail-pref-loading-mode").value,
|
||||||
messages_per_page: Number($("#mail-pref-per-page").value),
|
messages_per_page: Number($("#mail-pref-per-page").value),
|
||||||
mark_read_delay_ms: Number($("#mail-pref-read-delay").value),
|
mark_read_delay_ms: Number($("#mail-pref-read-delay").value),
|
||||||
remote_images: $("#mail-pref-remote-images").value,
|
remote_images: $("#mail-pref-remote-images").value,
|
||||||
@@ -564,7 +566,7 @@ function savePreference(key, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resetAppearance() {
|
async function resetAppearance() {
|
||||||
const payload = { theme: root.dataset.defaultTheme || "system", interface_skin: "default", preset_scale_unlocked: false, density: "compact", preview_pane: "right", ui_scale: "normal", color_preset: "sage", accent_color: "#6f9c86", background_color: "#121513", panel_color: "#181c19", text_color: "#edf0ec", font_family: "system", font_size: 13, row_height: 58, folder_pane_width: 228, message_pane_width: 420, border_radius: 4, contrast: "normal", show_avatars: true, show_attachment_icons: true, show_flags: true, reduce_motion: false, date_format: "locale", time_format: "locale", icon_size: 18, toolbar_density: "normal", sidebar_item_height: 29, reader_max_width: 0, composer_width: 900, selection_color: "#28352e", unread_font_weight: 730, unread_badge_style: "dot", compact_headers: false, show_borders: true, scrollbar_density: "thin", monochrome: false, folder_background: "", message_background: "", reader_background: "" };
|
const payload = { theme: root.dataset.defaultTheme || "system", interface_skin: "default", preset_scale_unlocked: false, density: "compact", preview_pane: "right", ui_scale: "normal", color_preset: "sage", accent_color: "#6f9c86", background_color: "#121513", panel_color: "#181c19", text_color: "#edf0ec", font_family: "system", font_size: 13, row_height: 58, folder_pane_width: 228, message_pane_width: 420, border_radius: 4, contrast: "normal", show_avatars: true, show_attachment_icons: true, show_flags: true, reduce_motion: false, date_format: "locale", time_format: "locale", icon_size: 18, toolbar_density: "normal", sidebar_item_height: 29, reader_max_width: 0, composer_width: 900, selection_color: "#28352e", unread_font_weight: 730, unread_badge_style: "dot", compact_headers: false, show_borders: true, scrollbar_density: "thin", monochrome: false, folder_background: "", message_background: "", reader_background: "", adaptive_email_colors: false };
|
||||||
try {
|
try {
|
||||||
normalizeServerPreferences(await request("/settings/display", { method: "PUT", body: JSON.stringify(payload) }));
|
normalizeServerPreferences(await request("/settings/display", { method: "PUT", body: JSON.stringify(payload) }));
|
||||||
state.mailPrefs = await request("/settings/mailbox", { method: "PUT", body: JSON.stringify({ compact_wide_rows: true }) });
|
state.mailPrefs = await request("/settings/mailbox", { method: "PUT", body: JSON.stringify({ compact_wide_rows: true }) });
|
||||||
|
|||||||
@@ -1,11 +1,45 @@
|
|||||||
|
function mailboxLoadingMode() {
|
||||||
|
const mode = state.mailPrefs.message_loading_mode || "pagination";
|
||||||
|
return ["pagination", "progressive", "all"].includes(mode) ? mode : "pagination";
|
||||||
|
}
|
||||||
|
|
||||||
|
function mailboxPageCount() {
|
||||||
|
return Math.max(1, Math.ceil(Number(state.total || 0) / Math.max(20, Number(state.perPage || 300))));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMessageLoadControls() {
|
||||||
|
const pagination = $("#message-pagination");
|
||||||
|
const loadActions = $("#message-load-actions");
|
||||||
|
const mode = mailboxLoadingMode();
|
||||||
|
const searchable = Boolean(state.searchContext);
|
||||||
|
const canLoadMore = !searchable && state.messages.length < state.total;
|
||||||
|
|
||||||
|
if (pagination) {
|
||||||
|
const pages = mailboxPageCount();
|
||||||
|
const showPagination = !searchable && mode === "pagination" && pages > 1;
|
||||||
|
pagination.classList.toggle("hidden", !showPagination);
|
||||||
|
const previous = $("#page-prev");
|
||||||
|
const next = $("#page-next");
|
||||||
|
const status = $("#page-status");
|
||||||
|
if (previous) previous.disabled = state.page <= 0;
|
||||||
|
if (next) next.disabled = state.page >= pages - 1;
|
||||||
|
if (status) status.textContent = `${tr("page", "Page")} ${Math.min(state.page + 1, pages)} ${tr("of", "of")} ${pages}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadActions) {
|
||||||
|
loadActions.classList.toggle("hidden", mode !== "progressive" || !canLoadMore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadMessages(reset = false, options = {}) {
|
async function loadMessages(reset = false, options = {}) {
|
||||||
const list = $("#message-list");
|
const list = $("#message-list");
|
||||||
|
const mode = mailboxLoadingMode();
|
||||||
const restoreState = Boolean(options.restoreState);
|
const restoreState = Boolean(options.restoreState);
|
||||||
const savedState = restoreState ? readMessageListState(state.folder) : null;
|
const savedState = restoreState ? readMessageListState(state.folder) : null;
|
||||||
const previousScrollTop = reset && !restoreState && list ? list.scrollTop : null;
|
const previousScrollTop = reset && !restoreState && list ? list.scrollTop : null;
|
||||||
|
const pageBeforeReset = state.page;
|
||||||
if (reset) {
|
if (reset) {
|
||||||
state.page = 0;
|
state.page = options.background && mode === "pagination" ? pageBeforeReset : 0;
|
||||||
if (!options.background) {
|
if (!options.background) {
|
||||||
state.messages = [];
|
state.messages = [];
|
||||||
state.selectedMessages.clear();
|
state.selectedMessages.clear();
|
||||||
@@ -17,15 +51,20 @@ async function loadMessages(reset = false, options = {}) {
|
|||||||
try {
|
try {
|
||||||
const sort = encodeURIComponent(state.mailPrefs.default_sort || "date_desc");
|
const sort = encodeURIComponent(state.mailPrefs.default_sort || "date_desc");
|
||||||
const maxRestorePage = Math.max(0, Math.ceil(500 / Math.max(20, state.perPage)) - 1);
|
const maxRestorePage = Math.max(0, Math.ceil(500 / Math.max(20, state.perPage)) - 1);
|
||||||
const targetPage = reset && savedState ? Math.min(savedState.page, maxRestorePage) : state.page;
|
const savedPage = reset && savedState
|
||||||
const pages = reset && targetPage > 0 ? Array.from({ length: targetPage + 1 }, (_, index) => index) : [state.page];
|
? (mode === "pagination" ? savedState.page : Math.min(savedState.page, maxRestorePage))
|
||||||
|
: state.page;
|
||||||
|
const targetPage = reset && mode === "all" ? 0 : savedPage;
|
||||||
|
const pages = reset && mode === "progressive" && targetPage > 0
|
||||||
|
? Array.from({ length: targetPage + 1 }, (_, index) => index)
|
||||||
|
: [targetPage];
|
||||||
let loaded = [];
|
let loaded = [];
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
let flaggedOnTop = state.currentFolderFlaggedOnTop;
|
let flaggedOnTop = state.currentFolderFlaggedOnTop;
|
||||||
let folderTotalCount = null;
|
let folderTotalCount = null;
|
||||||
let folderUnreadCount = null;
|
let folderUnreadCount = null;
|
||||||
let syncing = false;
|
let syncing = false;
|
||||||
let lastLoadedPage = state.page;
|
let lastLoadedPage = targetPage;
|
||||||
for (const page of pages) {
|
for (const page of pages) {
|
||||||
const forceRefresh = options.force ? "&refresh=true" : "";
|
const forceRefresh = options.force ? "&refresh=true" : "";
|
||||||
const data = await request(`/folders/${encodeURIComponent(state.folder)}/messages?page=${page}&per_page=${state.perPage}&sort=${sort}${forceRefresh}`, { method: "GET" });
|
const data = await request(`/folders/${encodeURIComponent(state.folder)}/messages?page=${page}&per_page=${state.perPage}&sort=${sort}${forceRefresh}`, { method: "GET" });
|
||||||
@@ -38,10 +77,10 @@ async function loadMessages(reset = false, options = {}) {
|
|||||||
lastLoadedPage = page;
|
lastLoadedPage = page;
|
||||||
if (loaded.length >= totalCount) break;
|
if (loaded.length >= totalCount) break;
|
||||||
}
|
}
|
||||||
state.page = reset ? lastLoadedPage : state.page;
|
state.page = lastLoadedPage;
|
||||||
state.total = totalCount;
|
state.total = totalCount;
|
||||||
state.currentFolderFlaggedOnTop = flaggedOnTop;
|
state.currentFolderFlaggedOnTop = flaggedOnTop;
|
||||||
state.messages = reset ? loaded : [...state.messages, ...loaded];
|
state.messages = reset || mode === "pagination" ? loaded : [...state.messages, ...loaded];
|
||||||
if (state.folders.some((item) => item.name === state.folder)) {
|
if (state.folders.some((item) => item.name === state.folder)) {
|
||||||
setFolderCounts(state.folder, folderTotalCount, folderUnreadCount);
|
setFolderCounts(state.folder, folderTotalCount, folderUnreadCount);
|
||||||
}
|
}
|
||||||
@@ -54,39 +93,87 @@ async function loadMessages(reset = false, options = {}) {
|
|||||||
$("#connection-label").textContent = "Connected";
|
$("#connection-label").textContent = "Connected";
|
||||||
renderMessages();
|
renderMessages();
|
||||||
const desiredScroll = savedState?.scrollTop ?? previousScrollTop;
|
const desiredScroll = savedState?.scrollTop ?? previousScrollTop;
|
||||||
if (Number.isFinite(desiredScroll) && desiredScroll > 0) {
|
if (mode === "pagination") {
|
||||||
|
requestAnimationFrame(() => { list.scrollTop = 0; });
|
||||||
|
} else if (Number.isFinite(desiredScroll) && desiredScroll > 0) {
|
||||||
requestAnimationFrame(() => { list.scrollTop = Math.min(desiredScroll, Math.max(0, list.scrollHeight - list.clientHeight)); });
|
requestAnimationFrame(() => { list.scrollTop = Math.min(desiredScroll, Math.max(0, list.scrollHeight - list.clientHeight)); });
|
||||||
}
|
}
|
||||||
saveMessageListState();
|
saveMessageListState();
|
||||||
|
if (reset && mode === "all" && !state.searchContext && state.messages.length < state.total && !options.skipAutoAll) {
|
||||||
|
await loadAllMessages();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
$("#connection-label").textContent = error.network ? (navigator.onLine ? "Reconnecting" : "Offline") : "Connection error";
|
$("#connection-label").textContent = error.network ? (navigator.onLine ? "Reconnecting" : "Offline") : "Connection error";
|
||||||
if (!state.messages.length) list.innerHTML = `<div class="empty-list">${escapeHtml(error.message)}</div>`;
|
if (!state.messages.length) list.innerHTML = `<div class="empty-list">${escapeHtml(error.message)}</div>`;
|
||||||
$("#message-count").textContent = state.messages.length ? `${state.total} messages · cached view` : "Unavailable";
|
$("#message-count").textContent = state.messages.length ? `${state.total} messages · cached view` : "Unavailable";
|
||||||
|
updateMessageLoadControls();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function changeMailboxPage(delta) {
|
||||||
|
if (state.searchContext || mailboxLoadingMode() !== "pagination") return false;
|
||||||
|
const pages = mailboxPageCount();
|
||||||
|
const target = Math.max(0, Math.min(pages - 1, state.page + delta));
|
||||||
|
if (target === state.page) return false;
|
||||||
|
const previousPage = state.page;
|
||||||
|
state.page = target;
|
||||||
|
const loaded = await loadMessages(false, { background: true });
|
||||||
|
if (!loaded) {
|
||||||
|
state.page = previousPage;
|
||||||
|
updateMessageLoadControls();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const list = $("#message-list");
|
||||||
|
if (list) list.scrollTop = 0;
|
||||||
|
saveMessageListState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadNextProgressivePage() {
|
||||||
|
if (state.searchContext || mailboxLoadingMode() !== "progressive" || state.loadingMoreMessages || state.messages.length >= state.total) return;
|
||||||
|
state.loadingMoreMessages = true;
|
||||||
|
const before = state.messages.length;
|
||||||
|
const previousPage = state.page;
|
||||||
|
state.page += 1;
|
||||||
|
try {
|
||||||
|
await loadMessages(false, { background: true });
|
||||||
|
if (state.messages.length <= before) state.page = previousPage;
|
||||||
|
} finally {
|
||||||
|
state.loadingMoreMessages = false;
|
||||||
|
updateMessageLoadControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessageListScroll() {
|
||||||
|
scheduleMessageListStateSave();
|
||||||
|
if (mailboxLoadingMode() !== "progressive" || state.loadingMoreMessages || state.searchContext) return;
|
||||||
|
const list = $("#message-list");
|
||||||
|
if (!list || state.messages.length >= state.total) return;
|
||||||
|
if (list.scrollTop + list.clientHeight >= list.scrollHeight - 240) loadNextProgressivePage();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAllMessages() {
|
async function loadAllMessages() {
|
||||||
if (state.searchContext || state.messages.length >= state.total) return;
|
if (state.searchContext || mailboxLoadingMode() !== "all" || state.messages.length >= state.total || state.loadingAllMessages) return;
|
||||||
const loadAll = $("#load-all");
|
state.loadingAllMessages = true;
|
||||||
const loadMore = $("#load-more");
|
|
||||||
setBusy(loadAll, true);
|
|
||||||
if (loadMore) loadMore.disabled = true;
|
|
||||||
try {
|
try {
|
||||||
let guard = 0;
|
let guard = 0;
|
||||||
while (state.messages.length < state.total && guard < 10000) {
|
while (state.messages.length < state.total && guard < 10000) {
|
||||||
const before = state.messages.length;
|
const before = state.messages.length;
|
||||||
|
const previousPage = state.page;
|
||||||
state.page += 1;
|
state.page += 1;
|
||||||
await loadMessages(false, { background: true });
|
await loadMessages(false, { background: true, skipAutoAll: true });
|
||||||
guard += 1;
|
guard += 1;
|
||||||
if (state.messages.length <= before) {
|
if (state.messages.length <= before) {
|
||||||
state.page = Math.max(0, state.page - 1);
|
state.page = previousPage;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
saveMessageListState();
|
saveMessageListState();
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(loadAll, false);
|
state.loadingAllMessages = false;
|
||||||
if (loadMore) loadMore.disabled = false;
|
updateMessageLoadControls();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,24 +388,30 @@ function renderMessages() {
|
|||||||
row.addEventListener("contextmenu", (event) => { event.preventDefault(); openMessageContextMenu(event.clientX, event.clientY, message); });
|
row.addEventListener("contextmenu", (event) => { event.preventDefault(); openMessageContextMenu(event.clientX, event.clientY, message); });
|
||||||
list.append(row);
|
list.append(row);
|
||||||
}
|
}
|
||||||
const canLoadMore = !state.searchContext && state.messages.length < state.total;
|
updateMessageLoadControls();
|
||||||
$("#message-load-actions")?.classList.toggle("hidden", !canLoadMore);
|
|
||||||
renderBulkToolbar();
|
renderBulkToolbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateMessage(delta) {
|
async function navigateMessage(delta) {
|
||||||
if (!state.selected) return;
|
if (!state.selected) return;
|
||||||
const index = state.messages.findIndex((message) => message.uid === state.selected.uid && (message.folder || state.folder) === state.selected.folder);
|
const index = state.messages.findIndex((message) => message.uid === state.selected.uid && (message.folder || state.folder) === state.selected.folder);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
const next = state.messages[index + delta];
|
const next = state.messages[index + delta];
|
||||||
if (!next) return toast(delta > 0 ? "No next loaded message" : "No previous loaded message");
|
if (next) return openMessage(next.folder || state.folder, next.uid);
|
||||||
openMessage(next.folder || state.folder, next.uid);
|
if (!state.searchContext && mailboxLoadingMode() === "pagination" && await changeMailboxPage(delta > 0 ? 1 : -1)) {
|
||||||
|
const boundary = delta > 0 ? state.messages[0] : state.messages[state.messages.length - 1];
|
||||||
|
if (boundary) return openMessage(boundary.folder || state.folder, boundary.uid);
|
||||||
|
}
|
||||||
|
toast(delta > 0 ? "No next message" : "No previous message");
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMessageNavigation() {
|
function updateMessageNavigation() {
|
||||||
const index = state.selected ? state.messages.findIndex((message) => message.uid === state.selected.uid && (message.folder || state.folder) === state.selected.folder) : -1;
|
const index = state.selected ? state.messages.findIndex((message) => message.uid === state.selected.uid && (message.folder || state.folder) === state.selected.folder) : -1;
|
||||||
$("#reader-prev").disabled = index <= 0;
|
const paginated = !state.searchContext && mailboxLoadingMode() === "pagination";
|
||||||
$("#reader-next").disabled = index < 0 || index >= state.messages.length - 1;
|
const hasPreviousPage = paginated && state.page > 0;
|
||||||
|
const hasNextPage = paginated && state.page < mailboxPageCount() - 1;
|
||||||
|
$("#reader-prev").disabled = index < 0 || (index <= 0 && !hasPreviousPage);
|
||||||
|
$("#reader-next").disabled = index < 0 || (index >= state.messages.length - 1 && !hasNextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function senderIsSavedContact(email) {
|
async function senderIsSavedContact(email) {
|
||||||
@@ -730,6 +823,162 @@ async function discoverCurrentPgpSigner() {
|
|||||||
if (key) await processCurrentPgpMessage();
|
if (key) await processCurrentPgpMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseAdaptiveEmailColor(value) {
|
||||||
|
const raw = String(value || "").trim().toLowerCase();
|
||||||
|
if (!raw || ["transparent", "inherit", "initial", "unset", "currentcolor"].includes(raw)) return null;
|
||||||
|
const named = {
|
||||||
|
black: [0, 0, 0], white: [255, 255, 255], gray: [128, 128, 128], grey: [128, 128, 128],
|
||||||
|
silver: [192, 192, 192], navy: [0, 0, 128], red: [255, 0, 0], green: [0, 128, 0],
|
||||||
|
blue: [0, 0, 255], yellow: [255, 255, 0], maroon: [128, 0, 0], purple: [128, 0, 128],
|
||||||
|
teal: [0, 128, 128], olive: [128, 128, 0], aqua: [0, 255, 255], fuchsia: [255, 0, 255],
|
||||||
|
lime: [0, 255, 0], whitesmoke: [245, 245, 245], gainsboro: [220, 220, 220],
|
||||||
|
};
|
||||||
|
if (named[raw]) return { r: named[raw][0], g: named[raw][1], b: named[raw][2], a: 1 };
|
||||||
|
let match = raw.match(/^#([0-9a-f]{3,8})$/i);
|
||||||
|
if (match) {
|
||||||
|
let hex = match[1];
|
||||||
|
if (hex.length === 3 || hex.length === 4) hex = [...hex].map((part) => part + part).join("");
|
||||||
|
if (hex.length === 6 || hex.length === 8) {
|
||||||
|
return {
|
||||||
|
r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16),
|
||||||
|
a: hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match = raw.match(/^rgba?\(\s*([0-9.]+)\s*[, ]\s*([0-9.]+)\s*[, ]\s*([0-9.]+)(?:\s*[,/]\s*([0-9.]+%?))?\s*\)$/i);
|
||||||
|
if (!match) return null;
|
||||||
|
const alpha = match[4]?.endsWith("%") ? Number(match[4].slice(0, -1)) / 100 : Number(match[4] ?? 1);
|
||||||
|
const clamp = (value) => Math.max(0, Math.min(255, Math.round(Number(value))));
|
||||||
|
return { r: clamp(match[1]), g: clamp(match[2]), b: clamp(match[3]), a: Math.max(0, Math.min(1, alpha)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptiveEmailRgbCss(color) {
|
||||||
|
return `rgb(${Math.round(color.r)} ${Math.round(color.g)} ${Math.round(color.b)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptiveEmailMix(a, b, amount) {
|
||||||
|
const t = Math.max(0, Math.min(1, Number(amount) || 0));
|
||||||
|
return { r: a.r + (b.r - a.r) * t, g: a.g + (b.g - a.g) * t, b: a.b + (b.b - a.b) * t, a: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptiveEmailComposite(foreground, background) {
|
||||||
|
if (!foreground || foreground.a >= 0.999) return foreground;
|
||||||
|
const alpha = Math.max(0, Math.min(1, foreground.a));
|
||||||
|
return {
|
||||||
|
r: foreground.r * alpha + background.r * (1 - alpha),
|
||||||
|
g: foreground.g * alpha + background.g * (1 - alpha),
|
||||||
|
b: foreground.b * alpha + background.b * (1 - alpha),
|
||||||
|
a: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptiveEmailBrightness(color) {
|
||||||
|
return (0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b) / 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptiveEmailSaturation(color) {
|
||||||
|
const max = Math.max(color.r, color.g, color.b);
|
||||||
|
const min = Math.min(color.r, color.g, color.b);
|
||||||
|
return max <= 0 ? 0 : (max - min) / max;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptiveEmailLuminance(color) {
|
||||||
|
const channel = (value) => {
|
||||||
|
const normalized = value / 255;
|
||||||
|
return normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
|
||||||
|
};
|
||||||
|
return 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptiveEmailContrast(a, b) {
|
||||||
|
const one = adaptiveEmailLuminance(a);
|
||||||
|
const two = adaptiveEmailLuminance(b);
|
||||||
|
return (Math.max(one, two) + 0.05) / (Math.min(one, two) + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptiveEmailReadableColor(color, background, preferred) {
|
||||||
|
if (!color || !background || adaptiveEmailContrast(color, background) >= 3.5) return color;
|
||||||
|
for (let step = 0.18; step <= 1.001; step += 0.14) {
|
||||||
|
const candidate = adaptiveEmailMix(color, preferred, step);
|
||||||
|
if (adaptiveEmailContrast(candidate, background) >= 4.5) return candidate;
|
||||||
|
}
|
||||||
|
const black = { r: 0, g: 0, b: 0, a: 1 };
|
||||||
|
const white = { r: 255, g: 255, b: 255, a: 1 };
|
||||||
|
return adaptiveEmailContrast(white, background) >= adaptiveEmailContrast(black, background) ? white : black;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptMessagePreviewColors(html, detail, body) {
|
||||||
|
const targetTheme = resolveTheme(state.prefs.theme);
|
||||||
|
const targetDark = targetTheme === "dark";
|
||||||
|
const bodyStyles = getComputedStyle(body);
|
||||||
|
const surface = parseAdaptiveEmailColor(bodyStyles.backgroundColor) || (targetDark ? { r: 24, g: 28, b: 25, a: 1 } : { r: 255, g: 255, b: 255, a: 1 });
|
||||||
|
const text = parseAdaptiveEmailColor(bodyStyles.color) || (targetDark ? { r: 237, g: 240, b: 236, a: 1 } : { r: 32, g: 38, b: 34, a: 1 });
|
||||||
|
const mailTheme = String(detail?.email_theme || "").toLowerCase();
|
||||||
|
const themeAlreadyMatches = mailTheme === targetTheme;
|
||||||
|
const doc = new DOMParser().parseFromString(`<body>${html}</body>`, "text/html");
|
||||||
|
|
||||||
|
const remapBackground = (color) => {
|
||||||
|
if (!color || color.a < 0.45) return null;
|
||||||
|
const brightness = adaptiveEmailBrightness(color);
|
||||||
|
const saturation = adaptiveEmailSaturation(color);
|
||||||
|
if (saturation > 0.38) return null;
|
||||||
|
if (targetDark && brightness >= 0.62) {
|
||||||
|
return adaptiveEmailMix(surface, text, Math.max(0, Math.min(0.12, (1 - brightness) * 0.26)));
|
||||||
|
}
|
||||||
|
if (!targetDark && brightness <= 0.38) {
|
||||||
|
return adaptiveEmailMix(surface, text, Math.max(0, Math.min(0.10, brightness * 0.24)));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const remapNeutralText = (color) => {
|
||||||
|
if (!color || color.a < 0.45) return null;
|
||||||
|
const brightness = adaptiveEmailBrightness(color);
|
||||||
|
const saturation = adaptiveEmailSaturation(color);
|
||||||
|
if (saturation > 0.32) return null;
|
||||||
|
if (targetDark && brightness <= 0.45) return adaptiveEmailMix(text, surface, brightness * 0.28);
|
||||||
|
if (!targetDark && brightness >= 0.62) return adaptiveEmailMix(text, surface, (1 - brightness) * 0.28);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const walk = (node, inheritedBackground) => {
|
||||||
|
let effectiveBackground = inheritedBackground;
|
||||||
|
const style = node.style;
|
||||||
|
const backgroundAttribute = node.getAttribute("bgcolor");
|
||||||
|
const sourceBackground = parseAdaptiveEmailColor(style?.backgroundColor || backgroundAttribute);
|
||||||
|
if (sourceBackground) {
|
||||||
|
const composited = adaptiveEmailComposite(sourceBackground, inheritedBackground);
|
||||||
|
const mappedBackground = themeAlreadyMatches ? null : remapBackground(composited);
|
||||||
|
if (mappedBackground) {
|
||||||
|
if (style) style.backgroundColor = adaptiveEmailRgbCss(mappedBackground);
|
||||||
|
else node.setAttribute("bgcolor", adaptiveEmailRgbCss(mappedBackground));
|
||||||
|
if (backgroundAttribute) node.setAttribute("bgcolor", adaptiveEmailRgbCss(mappedBackground));
|
||||||
|
effectiveBackground = mappedBackground;
|
||||||
|
} else {
|
||||||
|
effectiveBackground = composited;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorAttribute = node.getAttribute("color");
|
||||||
|
const sourceColor = parseAdaptiveEmailColor(style?.color || colorAttribute);
|
||||||
|
if (sourceColor) {
|
||||||
|
const composited = adaptiveEmailComposite(sourceColor, effectiveBackground);
|
||||||
|
const neutralMapped = themeAlreadyMatches ? null : remapNeutralText(composited);
|
||||||
|
const readable = adaptiveEmailReadableColor(neutralMapped || composited, effectiveBackground, text);
|
||||||
|
if (neutralMapped || readable !== composited) {
|
||||||
|
const replacement = adaptiveEmailRgbCss(readable);
|
||||||
|
if (style) style.color = replacement;
|
||||||
|
if (colorAttribute) node.setAttribute("color", replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of node.children) walk(child, effectiveBackground);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const child of doc.body.children) walk(child, surface);
|
||||||
|
return { html: doc.body.innerHTML, targetTheme, surface: adaptiveEmailRgbCss(surface), text: adaptiveEmailRgbCss(text) };
|
||||||
|
}
|
||||||
|
|
||||||
function renderMessageBody(detail, forceRemote = false) {
|
function renderMessageBody(detail, forceRemote = false) {
|
||||||
const body = $("#message-body");
|
const body = $("#message-body");
|
||||||
const remoteBar = $("#remote-image-bar");
|
const remoteBar = $("#remote-image-bar");
|
||||||
@@ -751,9 +1000,18 @@ function renderMessageBody(detail, forceRemote = false) {
|
|||||||
iframe.setAttribute("referrerpolicy", "no-referrer");
|
iframe.setAttribute("referrerpolicy", "no-referrer");
|
||||||
const imageSources = allowRemote ? "data: cid: https: http:" : "data: cid:";
|
const imageSources = allowRemote ? "data: cid: https: http:" : "data: cid:";
|
||||||
const csp = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${imageSources}; style-src 'unsafe-inline'; font-src data:">`;
|
const csp = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${imageSources}; style-src 'unsafe-inline'; font-src data:">`;
|
||||||
const quoteStyle = state.mailPrefs.collapse_quotes !== false ? "blockquote{max-height:16em;overflow:auto;border-left:2px solid #d9ddda;padding-left:12px;margin-left:0}" : "blockquote{border-left:2px solid #d9ddda;padding-left:12px;margin-left:0}";
|
let safeHtml = sanitizeMessagePreviewHtml(detail.html);
|
||||||
const safeHtml = sanitizeMessagePreviewHtml(detail.html);
|
let previewTheme = { targetTheme: "light", surface: "#fff", text: "#202622" };
|
||||||
iframe.srcdoc = `<!doctype html><html><head><meta charset="utf-8">${csp}<meta name="color-scheme" content="light"><style>body{margin:0;padding:22px;color:#202622;background:#fff;font:14px/1.6 Inter,system-ui,-apple-system,'Segoe UI',sans-serif}img{max-width:100%;height:auto}a{color:#356a56}pre{white-space:pre-wrap}${quoteStyle}</style></head><body>${safeHtml}</body></html>`;
|
if (state.prefs.adaptiveEmailColors) {
|
||||||
|
previewTheme = adaptMessagePreviewColors(safeHtml, detail, body);
|
||||||
|
safeHtml = previewTheme.html;
|
||||||
|
}
|
||||||
|
const quoteColor = state.prefs.adaptiveEmailColors ? `color-mix(in srgb, ${previewTheme.text} 25%, ${previewTheme.surface})` : "#d9ddda";
|
||||||
|
const quoteStyle = state.mailPrefs.collapse_quotes !== false ? `blockquote{max-height:16em;overflow:auto;border-left:2px solid ${quoteColor};padding-left:12px;margin-left:0}` : `blockquote{border-left:2px solid ${quoteColor};padding-left:12px;margin-left:0}`;
|
||||||
|
const previewStyle = state.prefs.adaptiveEmailColors
|
||||||
|
? `body{margin:0;padding:22px;color:${previewTheme.text};background:${previewTheme.surface};font:14px/1.6 Inter,system-ui,-apple-system,'Segoe UI',sans-serif}img{max-width:100%;height:auto}a{color:inherit;text-decoration:underline}pre{white-space:pre-wrap}${quoteStyle}`
|
||||||
|
: `body{margin:0;padding:22px;color:#202622;background:#fff;font:14px/1.6 Inter,system-ui,-apple-system,'Segoe UI',sans-serif}img{max-width:100%;height:auto}a{color:#356a56}pre{white-space:pre-wrap}${quoteStyle}`;
|
||||||
|
iframe.srcdoc = `<!doctype html><html><head><meta charset="utf-8">${csp}<meta name="color-scheme" content="${previewTheme.targetTheme}"><style>${previewStyle}</style></head><body>${safeHtml}</body></html>`;
|
||||||
body.append(iframe);
|
body.append(iframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -324,9 +324,10 @@ function bindEvents() {
|
|||||||
toast("Message sort updated", "success");
|
toast("Message sort updated", "success");
|
||||||
} catch (error) { toast(error.message, "error"); }
|
} catch (error) { toast(error.message, "error"); }
|
||||||
});
|
});
|
||||||
$("#load-more").addEventListener("click", async () => { state.page += 1; await loadMessages(false); });
|
$("#page-prev").addEventListener("click", () => changeMailboxPage(-1));
|
||||||
$("#load-all").addEventListener("click", loadAllMessages);
|
$("#page-next").addEventListener("click", () => changeMailboxPage(1));
|
||||||
$("#message-list").addEventListener("scroll", scheduleMessageListStateSave, { passive: true });
|
$("#load-more").addEventListener("click", loadNextProgressivePage);
|
||||||
|
$("#message-list").addEventListener("scroll", handleMessageListScroll, { passive: true });
|
||||||
window.addEventListener("offline", () => setConnectivityState(false));
|
window.addEventListener("offline", () => setConnectivityState(false));
|
||||||
window.addEventListener("online", async () => {
|
window.addEventListener("online", async () => {
|
||||||
setConnectivityState(true);
|
setConnectivityState(true);
|
||||||
@@ -404,7 +405,7 @@ function bindEvents() {
|
|||||||
$("#pgp-import-file-input").addEventListener("change", async (event) => { const file = event.target.files?.[0]; if (file) await importPgpFile(file); event.target.value = ""; });
|
$("#pgp-import-file-input").addEventListener("change", async (event) => { const file = event.target.files?.[0]; if (file) await importPgpFile(file); event.target.value = ""; });
|
||||||
$("#pgp-discover").addEventListener("click", () => discoverPgpEmail());
|
$("#pgp-discover").addEventListener("click", () => discoverPgpEmail());
|
||||||
$$(".settings-tab").forEach((button) => button.addEventListener("click", () => { selectSettingsTab(button.dataset.tab); if (button.dataset.tab === "sieve") loadSieve(); if (button.dataset.tab === "identities") { loadIdentities(); if (state.authPolicy.pgpEnabled !== false) loadOpenPgp(); } if (button.dataset.tab === "openpgp" && state.authPolicy.pgpEnabled !== false) loadOpenPgp(); if (button.dataset.tab === "notifications") loadNotificationPreferences(); if (button.dataset.tab === "mailbox") loadMailPreferences(); if (button.dataset.tab === "composing") loadResponseTemplates(); if (button.dataset.tab === "diagnostics") loadDiagnostics(); if (button.dataset.tab === "account") { renderAccountSettings(); loadActiveSessions(); } if (button.dataset.tab === "folders") { loadSpecialFolders(); loadQuota(); } }));
|
$$(".settings-tab").forEach((button) => button.addEventListener("click", () => { selectSettingsTab(button.dataset.tab); if (button.dataset.tab === "sieve") loadSieve(); if (button.dataset.tab === "identities") { loadIdentities(); if (state.authPolicy.pgpEnabled !== false) loadOpenPgp(); } if (button.dataset.tab === "openpgp" && state.authPolicy.pgpEnabled !== false) loadOpenPgp(); if (button.dataset.tab === "notifications") loadNotificationPreferences(); if (button.dataset.tab === "mailbox") loadMailPreferences(); if (button.dataset.tab === "composing") loadResponseTemplates(); if (button.dataset.tab === "diagnostics") loadDiagnostics(); if (button.dataset.tab === "account") { renderAccountSettings(); loadActiveSessions(); } if (button.dataset.tab === "folders") { loadSpecialFolders(); loadQuota(); } }));
|
||||||
$("#pref-theme").addEventListener("change", (event) => savePreference("theme", event.target.value));
|
$("#pref-theme").addEventListener("change", (event) => { savePreference("theme", event.target.value); if (state.currentDetail && state.prefs.adaptiveEmailColors) renderMessageDetail(state.currentDetail); });
|
||||||
$("#pref-interface-skin").addEventListener("change", (event) => savePreference("interfaceSkin", event.target.value));
|
$("#pref-interface-skin").addEventListener("change", (event) => savePreference("interfaceSkin", event.target.value));
|
||||||
$("#pref-preset-scale-unlocked").addEventListener("change", (event) => savePreference("presetScaleUnlocked", event.target.checked));
|
$("#pref-preset-scale-unlocked").addEventListener("change", (event) => savePreference("presetScaleUnlocked", event.target.checked));
|
||||||
$("#pref-density").addEventListener("change", (event) => savePreference("density", event.target.value));
|
$("#pref-density").addEventListener("change", (event) => savePreference("density", event.target.value));
|
||||||
@@ -423,6 +424,7 @@ function bindEvents() {
|
|||||||
$("#pref-attachment-icons").addEventListener("change", (event) => savePreference("showAttachmentIcons", event.target.checked));
|
$("#pref-attachment-icons").addEventListener("change", (event) => savePreference("showAttachmentIcons", event.target.checked));
|
||||||
$("#pref-flags").addEventListener("change", (event) => savePreference("showFlags", event.target.checked));
|
$("#pref-flags").addEventListener("change", (event) => savePreference("showFlags", event.target.checked));
|
||||||
$("#pref-reduce-motion").addEventListener("change", (event) => savePreference("reduceMotion", event.target.checked));
|
$("#pref-reduce-motion").addEventListener("change", (event) => savePreference("reduceMotion", event.target.checked));
|
||||||
|
$("#pref-adaptive-email-colors").addEventListener("change", (event) => { savePreference("adaptiveEmailColors", event.target.checked); if (state.currentDetail) renderMessageDetail(state.currentDetail); });
|
||||||
[["#pref-icon-size", "iconSize"], ["#pref-sidebar-height", "sidebarItemHeight"], ["#pref-composer-width", "composerWidth"], ["#pref-unread-weight", "unreadFontWeight"]].forEach(([selector, key]) => $(selector).addEventListener("input", (event) => savePreference(key, Number(event.target.value))));
|
[["#pref-icon-size", "iconSize"], ["#pref-sidebar-height", "sidebarItemHeight"], ["#pref-composer-width", "composerWidth"], ["#pref-unread-weight", "unreadFontWeight"]].forEach(([selector, key]) => $(selector).addEventListener("input", (event) => savePreference(key, Number(event.target.value))));
|
||||||
$("#pref-reader-width").addEventListener("change", (event) => savePreference("readerMaxWidth", Number(event.target.value)));
|
$("#pref-reader-width").addEventListener("change", (event) => savePreference("readerMaxWidth", Number(event.target.value)));
|
||||||
$("#pref-toolbar-density").addEventListener("change", (event) => savePreference("toolbarDensity", event.target.value));
|
$("#pref-toolbar-density").addEventListener("change", (event) => savePreference("toolbarDensity", event.target.value));
|
||||||
@@ -639,7 +641,7 @@ function bindEvents() {
|
|||||||
});
|
});
|
||||||
matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => {
|
matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => {
|
||||||
if (!$("#login-view").classList.contains("hidden")) showLogin();
|
if (!$("#login-view").classList.contains("hidden")) showLogin();
|
||||||
else if (state.prefs.theme === "system") applyPreferences();
|
else if (state.prefs.theme === "system") { applyPreferences(); if (state.currentDetail && state.prefs.adaptiveEmailColors) renderMessageDetail(state.currentDetail); }
|
||||||
});
|
});
|
||||||
window.addEventListener("resize", () => { if (innerWidth > 820) setFolderDrawerOpen(false); });
|
window.addEventListener("resize", () => { if (innerWidth > 820) setFolderDrawerOpen(false); });
|
||||||
window.addEventListener("popstate", async (event) => {
|
window.addEventListener("popstate", async (event) => {
|
||||||
|
|||||||
Reference in new issue
Block a user