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

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-31 00:37:16 +01:00
1 parent ecc201ef50
commit 698e3c4a1b
124 files changed
+4593 -2721

No files matched your search

+14
View File
@@ -1,5 +1,19 @@
# RustBox changelog
### 0.1.1
- Fixed the multi-backend compatibility layer compile blockers reported by the Rust compiler: empty `[]` parameter inference no longer overlaps the generic array implementation, legacy SQLite probing imports `rusqlite::OptionalExtension` without colliding with RustBox's own `OptionalExtension`, and MySQL/PostgreSQL transaction batch execution now uses the typed SQLx transaction helpers instead of the lifetime-problematic `raw_sql` executor path.
- Unified the storage model: `database_backend = sqlite | mysql | pgsql` now selects the database engine for the whole application instead of only the SQL session store; `session_backend = database | redis` either keeps sessions in that same database or moves them to Redis/Redis Sentinel.
- Added complete SQLite, MySQL and PostgreSQL baselines under `migrations/{sqlite,mysql,pgsql}/schema.sql`. Each schema contains global sessions plus the full account-data schema at baseline version 46; the historical SQLite V001-V046 files are squashed out of the release tree and V047 is reserved for the next schema change across all engines.
- Preserved per-account data isolation in a shared physical database with deterministic collision-checked table prefixes, while keeping the session table global.
- Added automatic first-use import of legacy 0.1.0 `data_dir/<user_hash>/db.sqlite` databases at `PRAGMA user_version = 46` into the configured backend. The source database is renamed to `db.sqlite.pre-0.1.1.bak` only after a successful import; older schemas are rejected with an explicit upgrade requirement.
- Added a backend-neutral database compatibility layer used by the existing Rust data modules and switched application query selection to the SQLite/MySQL/PostgreSQL catalog matching the configured backend.
- Corrected MySQL/PostgreSQL query dialects and parameter handling, including numbered/reused parameters, upsert syntax, timestamp expressions, JSON address lookup and generated-ID handling.
- Fixed transaction scope for server databases so cache/message synchronization, header invalidation and other grouped writes execute on the actual MySQL/PostgreSQL transaction connection rather than accidentally falling back to the pool.
- Removed separate `session.sql` files: sessions are part of each engine's complete `schema.sql`; Redis remains schema-less.
- Removed stale Python release-contract tests and the unused HTML fixture; retained the runtime JSON fixture used by Rust tests and added native tests for tenant scoping, placeholder adaptation and legacy SQLite import.
- Made Oxi synchronization treat upstream `backend/migrations/` and `backend/tests/` as RustBox-owned review-only areas so schemas and native tests are never overwritten automatically.
- Removed the accidental runtime log `qar.txt` from the source tree and refreshed release metadata for 0.1.1.
### 0.1.0
- Cleaned the final release gate for Rust 1.99 Clippy with `-D warnings`: refactored persistent session creation into a typed input struct and fixed all reported collapsible conditionals, redundant closures, needless borrows/question marks, test layout and collection checks without disabling Clippy globally.
- Fixed fresh user-database provisioning so only SQLite user migrations `V001`-`V046` are applied; session migration `V047` is now exclusively owned by the configured session-store backend and MySQL SQL can no longer be executed against a user SQLite database.
Generated
+1 -1
View File
@@ -3076,7 +3076,7 @@ dependencies = [
[[package]]
name = "rustbox"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"aes-gcm",
"async-imap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rustbox"
version = "0.1.0"
version = "0.1.1"
edition = "2024"
rust-version = "1.88"
default-run = "rustbox"
+1 -1
View File
@@ -1,6 +1,6 @@
# RustBox attribution / notice
RustBox 0.1.0 is derived from the Oxi webmail project:
RustBox 0.1.1 is derived from the Oxi webmail project:
- Upstream: `c0h1b4/oxi`
- Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d`
+17 -11
View File
@@ -1,4 +1,4 @@
# RustBox 0.1.0
# RustBox 0.1.1
RustBox is a webmail derived from `c0h1b4/oxi`. The consumed Oxi revision is recorded in `UPSTREAM_BASE`.
@@ -70,27 +70,33 @@ If the mail host settings are omitted, RustBox uses `localhost` with IMAP `993`
Mail-size limits are intentionally separate. `max_attachment_size_mb` applies to each raw uploaded file, while `max_message_size_mb` is checked against the final RFC822/MIME byte stream immediately before SMTP submission. The latter therefore includes message bodies, headers, MIME boundaries, base64 expansion, inline CID parts and OpenPGP transformations. The composer displays only an estimate; the server-side final-MIME check is authoritative.
## Persistent sessions and cluster traffic
## Database, sessions and cluster traffic
RustBox 0.1.0 persists login sessions by default in SQLite at `data_dir/rustbox.sqlite`. The session contains encrypted IMAP/SMTP credentials so it can survive a process restart; the encryption key is stored as `data_dir/session.key` with restrictive permissions for the default single-node SQLite setup.
RustBox 0.1.1 uses one SQL backend for the whole application. `database_backend = "sqlite" | "mysql" | "pgsql"` selects where account data is stored and, by default, where persistent login sessions are stored. SQLite defaults to `data_dir/rustbox.sqlite`; MySQL and PostgreSQL require `database_url`.
RustBox uses `rusqlite` for SQLite and `sqlx` only for MySQL/PostgreSQL, avoiding duplicate native SQLite linkage in the Cargo dependency graph.
Account data remains isolated per account inside the selected database. Existing 0.1.0 per-user SQLite databases at `data_dir/<user_hash>/db.sqlite` are imported on first access when they are at schema version 46; the source file is retained as `db.sqlite.pre-0.1.1.bak` after a successful import.
The session database backend can be `sqlite`, `mysql` or `pgsql`. For multi-node deployments use MySQL/PostgreSQL or Redis, set `cluster_mode = true`, and provide the same base64-encoded 32-byte `session_encryption_key` on every node. Redis is configured independently from the database and can use either a direct URL or Redis Sentinel discovery.
`session_backend = "database"` stores sessions in the selected application database. `session_backend = "redis"` uses Redis instead; Redis Sentinel is supported through the Sentinel settings. For a multi-node deployment the application database itself must be shared MySQL/PostgreSQL, even when Redis stores sessions. Provide the same base64-encoded 32-byte `session_encryption_key` on every node that shares sessions.
```toml
# Default: SQLite under data_dir.
# Default: the whole application uses SQLite under data_dir.
database_backend = "sqlite"
# database_url = "sqlite:///var/lib/rustbox/rustbox.sqlite?mode=rwc"
session_backend = "database"
# Shared SQL example:
# Full PostgreSQL backend:
# database_backend = "pgsql"
# database_url = "postgres://rustbox:secret@db/rustbox"
# cluster_mode = true
# session_backend = "database"
# session_encryption_key = "<base64-32-byte-key>"
# Redis example; the application database remains configured separately above.
# Full MySQL backend (MySQL 8.0.13+):
# database_backend = "mysql"
# database_url = "mysql://rustbox:secret@db/rustbox"
# session_backend = "database"
# session_encryption_key = "<base64-32-byte-key>"
# Optional Redis session persistence; application data still uses database_backend.
# session_backend = "redis"
# redis_url = "redis://redis:6379/"
@@ -101,7 +107,7 @@ session_backend = "database"
# redis_password = "secret"
```
All executable application SQL is centralized by dialect under `src/queries/{sqlite,mysql,pgsql}/queries.rs`. The migration tree is flat per dialect: `migrations/sqlite/`, `migrations/mysql/`, and `migrations/pgsql/`. SQLite user-schema migrations are `V001`-`V046`, and the session schema is `V047__sessions.sql` for each SQL dialect.
All executable application SQL is centralized by dialect under `src/queries/{sqlite,mysql,pgsql}/queries.rs`. Each engine has one complete baseline in `migrations/{sqlite,mysql,pgsql}/schema.sql`, including the session table. The squashed baseline is schema version 46 and the next schema migration number is V047 for all engines. Redis requires no SQL migration. See `migrations/README.md`.
## Development
@@ -170,7 +176,7 @@ Initialize Git after extracting the release archive:
```bash
git init
git add .
git commit -m "RustBox 0.1.0 release"
git commit -m "RustBox 0.1.1 release"
./scripts/setup-upstream.sh
```
+27 -86
View File
@@ -1,89 +1,30 @@
RustBox 0.1.0 source release
RustBox 0.1.1 release manifest
Runtime/source model:
- one root Cargo package
- src/ Rust application code
- src/web/ RustBox-owned embedded browser UI
- migrations/ dialect-separated SQLite/MySQL/PostgreSQL migrations
- no TypeScript/React/Next.js/Node.js/Bun build
- ManageSieve TLS/STARTTLS/explicit insecure transport option
- native systemd packaging
Storage model:
- database_backend = sqlite | mysql | pgsql selects the full application database
- session_backend = database stores sessions in the same database
- session_backend = redis uses Redis or Redis Sentinel for sessions
Database baselines:
- migrations/sqlite/schema.sql: complete SQLite schema, baseline version 46
- migrations/mysql/schema.sql: complete MySQL schema, baseline version 46
- migrations/pgsql/schema.sql: complete PostgreSQL schema, baseline version 46
- each schema includes global sessions and isolated account-data tables
- historical SQLite V001-V046 files are intentionally squashed out of this release
- next migration number for every SQL engine: V047
Compatibility:
- legacy RustBox 0.1.0 per-user SQLite databases at user_version 46 are imported on first access
- imported source DBs are retained as db.sqlite.pre-0.1.1.bak
- older legacy schemas must first be upgraded with RustBox 0.1.0
Tests/source cleanup:
- tests/fixtures/account-session-expired.json is retained because Rust tests include it
- stale tests/*.py and tests/theme_fixture.html are not part of the release tree
Oxi tracking:
- UPSTREAM_BASE stores consumed Oxi revision
- scripts/setup-upstream.sh configures upstream-oxi
- scripts/oxi-status.sh reports upstream changes
- scripts/sync-upstream.sh maps the cumulative Oxi backend delta into flat RustBox paths
- scripts/finish-upstream.sh advances UPSTREAM_BASE only after explicit upstream review/build validation
- scripts/oxi-port-commit.sh maps a selected Oxi backend commit without advancing the baseline
- Oxi frontend changes are reviewed and manually ported to src/web/
- docs/UI.md documents the browser UI/build conventions
Current 0.1.0 release additions:
- persistent restart-safe sessions; SQLite under data_dir is the zero-config default
- optional MySQL/PostgreSQL session databases
- optional Redis sessions with direct URL or Redis Sentinel master discovery/failover lookup
- shared-session validation for load-balanced cluster traffic and cross-node revocation
- AES-256-GCM encryption for persisted IMAP/SMTP session credentials
- all executable Rust SQL centralized under src/queries/{sqlite,mysql,pgsql}/queries.rs, including sessions
- cached-message query projections are kept in a strict 16-column contract matching Rust row decoding
- flat migrations under migrations/{sqlite,mysql,pgsql}; SQLite user schema uses V001-V046 and sessions use V047 per SQL dialect
- user provisioning uses only the canonical SQLite V001-V046 runner; session V047 is selected separately by the configured session backend and Refinery is no longer used
- allowed downloads explicitly from the sandboxed HTML message reader while retaining popup isolation
- added GitHub-, GitLab-, Discord-, Slack-, Reddit- and Notion-inspired interface styles in light and dark variants
- added release contracts that require every predefined interface style to be visible in the selector, backed by CSS and accepted by backend/config validation
- attachment left-click defaults to safe in-app preview for supported types
- attachment right-click exposes explicit Preview and Download actions
- oversized preview attempts offer direct download instead of ending with a warning
- Polish composer, reader toolbar and per-message context-menu translation pass
- grouped reader message details menu for headers/source and transfer menu for move/copy
- compact composer format selector moved to the lower-right opposite Send/Cancel
- mobile layout reliability pass for settings, source/header/calendar dialogs and narrow toolbars
- fixed Sieve visual-editor checkbox alignment and mobile single-column rule controls
- tap/keyboard expansion for full sender/recipient address details in the reader
- persistent IMAP quota progress above the sidebar IMAP/version footer
- global TOML `default_theme` for light/dark mode and `default_interface_skin` for login + account-wide default interface styling
- Gmail-, Proton-, Zoho- and Outlook-inspired interface profiles plus Minimal
- expanded Polish localization, including scoped dialog/settings text and common attributes
- JavaScript source split into functional `src/web/js/` modules and assembled by Cargo build.rs
- Interface style selector includes RustBox custom, Roundcube Classic/Larry/Elastic, Clean, Graphite, High contrast, Gmail/Proton/Zoho/Outlook/Fastmail/Apple Mail/Yahoo/HEY/GitHub/GitLab/Discord/Slack/Reddit/Notion-inspired profiles, Nord and Minimal
- legacy Compact/Balanced/Comfortable/Reading appearance cards removed
- persisted special folders and mailbox behavior preferences
- bulk IMAP actions, copy/move, folder mark-all-read/empty, message-to-folder drag/drop
- HTML/plain/Markdown composer modes, CID inline images and expanded identity/signature rules
- advanced/saved search, tags, contact groups, visual Sieve/vacation builder
- IMAP IDLE/WebSocket refresh, desktop notifications and DND
- IMAP capabilities diagnostics and multi-account browser session UI
- RFC822 .eml download/import, List-Unsubscribe handling and RFC 8058 one-click HTTPS unsubscribe
- expanded contacts with multiple e-mails/phones/addresses, vCard profile fields, duplicate merge and validated PNG/JPEG/WebP photos
- calendar Month/Week/Day/Agenda views, richer event metadata and ICS import/export
- search-index status/rebuild, IMAP reconnect diagnostics and live cross-browser session revoke UI
- 46 contiguous SQLite user migrations wired sequentially as V001-V046; V047 is reserved for the separate session store in each SQL dialect
- safe Undo for resolvable IMAP MOVE actions and select-all across complete cached search results
- exact-token thread matching and expanded keyboard selection/navigation shortcuts
- in-app attachment preview, clipboard CID image paste and upload progress
- separate 10 MiB per-file attachment limit and 50 MiB final RFC822/MIME message limit with pre-SMTP exact byte validation and composer size estimate
- recoverable drafts with browser emergency snapshots and throttled IMAP Drafts snapshots
- outgoing priority and MDN/read-receipt draft/send options with pre-SMTP mailbox validation
- expanded Sieve visual rules: nested three-condition grouping, two actions, managed reordering, backup/restore, active overwrite guard and optional date-scheduled vacation
- spam destination default plus per-account override, pre-pagination flagged-first sorting with per-folder overrides, and subtle flagged-row markers
- direct ManageSieve quick-filter wizard with validation/save/activation and advanced-editor handoff
- unread browser/app badge updates and a public local-storage readiness endpoint
- inline image editing with display sizing, alignment, crop/cut preview and CID cleanup
- dedicated Composing settings for format, autosave interval, default priority, MDN and attachment reminder
- corrected Settings header/content grid for short panels and scroll-safe navigation
Validation note:
- 56 Python release/runtime/query-layout contract tests passed, including fresh SQLite user migrations, separate SQLite session migration, session restart persistence, query-catalog parity and warning-hygiene contracts.
- The assembled browser JavaScript bundle passed `node --check`; shell scripts passed `bash -n`; JSON/TOML/Python sources and Cargo.lock parsed successfully.
- `scripts/build-release.sh` now enforces `cargo fmt --check`, `cargo test --locked`, `cargo clippy --all-targets -- -D warnings`, and `cargo build --release --locked`.
- A full Rust gate could not be executed in this environment because cargo/rustc are unavailable; the release script and CI must pass on the toolchain host before deployment.
0.1.0 additions:
- mobile reader action and attachment fixes
- WebSocket heartbeat, reconnect backoff, watchdog, resync, dedupe and fallback polling
- reliable live new-message refresh and unread reconciliation
- notification click-through to the newest message when UID is available
- full-screen message reader with context-menu entry, Escape, history and deep links
- composer-only upload progress with duplicate and cancellation safeguards
- source-code comment removal and regression contracts
- UPSTREAM_BASE stores the consumed Oxi revision
- backend source changes are mapped into the flat RustBox tree
- upstream backend/migrations and backend/tests are review-only and never auto-imported
- frontend changes are reviewed and manually ported to src/web/
- scripts/finish-upstream.sh requires explicit acknowledgement for changed RustBox-owned upstream areas before advancing UPSTREAM_BASE
+28
View File
@@ -0,0 +1,28 @@
RustBox 0.1.1 - release validation
Completed in this environment:
- compiler-log remediation: all 47 reported compile errors map to and are covered by three root fixes (41 E0283 empty-parameter inference errors, 2 E0599 rusqlite OptionalExtension errors, 4 SQLx transaction Executor lifetime errors)
- version consistency: Cargo.toml / Cargo.lock / CHANGELOG.md / release metadata = 0.1.1
- migration layout: exactly one schema.sql for sqlite, mysql and pgsql; no session.sql or V001-V046 files
- query catalog parity: 27 modules / 197 public query constants present in sqlite, mysql and pgsql catalogs
- include_str! validation: all referenced files exist
- logical schema parity: sqlite/mysql/pgsql define the same 28 logical tables and column sets
- SQLite execution test: global schema + account tenant schema execute successfully with foreign keys enabled
- SQLite session smoke test: account and session rows insert successfully
- TOML parse: Cargo.toml, config.example.toml and config.dev.toml
- shell syntax: dev.sh and scripts/*.sh pass bash -n
- browser JavaScript: manifest resolves 13 files; concatenated production-order bundle passes node --check
- cleanup: no stale session.sql, V001-V046, migrations/user or migrations/session references outside historical changelog/manifest notes
- tests cleanup: only tests/fixtures/account-session-expired.json remains in tests/
Not executable in this environment:
- cargo fmt/test/clippy/build: Rust/Cargo toolchain is not installed and outbound package/toolchain download is unavailable
- live MySQL/PostgreSQL integration tests: database servers/clients are not installed
The repository contains native Rust tests for tenant scoping, placeholder adaptation and legacy SQLite import; run scripts/build-release.sh on a Rust 1.88+ build host before publishing a production binary.
Hotfix validation (release-gate lint failures):
- removed unusable null_db_value overrides for unsized str/[u8]
- removed redundant row_to_cached_message closure
- collapsed MySQL reordered-params conditional
- cargo/rustc unavailable in packaging environment; run scripts/build-release.sh on Rust host
+4 -3
View File
@@ -11,13 +11,14 @@ default_interface_skin = "rustbox"
data_dir = ".dev/data"
session_timeout_hours = 24
# Persistent sessions. With no extra configuration, sessions use SQLite at
# data_dir/rustbox.sqlite and survive process restarts.
# Application database. The selected engine stores all RustBox account data.
# With session_backend="database", it stores sessions as well.
database_backend = "sqlite" # sqlite | mysql | pgsql
# SQLite defaults to data_dir/rustbox.sqlite. MySQL/PostgreSQL require database_url.
# database_url = "postgres://rustbox:secret@db.example.com/rustbox"
session_backend = "database" # database | redis
cluster_mode = false
# For shared DB/Redis or cluster mode provide the same 32-byte key (base64) on all nodes.
# For shared database sessions, Redis sessions, or cluster mode provide the same 32-byte key (base64) on all nodes.
# session_encryption_key = "..."
# Direct Redis session backend:
# redis_url = "redis://redis.example.com:6379/"
+5 -4
View File
@@ -1,4 +1,4 @@
# RustBox 0.1.0
# RustBox 0.1.1
host = "127.0.0.1"
port = 3001
environment = "production"
@@ -12,13 +12,14 @@ data_dir = "/var/lib/rustbox"
log_dir = "/var/log/rustbox"
session_timeout_hours = 24
# Persistent sessions. With no extra configuration, sessions use SQLite at
# data_dir/rustbox.sqlite and survive process restarts.
# Application database. The selected engine stores all RustBox account data.
# With session_backend="database", it stores sessions as well.
database_backend = "sqlite" # sqlite | mysql | pgsql
# SQLite defaults to data_dir/rustbox.sqlite. MySQL/PostgreSQL require database_url.
# database_url = "postgres://rustbox:secret@db.example.com/rustbox"
session_backend = "database" # database | redis
cluster_mode = false
# For shared DB/Redis or cluster mode provide the same 32-byte key (base64) on all nodes.
# For shared database sessions, Redis sessions, or cluster mode provide the same 32-byte key (base64) on all nodes.
# session_encryption_key = "..."
# Direct Redis session backend:
# redis_url = "redis://redis.example.com:6379/"
+2 -2
View File
@@ -84,13 +84,13 @@ This file tracks the internal `0.0.4` baseline. The user-approved `docs/RUSTBOX_
- Account switch/remove/add workflow for multiple authenticated accounts in one browser session.
- Active-session list across browsers while the RustBox process is running, with creation/last-active timestamps, friendly browser/device labels, full User-Agent hover detail, individual revoke and revoke-all-others.
- Diagnostics panel exposing IMAP capabilities (including persistent custom-keyword support), authenticated SMTP connectivity, ManageSieve status, browser WebSocket state, local synchronization-cache timestamps/counts and live IMAP IDLE task/subscriber counts without returning server host names or credentials. Public liveness (`/api/health`) and local-storage readiness (`/api/ready`) endpoints are available for service supervision.
- Search-index diagnostics expose cached/indexed counts, with a rebuild operation from the local SQLite cache.
- Search-index diagnostics expose cached/indexed counts, with a rebuild operation from the configured account-data backend.
- IMAP reconnect control drops reusable request-path sessions and verifies a fresh authenticated connection.
- Graceful SIGTERM/Ctrl+C shutdown stops accepting new requests and aborts dedicated IMAP IDLE tasks before process exit.
### Personalization
- Persistent per-user display preferences in SQLite.
- Persistent per-user display preferences in the configured application database.
- Light/dark/system theme.
- Presets: sage, blue, teal, cyan, indigo, slate, graphite, amber, orange, violet, rose, red and custom.
- Custom accent, background, panel and text colors.
+22 -12
View File
@@ -6,19 +6,24 @@ RustBox uses a flat Cargo layout, while Oxi uses a `backend/` subtree. The sync
## Path mapping
Examples:
Automatically mapped examples:
```text
Oxi backend/Cargo.toml -> RustBox Cargo.toml
Oxi backend/Cargo.lock -> RustBox Cargo.lock
Oxi backend/rust-toolchain.toml -> RustBox rust-toolchain.toml
Oxi backend/src/... -> RustBox src/...
Oxi backend/migrations/... -> RustBox migrations/...
Oxi backend/tests/... -> RustBox tests/...
Oxi backend/examples/... -> RustBox examples/...
Oxi backend/docs/... -> RustBox docs/...
```
Review-only boundaries, never mapped automatically:
```text
Oxi backend/migrations/... -> review against RustBox migrations/...
Oxi backend/tests/... -> review/port as native Rust tests when useful
```
RustBox-owned `src/web/` has no automatic mapping from the Oxi frontend.
## Configure the upstream
@@ -28,7 +33,7 @@ A release archive does not contain `.git`. Initialize the RustBox repository onc
```bash
git init
git add .
git commit -m "RustBox 0.0.1 baseline"
git commit -m "RustBox 0.1.1 baseline"
./scripts/setup-upstream.sh
```
@@ -55,10 +60,11 @@ or inspect a specific Oxi ref:
The report separates:
1. Oxi `backend/` changes that can be mapped into RustBox;
2. Oxi `frontend/` changes that require a manual UI port;
3. other Oxi root-level changes that require manual review;
4. upstream commits since `UPSTREAM_BASE`.
1. Oxi backend changes that can be mapped automatically;
2. Oxi backend migrations/tests that require manual review;
3. Oxi frontend changes that require a manual UI port;
4. other Oxi root-level changes that require manual review;
5. upstream commits since `UPSTREAM_BASE`.
## Advance the consumed Oxi backend state
@@ -72,10 +78,10 @@ The operation:
2. fetches `upstream-oxi`;
3. verifies that the target descends from the recorded `UPSTREAM_BASE`;
4. creates a `sync/oxi-*` review branch;
5. generates the Oxi backend delta from `UPSTREAM_BASE` to the target;
5. generates the Oxi backend delta from `UPSTREAM_BASE` to the target, excluding `backend/migrations/` and `backend/tests/`;
6. rewrites only patch metadata so `backend/<path>` becomes `<path>`;
7. applies the mapped delta with Git three-way merge support;
8. does not import Oxi frontend sources;
8. does not import Oxi frontend sources, migrations, or tests;
9. records the target as a pending sync inside Git metadata;
10. deliberately leaves `UPSTREAM_BASE` unchanged until the maintainer finishes review.
@@ -87,12 +93,14 @@ After resolving backend conflicts and porting any relevant UI behavior, finish t
./scripts/finish-upstream.sh
```
If Oxi changed its frontend or root-level files, the finish step refuses to advance the baseline until you explicitly acknowledge those reviews:
If Oxi changed its frontend, root-level files, or the RustBox-owned upstream migration/test areas, the finish step refuses to advance the baseline until those reviews are explicitly acknowledged:
```bash
./scripts/finish-upstream.sh --ui-reviewed --root-reviewed
./scripts/finish-upstream.sh --ui-reviewed --root-reviewed --rustbox-owned-reviewed
```
`--rustbox-owned-reviewed` confirms that changes under Oxi `backend/migrations/` and `backend/tests/` were inspected and any relevant behavior was manually ported. Those files are never copied into RustBox automatically.
The finish step runs the RustBox release build gate and only then stages the new `UPSTREAM_BASE`. Review `git status` and commit the sync. `--skip-build` exists for exceptional local workflows, but CI should still run the full gate before merge.
If the sync is rejected, discard the review branch; the main branch and its `UPSTREAM_BASE` remain unchanged.
@@ -123,6 +131,8 @@ Oxi frontend source is never copied into RustBox. For a new Oxi feature that inc
These areas require explicit review and should not be replaced blindly by upstream changes:
- `migrations/` (RustBox owns the squashed baseline and future migration numbering);
- `tests/` (RustBox keeps native Rust/runtime fixtures rather than importing upstream Python contracts);
- `src/web/`;
- `src/ui.rs`;
- RustBox UI routing and same-origin behavior;
+52
View File
@@ -0,0 +1,52 @@
# Database schemas and migrations
RustBox 0.1.1 has one database backend for the whole application:
```text
database_backend = sqlite | mysql | pgsql
session_backend = database | redis
```
When `session_backend = "database"`, sessions live in the same configured SQL database as the application data. `session_backend = "redis"` moves only session persistence to Redis; Redis Sentinel is a Redis discovery option and has no SQL schema.
## Layout
```text
migrations/
├── sqlite/
│ └── schema.sql
├── mysql/
│ └── schema.sql
└── pgsql/
└── schema.sql
```
Every `schema.sql` is the complete schema for that engine. It contains:
- global tables (`sessions`, `rustbox_accounts`), and
- the full account-data baseline at schema version 46.
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 the tenant part of each schema is replaced by RustBox at provisioning time. Sessions are global and therefore are not prefixed.
The SQLite baseline is structurally equivalent to the legacy V001-V046 migration chain. On first access, RustBox 0.1.1 imports an existing legacy `data_dir/<user_hash>/db.sqlite` at `PRAGMA user_version = 46` into the configured backend. Only after a successful import is the legacy file renamed to `db.sqlite.pre-0.1.1.bak`. Older legacy schemas must first be upgraded with RustBox 0.1.0.
## Future migrations
The next application schema change is **V047**. A schema-changing release must add the same logical migration for every supported SQL engine, for example:
```text
migrations/sqlite/V047__description.sql
migrations/mysql/V047__description.sql
migrations/pgsql/V047__description.sql
```
Tenant migration SQL uses `{{p}}` for account tables. Register each new migration in the corresponding migration array in `src/db/pool.rs` and keep the version numbers aligned across all three engines. Do not edit `schema.sql` alone for an upgrade that must work on existing installations; update both the baseline and the versioned migration.
Redis and Redis Sentinel do not use SQL migrations.
## Engine notes
- SQLite uses `data_dir/rustbox.sqlite` by default; `database_url` may point to another persistent SQLite file.
- MySQL requires `database_url` and targets MySQL 8.0.13 or newer.
- PostgreSQL requires `database_url`.
- MySQL/PostgreSQL are full application backends, not session-only databases.
-23
View File
@@ -1,23 +0,0 @@
CREATE TABLE IF NOT EXISTS sessions (
token VARCHAR(64) PRIMARY KEY,
account_id VARCHAR(64) NOT NULL UNIQUE,
browser_id VARCHAR(64) NOT NULL,
email VARCHAR(320) NOT NULL,
password_ciphertext TEXT NOT NULL,
user_hash VARCHAR(128) NOT NULL,
imap_host VARCHAR(255) NOT NULL,
imap_port BIGINT NOT NULL,
imap_tls BIGINT NOT NULL,
imap_tls_verify BIGINT NOT NULL,
smtp_host VARCHAR(255) NOT NULL,
smtp_port BIGINT NOT NULL,
smtp_tls BIGINT NOT NULL,
smtp_tls_verify BIGINT NOT NULL,
timeout_secs BIGINT NOT NULL,
created_at_unix BIGINT NOT NULL,
last_accessed_unix BIGINT NOT NULL,
user_agent VARCHAR(512) NOT NULL DEFAULT '',
INDEX idx_sessions_browser (browser_id),
INDEX idx_sessions_user (user_hash),
INDEX idx_sessions_expiry (last_accessed_unix, timeout_secs)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+424
View File
@@ -0,0 +1,424 @@
-- RustBox 0.1.1 MySQL schema.
-- One physical database contains global sessions plus isolated per-account tables.
-- Application tables are materialized by replacing {{p}} with a collision-checked prefix.
-- Baseline application schema version: 46. Future migrations start at V047.
-- RUSTBOX:GLOBAL-BEGIN
CREATE TABLE IF NOT EXISTS rustbox_accounts (
user_hash VARCHAR(128) PRIMARY KEY,
table_prefix VARCHAR(64) NOT NULL UNIQUE,
schema_version BIGINT NOT NULL DEFAULT 46,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS sessions (
token VARCHAR(64) PRIMARY KEY,
account_id VARCHAR(64) NOT NULL UNIQUE,
browser_id VARCHAR(64) NOT NULL,
email VARCHAR(320) NOT NULL,
password_ciphertext TEXT NOT NULL,
user_hash VARCHAR(128) NOT NULL,
imap_host VARCHAR(255) NOT NULL,
imap_port BIGINT NOT NULL,
imap_tls BIGINT NOT NULL,
imap_tls_verify BIGINT NOT NULL,
smtp_host VARCHAR(255) NOT NULL,
smtp_port BIGINT NOT NULL,
smtp_tls BIGINT NOT NULL,
smtp_tls_verify BIGINT NOT NULL,
timeout_secs BIGINT NOT NULL,
created_at_unix BIGINT NOT NULL,
last_accessed_unix BIGINT NOT NULL,
user_agent VARCHAR(512) NOT NULL DEFAULT '',
INDEX idx_sessions_browser (browser_id),
INDEX idx_sessions_user (user_hash),
INDEX idx_sessions_expiry (last_accessed_unix, timeout_secs)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- RUSTBOX:GLOBAL-END
-- RUSTBOX:TENANT-BEGIN
-- {{p}} is replaced at runtime with the account table prefix.
CREATE TABLE {{p}}calendar_events (
id VARCHAR(255) PRIMARY KEY,
title TEXT NOT NULL,
description LONGTEXT NOT NULL DEFAULT (''),
location TEXT NOT NULL DEFAULT (''),
start_time VARCHAR(255) NOT NULL,
end_time VARCHAR(255) NOT NULL,
all_day BIGINT NOT NULL DEFAULT 0,
recurrence_rule TEXT,
attendees LONGTEXT NOT NULL DEFAULT ('[]'),
organizer TEXT NOT NULL DEFAULT (''),
status TEXT NOT NULL DEFAULT ('confirmed'),
source TEXT NOT NULL DEFAULT ('manual'),
source_uid VARCHAR(255),
source_message_uid BIGINT,
source_message_folder TEXT,
meeting_url TEXT,
color TEXT,
reminder_minutes BIGINT,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}calendar_settings (
id BIGINT DEFAULT 1 PRIMARY KEY,
default_view TEXT NOT NULL DEFAULT ('month'),
week_starts_on BIGINT NOT NULL DEFAULT 0,
working_hours_start TEXT NOT NULL DEFAULT ('09:00'),
working_hours_end TEXT NOT NULL DEFAULT ('17:00'),
time_format TEXT NOT NULL DEFAULT ('12h'),
zoom_link TEXT NOT NULL DEFAULT (''),
teams_link TEXT NOT NULL DEFAULT (''),
meet_link TEXT NOT NULL DEFAULT (''),
caldav_url TEXT NOT NULL DEFAULT (''),
caldav_username TEXT NOT NULL DEFAULT (''),
caldav_password TEXT NOT NULL DEFAULT (''),
caldav_enabled BIGINT NOT NULL DEFAULT 0,
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}contact_group_members (
group_id VARCHAR(255) NOT NULL,
contact_id VARCHAR(255) NOT NULL,
PRIMARY KEY (group_id, contact_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}contact_groups (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(512) NOT NULL,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}contact_photos (
contact_id VARCHAR(255) PRIMARY KEY,
content_type TEXT NOT NULL,
data LONGBLOB NOT NULL,
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}contacts (
id VARCHAR(255) PRIMARY KEY,
email VARCHAR(320) NOT NULL,
name VARCHAR(512) NOT NULL DEFAULT '',
company TEXT NOT NULL DEFAULT (''),
notes LONGTEXT NOT NULL DEFAULT (''),
is_favorite BIGINT NOT NULL DEFAULT 0,
last_contacted VARCHAR(64),
contact_count BIGINT NOT NULL DEFAULT 0,
source TEXT NOT NULL DEFAULT ('manual'),
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
phone TEXT NOT NULL DEFAULT (''),
job_title TEXT NOT NULL DEFAULT (''),
address TEXT NOT NULL DEFAULT (''),
website TEXT NOT NULL DEFAULT (''),
birthday TEXT NOT NULL DEFAULT (''),
emails_json LONGTEXT NOT NULL DEFAULT ('[]'),
phones_json LONGTEXT NOT NULL DEFAULT ('[]'),
addresses_json LONGTEXT NOT NULL DEFAULT ('[]')
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}display_preferences (
id BIGINT DEFAULT 1 PRIMARY KEY,
density TEXT NOT NULL DEFAULT ('comfortable'),
theme TEXT NOT NULL DEFAULT ('system'),
language TEXT NOT NULL DEFAULT ('en'),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
compose_format TEXT NOT NULL DEFAULT ('html'),
deep_index BIGINT NOT NULL DEFAULT 0,
animation_mode TEXT,
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 BIGINT NOT NULL DEFAULT 13,
row_height BIGINT NOT NULL DEFAULT 58,
folder_pane_width BIGINT NOT NULL DEFAULT 228,
message_pane_width BIGINT NOT NULL DEFAULT 420,
border_radius BIGINT NOT NULL DEFAULT 4,
contrast TEXT NOT NULL DEFAULT ('normal'),
show_avatars BIGINT NOT NULL DEFAULT 1,
show_attachment_icons BIGINT NOT NULL DEFAULT 1,
show_flags BIGINT NOT NULL DEFAULT 1,
reduce_motion BIGINT 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 BIGINT NOT NULL DEFAULT 3000,
default_priority TEXT NOT NULL DEFAULT ('normal'),
request_mdn_default BIGINT NOT NULL DEFAULT 0,
attachment_reminder BIGINT NOT NULL DEFAULT 1,
icon_size BIGINT NOT NULL DEFAULT 18,
toolbar_density TEXT NOT NULL DEFAULT ('normal'),
sidebar_item_height BIGINT NOT NULL DEFAULT 29,
reader_max_width BIGINT NOT NULL DEFAULT 0,
composer_width BIGINT NOT NULL DEFAULT 900,
selection_color TEXT NOT NULL DEFAULT ('#28352e'),
unread_font_weight BIGINT NOT NULL DEFAULT 730,
unread_badge_style TEXT NOT NULL DEFAULT ('dot'),
compact_headers BIGINT NOT NULL DEFAULT 0,
show_borders BIGINT NOT NULL DEFAULT 1,
scrollbar_density TEXT NOT NULL DEFAULT ('thin'),
monochrome BIGINT 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 BIGINT NOT NULL DEFAULT 1,
interface_skin TEXT NOT NULL DEFAULT ('rustbox'),
preset_scale_unlocked BIGINT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}draft_attachments (
id VARCHAR(255) PRIMARY KEY,
draft_id VARCHAR(255) NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
size BIGINT NOT NULL,
file_path TEXT NOT NULL,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}drafts (
id VARCHAR(255) PRIMARY KEY,
to_addresses LONGTEXT NOT NULL DEFAULT (''),
cc_addresses LONGTEXT NOT NULL DEFAULT (''),
bcc_addresses LONGTEXT NOT NULL DEFAULT (''),
subject TEXT NOT NULL DEFAULT (''),
text_body LONGTEXT NOT NULL DEFAULT (''),
html_body LONGTEXT,
in_reply_to TEXT,
references_header LONGTEXT,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
request_mdn BIGINT NOT NULL DEFAULT 0,
priority TEXT NOT NULL DEFAULT ('normal'),
compose_format TEXT NOT NULL DEFAULT ('html'),
remote_folder TEXT,
remote_uid BIGINT,
remote_uid_validity BIGINT,
pgp_mode TEXT NOT NULL DEFAULT ('none'),
pgp_inline BIGINT NOT NULL DEFAULT 0,
pgp_attach_public_key BIGINT NOT NULL DEFAULT 0,
pgp_attach_recipient_keys BIGINT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}folder_preferences (
folder_name VARCHAR(512) PRIMARY KEY,
flagged_on_top BIGINT,
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}folders (
name VARCHAR(512) PRIMARY KEY,
delimiter TEXT,
parent TEXT,
flags TEXT NOT NULL DEFAULT (''),
is_subscribed BIGINT NOT NULL DEFAULT 1,
total_count BIGINT NOT NULL DEFAULT 0,
unread_count BIGINT NOT NULL DEFAULT 0,
uid_validity BIGINT NOT NULL DEFAULT 0,
highest_modseq BIGINT NOT NULL DEFAULT 0,
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
messages_updated_at VARCHAR(64)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}identities (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
display_name TEXT NOT NULL DEFAULT (''),
email TEXT NOT NULL,
signature_html LONGTEXT NOT NULL DEFAULT (''),
is_default BIGINT NOT NULL DEFAULT 0,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
reply_to TEXT NOT NULL DEFAULT (''),
bcc TEXT NOT NULL DEFAULT (''),
organization TEXT NOT NULL DEFAULT (''),
signature_text LONGTEXT NOT NULL DEFAULT (''),
signature_new BIGINT NOT NULL DEFAULT 1,
signature_reply BIGINT NOT NULL DEFAULT 1,
signature_forward BIGINT NOT NULL DEFAULT 1,
signature_position TEXT NOT NULL DEFAULT ('above'),
pgp_fingerprint TEXT,
pgp_sign_default BIGINT NOT NULL DEFAULT 0,
pgp_encrypt_default BIGINT NOT NULL DEFAULT 0,
pgp_inline_default BIGINT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}known_addresses (
email VARCHAR(320) NOT NULL PRIMARY KEY,
name VARCHAR(512) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}mail_preferences (
id BIGINT PRIMARY KEY,
messages_per_page BIGINT NOT NULL DEFAULT 50,
mark_read_delay_ms BIGINT NOT NULL DEFAULT 0,
remote_images TEXT NOT NULL DEFAULT ('ask'),
preferred_body TEXT NOT NULL DEFAULT ('auto'),
thread_mode TEXT NOT NULL DEFAULT ('threaded'),
collapse_quotes BIGINT NOT NULL DEFAULT 1,
show_snippets BIGINT NOT NULL DEFAULT 1,
default_sort TEXT NOT NULL DEFAULT ('date_desc'),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
flagged_on_top BIGINT NOT NULL DEFAULT 0,
sync_imap_keywords BIGINT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}meeting_templates (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name TEXT NOT NULL,
url_template TEXT NOT NULL,
icon TEXT NOT NULL DEFAULT (''),
is_default BIGINT NOT NULL DEFAULT 0,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}message_tags (
tag_id VARCHAR(128) NOT NULL,
message_uid BIGINT NOT NULL,
message_folder VARCHAR(512) NOT NULL,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
PRIMARY KEY (tag_id, message_uid, message_folder)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}messages (
uid BIGINT NOT NULL,
folder VARCHAR(512) NOT NULL,
message_id VARCHAR(255),
in_reply_to VARCHAR(255),
subject TEXT NOT NULL DEFAULT (''),
from_address TEXT NOT NULL DEFAULT (''),
from_name TEXT NOT NULL DEFAULT (''),
to_addresses LONGTEXT NOT NULL DEFAULT (''),
cc_addresses LONGTEXT NOT NULL DEFAULT (''),
date TEXT NOT NULL,
flags TEXT NOT NULL DEFAULT (''),
size BIGINT NOT NULL DEFAULT 0,
has_attachments BIGINT NOT NULL DEFAULT 0,
snippet TEXT NOT NULL DEFAULT (''),
body_cached BIGINT NOT NULL DEFAULT 0,
body_html LONGTEXT,
body_text LONGTEXT,
references_header LONGTEXT,
date_epoch BIGINT NOT NULL DEFAULT 0,
attachments_json LONGTEXT,
raw_headers LONGTEXT,
reaction TEXT,
email_theme BIGINT,
PRIMARY KEY (uid, folder)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}notification_preferences (
id BIGINT DEFAULT 1 PRIMARY KEY,
enabled BIGINT NOT NULL DEFAULT 1,
sound BIGINT NOT NULL DEFAULT 0,
folders TEXT NOT NULL DEFAULT ('["INBOX"]'),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
dnd_enabled BIGINT NOT NULL DEFAULT 0,
dnd_start TEXT NOT NULL DEFAULT ('22:00'),
dnd_end TEXT NOT NULL DEFAULT ('07:00')
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}pgp_keys (
fingerprint VARCHAR(255) PRIMARY KEY,
key_id TEXT NOT NULL,
email TEXT NOT NULL DEFAULT (''),
user_id TEXT NOT NULL DEFAULT (''),
public_key_armor LONGTEXT NOT NULL,
secret_key_path TEXT,
has_secret BIGINT NOT NULL DEFAULT 0,
trust TEXT NOT NULL DEFAULT ('unknown'),
source TEXT NOT NULL DEFAULT ('import'),
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}pgp_preferences (
id BIGINT PRIMARY KEY,
wkd_enabled BIGINT NOT NULL DEFAULT 1,
hkp_enabled BIGINT NOT NULL DEFAULT 0,
require_trusted_encryption BIGINT NOT NULL DEFAULT 0,
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}response_templates (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(512) NOT NULL,
body TEXT NOT NULL DEFAULT (''),
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
format TEXT NOT NULL DEFAULT ('text'),
UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}saved_searches (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(512) NOT NULL,
query_string LONGTEXT NOT NULL,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}special_folders (
role VARCHAR(255) PRIMARY KEY,
folder_name TEXT NOT NULL,
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}tags (
id VARCHAR(128) PRIMARY KEY,
name VARCHAR(512) NOT NULL,
color TEXT NOT NULL DEFAULT ('#6b7280'),
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
imap_keyword VARCHAR(255) NOT NULL DEFAULT '',
UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}trusted_senders (
email VARCHAR(320) PRIMARY KEY,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}user_meta (
key VARCHAR(255) PRIMARY KEY,
value TEXT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
ALTER TABLE {{p}}contact_group_members ADD CONSTRAINT {{p}}fk_contact_group_members_0 FOREIGN KEY (contact_id) REFERENCES {{p}}contacts (id) ON DELETE CASCADE;
ALTER TABLE {{p}}contact_group_members ADD CONSTRAINT {{p}}fk_contact_group_members_1 FOREIGN KEY (group_id) REFERENCES {{p}}contact_groups (id) ON DELETE CASCADE;
ALTER TABLE {{p}}contact_photos ADD CONSTRAINT {{p}}fk_contact_photos_0 FOREIGN KEY (contact_id) REFERENCES {{p}}contacts (id) ON DELETE CASCADE;
ALTER TABLE {{p}}draft_attachments ADD CONSTRAINT {{p}}fk_draft_attachments_0 FOREIGN KEY (draft_id) REFERENCES {{p}}drafts (id) ON DELETE CASCADE;
ALTER TABLE {{p}}folder_preferences ADD CONSTRAINT {{p}}fk_folder_preferences_0 FOREIGN KEY (folder_name) REFERENCES {{p}}folders (name) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE {{p}}message_tags ADD CONSTRAINT {{p}}fk_message_tags_0 FOREIGN KEY (message_uid, message_folder) REFERENCES {{p}}messages (uid, folder) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE {{p}}message_tags ADD CONSTRAINT {{p}}fk_message_tags_1 FOREIGN KEY (tag_id) REFERENCES {{p}}tags (id) ON DELETE CASCADE;
ALTER TABLE {{p}}messages ADD CONSTRAINT {{p}}fk_messages_0 FOREIGN KEY (folder) REFERENCES {{p}}folders (name) ON DELETE CASCADE;
CREATE INDEX {{p}}idx_calendar_events_source_uid ON {{p}}calendar_events (source_uid);
CREATE INDEX {{p}}idx_calendar_events_time ON {{p}}calendar_events (start_time, end_time);
CREATE INDEX {{p}}idx_cgm_contact_id ON {{p}}contact_group_members (contact_id);
CREATE INDEX {{p}}idx_contacts_contact_count ON {{p}}contacts (contact_count DESC);
CREATE UNIQUE INDEX {{p}}idx_contacts_email ON {{p}}contacts (email);
CREATE INDEX {{p}}idx_contacts_last_contacted ON {{p}}contacts (last_contacted DESC);
CREATE INDEX {{p}}idx_contacts_name ON {{p}}contacts (name);
CREATE INDEX {{p}}idx_known_addresses_name ON {{p}}known_addresses (name);
CREATE INDEX {{p}}idx_message_tags_message ON {{p}}message_tags (message_uid, message_folder);
CREATE INDEX {{p}}idx_message_tags_tag ON {{p}}message_tags (tag_id);
CREATE INDEX {{p}}idx_messages_folder_date ON {{p}}messages (folder, date_epoch DESC);
CREATE INDEX {{p}}idx_messages_in_reply_to ON {{p}}messages (in_reply_to);
CREATE INDEX {{p}}idx_messages_message_id ON {{p}}messages (message_id);
CREATE INDEX {{p}}idx_pgp_keys_email ON {{p}}pgp_keys (email);
CREATE UNIQUE INDEX {{p}}idx_saved_searches_name ON {{p}}saved_searches (name);
-- RUSTBOX:TENANT-END
-23
View File
@@ -1,23 +0,0 @@
CREATE TABLE IF NOT EXISTS sessions (
token VARCHAR(64) PRIMARY KEY,
account_id VARCHAR(64) NOT NULL UNIQUE,
browser_id VARCHAR(64) NOT NULL,
email VARCHAR(320) NOT NULL,
password_ciphertext TEXT NOT NULL,
user_hash VARCHAR(128) NOT NULL,
imap_host VARCHAR(255) NOT NULL,
imap_port BIGINT NOT NULL,
imap_tls BIGINT NOT NULL,
imap_tls_verify BIGINT NOT NULL,
smtp_host VARCHAR(255) NOT NULL,
smtp_port BIGINT NOT NULL,
smtp_tls BIGINT NOT NULL,
smtp_tls_verify BIGINT NOT NULL,
timeout_secs BIGINT NOT NULL,
created_at_unix BIGINT NOT NULL,
last_accessed_unix BIGINT NOT NULL,
user_agent VARCHAR(512) NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_sessions_browser ON sessions(browser_id);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_hash);
CREATE INDEX IF NOT EXISTS idx_sessions_expiry ON sessions(last_accessed_unix, timeout_secs);
+425
View File
@@ -0,0 +1,425 @@
-- RustBox 0.1.1 PostgreSQL schema.
-- One physical database contains global sessions plus isolated per-account tables.
-- Application tables are materialized by replacing {{p}} with a collision-checked prefix.
-- Baseline application schema version: 46. Future migrations start at V047.
-- RUSTBOX:GLOBAL-BEGIN
CREATE TABLE IF NOT EXISTS rustbox_accounts (
user_hash VARCHAR(128) PRIMARY KEY,
table_prefix VARCHAR(64) NOT NULL UNIQUE,
schema_version BIGINT NOT NULL DEFAULT 46,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
CREATE TABLE IF NOT EXISTS sessions (
token VARCHAR(64) PRIMARY KEY,
account_id VARCHAR(64) NOT NULL UNIQUE,
browser_id VARCHAR(64) NOT NULL,
email VARCHAR(320) NOT NULL,
password_ciphertext TEXT NOT NULL,
user_hash VARCHAR(128) NOT NULL,
imap_host VARCHAR(255) NOT NULL,
imap_port BIGINT NOT NULL,
imap_tls BIGINT NOT NULL,
imap_tls_verify BIGINT NOT NULL,
smtp_host VARCHAR(255) NOT NULL,
smtp_port BIGINT NOT NULL,
smtp_tls BIGINT NOT NULL,
smtp_tls_verify BIGINT NOT NULL,
timeout_secs BIGINT NOT NULL,
created_at_unix BIGINT NOT NULL,
last_accessed_unix BIGINT NOT NULL,
user_agent VARCHAR(512) NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_sessions_browser ON sessions(browser_id);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_hash);
CREATE INDEX IF NOT EXISTS idx_sessions_expiry ON sessions(last_accessed_unix, timeout_secs);
-- RUSTBOX:GLOBAL-END
-- RUSTBOX:TENANT-BEGIN
-- {{p}} is replaced at runtime with the account table prefix.
CREATE TABLE {{p}}calendar_events (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
location TEXT NOT NULL DEFAULT '',
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
all_day BIGINT NOT NULL DEFAULT 0,
recurrence_rule TEXT,
attendees TEXT NOT NULL DEFAULT '[]',
organizer TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'confirmed',
source TEXT NOT NULL DEFAULT 'manual',
source_uid TEXT,
source_message_uid BIGINT,
source_message_folder TEXT,
meeting_url TEXT,
color TEXT,
reminder_minutes BIGINT,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
CREATE TABLE {{p}}calendar_settings (
id BIGINT DEFAULT 1 PRIMARY KEY,
default_view TEXT NOT NULL DEFAULT 'month',
week_starts_on BIGINT NOT NULL DEFAULT 0,
working_hours_start TEXT NOT NULL DEFAULT '09:00',
working_hours_end TEXT NOT NULL DEFAULT '17:00',
time_format TEXT NOT NULL DEFAULT '12h',
zoom_link TEXT NOT NULL DEFAULT '',
teams_link TEXT NOT NULL DEFAULT '',
meet_link TEXT NOT NULL DEFAULT '',
caldav_url TEXT NOT NULL DEFAULT '',
caldav_username TEXT NOT NULL DEFAULT '',
caldav_password TEXT NOT NULL DEFAULT '',
caldav_enabled BIGINT NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
CREATE TABLE {{p}}contact_group_members (
group_id TEXT NOT NULL,
contact_id TEXT NOT NULL,
PRIMARY KEY (group_id, contact_id)
);
CREATE TABLE {{p}}contact_groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
UNIQUE (name)
);
CREATE TABLE {{p}}contact_photos (
contact_id TEXT PRIMARY KEY,
content_type TEXT NOT NULL,
data BYTEA NOT NULL,
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
CREATE TABLE {{p}}contacts (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
company TEXT NOT NULL DEFAULT '',
notes TEXT NOT NULL DEFAULT '',
is_favorite BIGINT NOT NULL DEFAULT 0,
last_contacted TEXT,
contact_count BIGINT NOT NULL DEFAULT 0,
source TEXT NOT NULL DEFAULT 'manual',
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
phone TEXT NOT NULL DEFAULT '',
job_title TEXT NOT NULL DEFAULT '',
address TEXT NOT NULL DEFAULT '',
website TEXT NOT NULL DEFAULT '',
birthday TEXT NOT NULL DEFAULT '',
emails_json TEXT NOT NULL DEFAULT '[]',
phones_json TEXT NOT NULL DEFAULT '[]',
addresses_json TEXT NOT NULL DEFAULT '[]'
);
CREATE TABLE {{p}}display_preferences (
id BIGINT DEFAULT 1 PRIMARY KEY,
density TEXT NOT NULL DEFAULT 'comfortable',
theme TEXT NOT NULL DEFAULT 'system',
language TEXT NOT NULL DEFAULT 'en',
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
compose_format TEXT NOT NULL DEFAULT 'html',
deep_index BIGINT NOT NULL DEFAULT 0,
animation_mode TEXT,
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 BIGINT NOT NULL DEFAULT 13,
row_height BIGINT NOT NULL DEFAULT 58,
folder_pane_width BIGINT NOT NULL DEFAULT 228,
message_pane_width BIGINT NOT NULL DEFAULT 420,
border_radius BIGINT NOT NULL DEFAULT 4,
contrast TEXT NOT NULL DEFAULT 'normal',
show_avatars BIGINT NOT NULL DEFAULT 1,
show_attachment_icons BIGINT NOT NULL DEFAULT 1,
show_flags BIGINT NOT NULL DEFAULT 1,
reduce_motion BIGINT 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 BIGINT NOT NULL DEFAULT 3000,
default_priority TEXT NOT NULL DEFAULT 'normal',
request_mdn_default BIGINT NOT NULL DEFAULT 0,
attachment_reminder BIGINT NOT NULL DEFAULT 1,
icon_size BIGINT NOT NULL DEFAULT 18,
toolbar_density TEXT NOT NULL DEFAULT 'normal',
sidebar_item_height BIGINT NOT NULL DEFAULT 29,
reader_max_width BIGINT NOT NULL DEFAULT 0,
composer_width BIGINT NOT NULL DEFAULT 900,
selection_color TEXT NOT NULL DEFAULT '#28352e',
unread_font_weight BIGINT NOT NULL DEFAULT 730,
unread_badge_style TEXT NOT NULL DEFAULT 'dot',
compact_headers BIGINT NOT NULL DEFAULT 0,
show_borders BIGINT NOT NULL DEFAULT 1,
scrollbar_density TEXT NOT NULL DEFAULT 'thin',
monochrome BIGINT 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 BIGINT NOT NULL DEFAULT 1,
interface_skin TEXT NOT NULL DEFAULT 'rustbox',
preset_scale_unlocked BIGINT NOT NULL DEFAULT 0
);
CREATE TABLE {{p}}draft_attachments (
id TEXT PRIMARY KEY,
draft_id TEXT NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
size BIGINT NOT NULL,
file_path TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
CREATE TABLE {{p}}drafts (
id TEXT PRIMARY KEY,
to_addresses TEXT NOT NULL DEFAULT '',
cc_addresses TEXT NOT NULL DEFAULT '',
bcc_addresses TEXT NOT NULL DEFAULT '',
subject TEXT NOT NULL DEFAULT '',
text_body TEXT NOT NULL DEFAULT '',
html_body TEXT,
in_reply_to TEXT,
references_header TEXT,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
request_mdn BIGINT NOT NULL DEFAULT 0,
priority TEXT NOT NULL DEFAULT 'normal',
compose_format TEXT NOT NULL DEFAULT 'html',
remote_folder TEXT,
remote_uid BIGINT,
remote_uid_validity BIGINT,
pgp_mode TEXT NOT NULL DEFAULT 'none',
pgp_inline BIGINT NOT NULL DEFAULT 0,
pgp_attach_public_key BIGINT NOT NULL DEFAULT 0,
pgp_attach_recipient_keys BIGINT NOT NULL DEFAULT 0
);
CREATE TABLE {{p}}folder_preferences (
folder_name TEXT PRIMARY KEY,
flagged_on_top BIGINT,
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
CREATE TABLE {{p}}folders (
name TEXT PRIMARY KEY,
delimiter TEXT,
parent TEXT,
flags TEXT NOT NULL DEFAULT '',
is_subscribed BIGINT NOT NULL DEFAULT 1,
total_count BIGINT NOT NULL DEFAULT 0,
unread_count BIGINT NOT NULL DEFAULT 0,
uid_validity BIGINT NOT NULL DEFAULT 0,
highest_modseq BIGINT NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
messages_updated_at TEXT
);
CREATE TABLE {{p}}identities (
id BIGSERIAL PRIMARY KEY,
display_name TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL,
signature_html TEXT NOT NULL DEFAULT '',
is_default BIGINT NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
reply_to TEXT NOT NULL DEFAULT '',
bcc TEXT NOT NULL DEFAULT '',
organization TEXT NOT NULL DEFAULT '',
signature_text TEXT NOT NULL DEFAULT '',
signature_new BIGINT NOT NULL DEFAULT 1,
signature_reply BIGINT NOT NULL DEFAULT 1,
signature_forward BIGINT NOT NULL DEFAULT 1,
signature_position TEXT NOT NULL DEFAULT 'above',
pgp_fingerprint TEXT,
pgp_sign_default BIGINT NOT NULL DEFAULT 0,
pgp_encrypt_default BIGINT NOT NULL DEFAULT 0,
pgp_inline_default BIGINT NOT NULL DEFAULT 0
);
CREATE TABLE {{p}}known_addresses (
email TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL DEFAULT ''
);
CREATE TABLE {{p}}mail_preferences (
id BIGINT PRIMARY KEY,
messages_per_page BIGINT NOT NULL DEFAULT 50,
mark_read_delay_ms BIGINT NOT NULL DEFAULT 0,
remote_images TEXT NOT NULL DEFAULT 'ask',
preferred_body TEXT NOT NULL DEFAULT 'auto',
thread_mode TEXT NOT NULL DEFAULT 'threaded',
collapse_quotes BIGINT NOT NULL DEFAULT 1,
show_snippets BIGINT NOT NULL DEFAULT 1,
default_sort TEXT NOT NULL DEFAULT 'date_desc',
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
flagged_on_top BIGINT NOT NULL DEFAULT 0,
sync_imap_keywords BIGINT NOT NULL DEFAULT 0
);
CREATE TABLE {{p}}meeting_templates (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
url_template TEXT NOT NULL,
icon TEXT NOT NULL DEFAULT '',
is_default BIGINT NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
CREATE TABLE {{p}}message_tags (
tag_id TEXT NOT NULL,
message_uid BIGINT NOT NULL,
message_folder TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
PRIMARY KEY (tag_id, message_uid, message_folder)
);
CREATE TABLE {{p}}messages (
uid BIGINT NOT NULL,
folder TEXT NOT NULL,
message_id TEXT,
in_reply_to TEXT,
subject TEXT NOT NULL DEFAULT '',
from_address TEXT NOT NULL DEFAULT '',
from_name TEXT NOT NULL DEFAULT '',
to_addresses TEXT NOT NULL DEFAULT '',
cc_addresses TEXT NOT NULL DEFAULT '',
date TEXT NOT NULL,
flags TEXT NOT NULL DEFAULT '',
size BIGINT NOT NULL DEFAULT 0,
has_attachments BIGINT NOT NULL DEFAULT 0,
snippet TEXT NOT NULL DEFAULT '',
body_cached BIGINT NOT NULL DEFAULT 0,
body_html TEXT,
body_text TEXT,
references_header TEXT,
date_epoch BIGINT NOT NULL DEFAULT 0,
attachments_json TEXT,
raw_headers TEXT,
reaction TEXT,
email_theme BIGINT,
PRIMARY KEY (uid, folder)
);
CREATE TABLE {{p}}notification_preferences (
id BIGINT DEFAULT 1 PRIMARY KEY,
enabled BIGINT NOT NULL DEFAULT 1,
sound BIGINT NOT NULL DEFAULT 0,
folders TEXT NOT NULL DEFAULT '["INBOX"]',
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
dnd_enabled BIGINT NOT NULL DEFAULT 0,
dnd_start TEXT NOT NULL DEFAULT '22:00',
dnd_end TEXT NOT NULL DEFAULT '07:00'
);
CREATE TABLE {{p}}pgp_keys (
fingerprint TEXT PRIMARY KEY,
key_id TEXT NOT NULL,
email TEXT NOT NULL DEFAULT '',
user_id TEXT NOT NULL DEFAULT '',
public_key_armor TEXT NOT NULL,
secret_key_path TEXT,
has_secret BIGINT NOT NULL DEFAULT 0,
trust TEXT NOT NULL DEFAULT 'unknown',
source TEXT NOT NULL DEFAULT 'import',
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
CREATE TABLE {{p}}pgp_preferences (
id BIGINT PRIMARY KEY,
wkd_enabled BIGINT NOT NULL DEFAULT 1,
hkp_enabled BIGINT NOT NULL DEFAULT 0,
require_trusted_encryption BIGINT NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
CREATE TABLE {{p}}response_templates (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
body TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
format TEXT NOT NULL DEFAULT 'text',
UNIQUE (name)
);
CREATE TABLE {{p}}saved_searches (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
query_string TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
CREATE TABLE {{p}}special_folders (
role TEXT PRIMARY KEY,
folder_name TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
CREATE TABLE {{p}}tags (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '#6b7280',
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
imap_keyword TEXT NOT NULL DEFAULT '',
UNIQUE (name)
);
CREATE TABLE {{p}}trusted_senders (
email TEXT PRIMARY KEY,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
CREATE TABLE {{p}}user_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
ALTER TABLE {{p}}contact_group_members ADD CONSTRAINT {{p}}fk_contact_group_members_0 FOREIGN KEY (contact_id) REFERENCES {{p}}contacts (id) ON DELETE CASCADE;
ALTER TABLE {{p}}contact_group_members ADD CONSTRAINT {{p}}fk_contact_group_members_1 FOREIGN KEY (group_id) REFERENCES {{p}}contact_groups (id) ON DELETE CASCADE;
ALTER TABLE {{p}}contact_photos ADD CONSTRAINT {{p}}fk_contact_photos_0 FOREIGN KEY (contact_id) REFERENCES {{p}}contacts (id) ON DELETE CASCADE;
ALTER TABLE {{p}}draft_attachments ADD CONSTRAINT {{p}}fk_draft_attachments_0 FOREIGN KEY (draft_id) REFERENCES {{p}}drafts (id) ON DELETE CASCADE;
ALTER TABLE {{p}}folder_preferences ADD CONSTRAINT {{p}}fk_folder_preferences_0 FOREIGN KEY (folder_name) REFERENCES {{p}}folders (name) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE {{p}}message_tags ADD CONSTRAINT {{p}}fk_message_tags_0 FOREIGN KEY (message_uid, message_folder) REFERENCES {{p}}messages (uid, folder) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE {{p}}message_tags ADD CONSTRAINT {{p}}fk_message_tags_1 FOREIGN KEY (tag_id) REFERENCES {{p}}tags (id) ON DELETE CASCADE;
ALTER TABLE {{p}}messages ADD CONSTRAINT {{p}}fk_messages_0 FOREIGN KEY (folder) REFERENCES {{p}}folders (name) ON DELETE CASCADE;
CREATE INDEX {{p}}idx_calendar_events_source_uid ON {{p}}calendar_events (source_uid);
CREATE INDEX {{p}}idx_calendar_events_time ON {{p}}calendar_events (start_time, end_time);
CREATE INDEX {{p}}idx_cgm_contact_id ON {{p}}contact_group_members (contact_id);
CREATE INDEX {{p}}idx_contacts_contact_count ON {{p}}contacts (contact_count DESC);
CREATE UNIQUE INDEX {{p}}idx_contacts_email ON {{p}}contacts (email);
CREATE INDEX {{p}}idx_contacts_last_contacted ON {{p}}contacts (last_contacted DESC);
CREATE INDEX {{p}}idx_contacts_name ON {{p}}contacts (name);
CREATE INDEX {{p}}idx_known_addresses_name ON {{p}}known_addresses (name);
CREATE INDEX {{p}}idx_message_tags_message ON {{p}}message_tags (message_uid, message_folder);
CREATE INDEX {{p}}idx_message_tags_tag ON {{p}}message_tags (tag_id);
CREATE INDEX {{p}}idx_messages_folder_date ON {{p}}messages (folder, date_epoch DESC);
CREATE INDEX {{p}}idx_messages_in_reply_to ON {{p}}messages (in_reply_to);
CREATE INDEX {{p}}idx_messages_message_id ON {{p}}messages (message_id);
CREATE INDEX {{p}}idx_pgp_keys_email ON {{p}}pgp_keys (lower(email));
CREATE UNIQUE INDEX {{p}}idx_saved_searches_name ON {{p}}saved_searches (name);
CREATE UNIQUE INDEX {{p}}idx_tags_imap_keyword ON {{p}}tags (imap_keyword) WHERE imap_keyword <> '';
-- RUSTBOX:TENANT-END
@@ -1,6 +0,0 @@
CREATE TABLE IF NOT EXISTS user_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
INSERT OR IGNORE INTO user_meta (key, value) VALUES ('schema_version', '1');
@@ -1,41 +0,0 @@
-- Folder cache: stores the IMAP folder hierarchy
CREATE TABLE IF NOT EXISTS folders (
name TEXT PRIMARY KEY,
delimiter TEXT,
parent TEXT,
flags TEXT NOT NULL DEFAULT '',
is_subscribed INTEGER NOT NULL DEFAULT 1,
total_count INTEGER NOT NULL DEFAULT 0,
unread_count INTEGER NOT NULL DEFAULT 0,
uid_validity INTEGER NOT NULL DEFAULT 0,
highest_modseq INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Message header cache: stores synced email headers per folder
CREATE TABLE IF NOT EXISTS messages (
uid INTEGER NOT NULL,
folder TEXT NOT NULL,
message_id TEXT,
in_reply_to TEXT,
subject TEXT NOT NULL DEFAULT '',
from_address TEXT NOT NULL DEFAULT '',
from_name TEXT NOT NULL DEFAULT '',
to_addresses TEXT NOT NULL DEFAULT '',
cc_addresses TEXT NOT NULL DEFAULT '',
date TEXT NOT NULL,
flags TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
has_attachments INTEGER NOT NULL DEFAULT 0,
snippet TEXT NOT NULL DEFAULT '',
body_cached INTEGER NOT NULL DEFAULT 0,
body_html TEXT,
body_text TEXT,
references_header TEXT,
PRIMARY KEY (uid, folder),
FOREIGN KEY (folder) REFERENCES folders(name) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_messages_folder_date ON messages(folder, date DESC);
CREATE INDEX IF NOT EXISTS idx_messages_message_id ON messages(message_id);
CREATE INDEX IF NOT EXISTS idx_messages_in_reply_to ON messages(in_reply_to);
@@ -1,6 +0,0 @@
-- Add a sortable integer timestamp column for proper date ordering.
ALTER TABLE messages ADD COLUMN date_epoch INTEGER NOT NULL DEFAULT 0;
-- Rebuild index to use the new column for sorting.
DROP INDEX IF EXISTS idx_messages_folder_date;
CREATE INDEX idx_messages_folder_date ON messages(folder, date_epoch DESC);
@@ -1,3 +0,0 @@
-- Separate timestamp for when a folder's messages were last synced from IMAP,
-- distinct from `updated_at` which tracks when the folder list was last synced.
ALTER TABLE folders ADD COLUMN messages_updated_at TEXT;
@@ -1,24 +0,0 @@
CREATE TABLE IF NOT EXISTS drafts (
id TEXT PRIMARY KEY,
to_addresses TEXT NOT NULL DEFAULT '',
cc_addresses TEXT NOT NULL DEFAULT '',
bcc_addresses TEXT NOT NULL DEFAULT '',
subject TEXT NOT NULL DEFAULT '',
text_body TEXT NOT NULL DEFAULT '',
html_body TEXT,
in_reply_to TEXT,
references_header TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS draft_attachments (
id TEXT PRIMARY KEY,
draft_id TEXT NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
size INTEGER NOT NULL,
file_path TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (draft_id) REFERENCES drafts(id) ON DELETE CASCADE
);
@@ -1,3 +0,0 @@
-- Cache attachment metadata and raw headers alongside the message body.
ALTER TABLE messages ADD COLUMN attachments_json TEXT;
ALTER TABLE messages ADD COLUMN raw_headers TEXT;
-18
View File
@@ -1,18 +0,0 @@
CREATE TABLE IF NOT EXISTS contacts (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
company TEXT NOT NULL DEFAULT '',
notes TEXT NOT NULL DEFAULT '',
is_favorite INTEGER NOT NULL DEFAULT 0,
last_contacted TEXT,
contact_count INTEGER NOT NULL DEFAULT 0,
source TEXT NOT NULL DEFAULT 'manual',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_email ON contacts(email);
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
CREATE INDEX IF NOT EXISTS idx_contacts_last_contacted ON contacts(last_contacted DESC);
CREATE INDEX IF NOT EXISTS idx_contacts_contact_count ON contacts(contact_count DESC);
-9
View File
@@ -1,9 +0,0 @@
CREATE TABLE IF NOT EXISTS identities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
display_name TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL,
signature_html TEXT NOT NULL DEFAULT '',
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -1,9 +0,0 @@
CREATE TABLE IF NOT EXISTS notification_preferences (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
enabled INTEGER NOT NULL DEFAULT 1,
sound INTEGER NOT NULL DEFAULT 0,
folders TEXT NOT NULL DEFAULT '["INBOX"]',
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT OR IGNORE INTO notification_preferences (id) VALUES (1);
@@ -1,16 +0,0 @@
CREATE TABLE IF NOT EXISTS contact_groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS contact_group_members (
group_id TEXT NOT NULL,
contact_id TEXT NOT NULL,
PRIMARY KEY (group_id, contact_id),
FOREIGN KEY (group_id) REFERENCES contact_groups(id) ON DELETE CASCADE,
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_cgm_contact_id ON contact_group_members(contact_id);
@@ -1 +0,0 @@
ALTER TABLE messages ADD COLUMN reaction TEXT;
-20
View File
@@ -1,20 +0,0 @@
CREATE TABLE IF NOT EXISTS tags (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
color TEXT NOT NULL DEFAULT '#6b7280',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS message_tags (
tag_id TEXT NOT NULL,
message_uid INTEGER NOT NULL,
message_folder TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (tag_id, message_uid, message_folder),
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
FOREIGN KEY (message_uid, message_folder) REFERENCES messages(uid, folder) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_message_tags_message ON message_tags(message_uid, message_folder);
CREATE INDEX IF NOT EXISTS idx_message_tags_tag ON message_tags(tag_id);
@@ -1,7 +0,0 @@
CREATE TABLE IF NOT EXISTS display_preferences (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
density TEXT NOT NULL DEFAULT 'comfortable',
theme TEXT NOT NULL DEFAULT 'system',
language TEXT NOT NULL DEFAULT 'en',
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -1 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN compose_format TEXT NOT NULL DEFAULT 'html';
-1
View File
@@ -1 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN deep_index BOOLEAN NOT NULL DEFAULT 0;
-51
View File
@@ -1,51 +0,0 @@
CREATE TABLE calendar_events (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
location TEXT NOT NULL DEFAULT '',
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
all_day INTEGER NOT NULL DEFAULT 0,
recurrence_rule TEXT,
attendees TEXT NOT NULL DEFAULT '[]',
organizer TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'confirmed',
source TEXT NOT NULL DEFAULT 'manual',
source_uid TEXT,
source_message_uid INTEGER,
source_message_folder TEXT,
meeting_url TEXT,
color TEXT,
reminder_minutes INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_calendar_events_time ON calendar_events(start_time, end_time);
CREATE INDEX idx_calendar_events_source_uid ON calendar_events(source_uid);
CREATE TABLE calendar_settings (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
default_view TEXT NOT NULL DEFAULT 'month',
week_starts_on INTEGER NOT NULL DEFAULT 0,
working_hours_start TEXT NOT NULL DEFAULT '09:00',
working_hours_end TEXT NOT NULL DEFAULT '17:00',
time_format TEXT NOT NULL DEFAULT '12h',
zoom_link TEXT NOT NULL DEFAULT '',
teams_link TEXT NOT NULL DEFAULT '',
meet_link TEXT NOT NULL DEFAULT '',
caldav_url TEXT NOT NULL DEFAULT '',
caldav_username TEXT NOT NULL DEFAULT '',
caldav_password TEXT NOT NULL DEFAULT '',
caldav_enabled INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE meeting_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url_template TEXT NOT NULL,
icon TEXT NOT NULL DEFAULT '',
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -1,4 +0,0 @@
-- Add email_theme column to messages table
-- NULL = not yet detected (only relevant when body_html exists)
-- 0 = Light, 1 = Dark, 2 = Transparent
ALTER TABLE messages ADD COLUMN email_theme INTEGER;
@@ -1,2 +0,0 @@
ALTER TABLE display_preferences
ADD COLUMN animation_mode TEXT NULL;
@@ -1,34 +0,0 @@
-- Denormalized table of known email addresses extracted from message headers.
-- Avoids expensive json_each() scanning of to_addresses/cc_addresses columns.
CREATE TABLE IF NOT EXISTS known_addresses (
email TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
PRIMARY KEY (email)
);
CREATE INDEX IF NOT EXISTS idx_known_addresses_name ON known_addresses(name);
-- Backfill from existing messages: from_address
INSERT INTO known_addresses (email, name)
SELECT from_address, COALESCE(from_name, '')
FROM messages
WHERE from_address != ''
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != '';
-- Backfill from existing messages: to_addresses (JSON array)
INSERT INTO known_addresses (email, name)
SELECT value->>'address', COALESCE(value->>'name', '')
FROM messages, json_each(to_addresses)
WHERE json_valid(to_addresses)
AND value->>'address' IS NOT NULL
AND value->>'address' != ''
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != '';
-- Backfill from existing messages: cc_addresses (JSON array)
INSERT INTO known_addresses (email, name)
SELECT value->>'address', COALESCE(value->>'name', '')
FROM messages, json_each(cc_addresses)
WHERE json_valid(cc_addresses)
AND value->>'address' IS NOT NULL
AND value->>'address' != ''
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != '';
@@ -1,17 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN preview_pane TEXT NOT NULL DEFAULT 'right';
ALTER TABLE display_preferences ADD COLUMN ui_scale TEXT NOT NULL DEFAULT 'normal';
ALTER TABLE display_preferences ADD COLUMN color_preset TEXT NOT NULL DEFAULT 'sage';
ALTER TABLE display_preferences ADD COLUMN accent_color TEXT NOT NULL DEFAULT '#6f9c86';
ALTER TABLE display_preferences ADD COLUMN font_family TEXT NOT NULL DEFAULT 'system';
ALTER TABLE display_preferences ADD COLUMN font_size INTEGER NOT NULL DEFAULT 13;
ALTER TABLE display_preferences ADD COLUMN row_height INTEGER NOT NULL DEFAULT 58;
ALTER TABLE display_preferences ADD COLUMN folder_pane_width INTEGER NOT NULL DEFAULT 228;
ALTER TABLE display_preferences ADD COLUMN message_pane_width INTEGER NOT NULL DEFAULT 420;
ALTER TABLE display_preferences ADD COLUMN border_radius INTEGER NOT NULL DEFAULT 4;
ALTER TABLE display_preferences ADD COLUMN contrast TEXT NOT NULL DEFAULT 'normal';
ALTER TABLE display_preferences ADD COLUMN show_avatars INTEGER NOT NULL DEFAULT 1;
ALTER TABLE display_preferences ADD COLUMN show_attachment_icons INTEGER NOT NULL DEFAULT 1;
ALTER TABLE display_preferences ADD COLUMN show_flags INTEGER NOT NULL DEFAULT 1;
ALTER TABLE display_preferences ADD COLUMN reduce_motion INTEGER NOT NULL DEFAULT 0;
ALTER TABLE display_preferences ADD COLUMN date_format TEXT NOT NULL DEFAULT 'locale';
ALTER TABLE display_preferences ADD COLUMN time_format TEXT NOT NULL DEFAULT 'locale';
@@ -1,5 +0,0 @@
CREATE TABLE IF NOT EXISTS special_folders (
role TEXT PRIMARY KEY CHECK(role IN ('sent','drafts','trash','junk','archive')),
folder_name TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -1,8 +0,0 @@
ALTER TABLE identities ADD COLUMN reply_to TEXT NOT NULL DEFAULT '';
ALTER TABLE identities ADD COLUMN bcc TEXT NOT NULL DEFAULT '';
ALTER TABLE identities ADD COLUMN organization TEXT NOT NULL DEFAULT '';
ALTER TABLE identities ADD COLUMN signature_text TEXT NOT NULL DEFAULT '';
ALTER TABLE identities ADD COLUMN signature_new INTEGER NOT NULL DEFAULT 1;
ALTER TABLE identities ADD COLUMN signature_reply INTEGER NOT NULL DEFAULT 1;
ALTER TABLE identities ADD COLUMN signature_forward INTEGER NOT NULL DEFAULT 1;
ALTER TABLE identities ADD COLUMN signature_position TEXT NOT NULL DEFAULT 'above';
@@ -1,9 +0,0 @@
CREATE TABLE IF NOT EXISTS saved_searches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
query_string TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_saved_searches_name ON saved_searches(name COLLATE NOCASE);
@@ -1,3 +0,0 @@
ALTER TABLE notification_preferences ADD COLUMN dnd_enabled INTEGER NOT NULL DEFAULT 0;
ALTER TABLE notification_preferences ADD COLUMN dnd_start TEXT NOT NULL DEFAULT '22:00';
ALTER TABLE notification_preferences ADD COLUMN dnd_end TEXT NOT NULL DEFAULT '07:00';
@@ -1,3 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN background_color TEXT NOT NULL DEFAULT '#121513';
ALTER TABLE display_preferences ADD COLUMN panel_color TEXT NOT NULL DEFAULT '#181c19';
ALTER TABLE display_preferences ADD COLUMN text_color TEXT NOT NULL DEFAULT '#edf0ec';
@@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS mail_preferences (
id INTEGER PRIMARY KEY CHECK (id = 1),
messages_per_page INTEGER NOT NULL DEFAULT 50,
mark_read_delay_ms INTEGER NOT NULL DEFAULT 0,
remote_images TEXT NOT NULL DEFAULT 'ask',
preferred_body TEXT NOT NULL DEFAULT 'auto',
thread_mode TEXT NOT NULL DEFAULT 'threaded',
collapse_quotes INTEGER NOT NULL DEFAULT 1,
show_snippets INTEGER NOT NULL DEFAULT 1,
default_sort TEXT NOT NULL DEFAULT 'date_desc',
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT OR IGNORE INTO mail_preferences (id) VALUES (1);
@@ -1,13 +0,0 @@
ALTER TABLE contacts ADD COLUMN phone TEXT NOT NULL DEFAULT '';
ALTER TABLE contacts ADD COLUMN job_title TEXT NOT NULL DEFAULT '';
ALTER TABLE contacts ADD COLUMN address TEXT NOT NULL DEFAULT '';
ALTER TABLE contacts ADD COLUMN website TEXT NOT NULL DEFAULT '';
ALTER TABLE contacts ADD COLUMN birthday TEXT NOT NULL DEFAULT '';
CREATE TABLE IF NOT EXISTS contact_photos (
contact_id TEXT PRIMARY KEY,
content_type TEXT NOT NULL,
data BLOB NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE
);
@@ -1,3 +0,0 @@
ALTER TABLE contacts ADD COLUMN emails_json TEXT NOT NULL DEFAULT '[]';
ALTER TABLE contacts ADD COLUMN phones_json TEXT NOT NULL DEFAULT '[]';
ALTER TABLE contacts ADD COLUMN addresses_json TEXT NOT NULL DEFAULT '[]';
@@ -1,2 +0,0 @@
ALTER TABLE drafts ADD COLUMN request_mdn INTEGER NOT NULL DEFAULT 0;
ALTER TABLE drafts ADD COLUMN priority TEXT NOT NULL DEFAULT 'normal';
@@ -1,7 +0,0 @@
ALTER TABLE mail_preferences ADD COLUMN flagged_on_top INTEGER NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS folder_preferences (
folder_name TEXT PRIMARY KEY REFERENCES folders(name) ON UPDATE CASCADE ON DELETE CASCADE,
flagged_on_top INTEGER CHECK(flagged_on_top IN (0, 1) OR flagged_on_top IS NULL),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -1,4 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN compose_autosave_ms INTEGER NOT NULL DEFAULT 3000;
ALTER TABLE display_preferences ADD COLUMN default_priority TEXT NOT NULL DEFAULT 'normal';
ALTER TABLE display_preferences ADD COLUMN request_mdn_default INTEGER NOT NULL DEFAULT 0;
ALTER TABLE display_preferences ADD COLUMN attachment_reminder INTEGER NOT NULL DEFAULT 1;
@@ -1,15 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN icon_size INTEGER NOT NULL DEFAULT 18;
ALTER TABLE display_preferences ADD COLUMN toolbar_density TEXT NOT NULL DEFAULT 'normal';
ALTER TABLE display_preferences ADD COLUMN sidebar_item_height INTEGER NOT NULL DEFAULT 29;
ALTER TABLE display_preferences ADD COLUMN reader_max_width INTEGER NOT NULL DEFAULT 0;
ALTER TABLE display_preferences ADD COLUMN composer_width INTEGER NOT NULL DEFAULT 900;
ALTER TABLE display_preferences ADD COLUMN selection_color TEXT NOT NULL DEFAULT '#28352e';
ALTER TABLE display_preferences ADD COLUMN unread_font_weight INTEGER NOT NULL DEFAULT 730;
ALTER TABLE display_preferences ADD COLUMN unread_badge_style TEXT NOT NULL DEFAULT 'dot';
ALTER TABLE display_preferences ADD COLUMN compact_headers INTEGER NOT NULL DEFAULT 0;
ALTER TABLE display_preferences ADD COLUMN show_borders INTEGER NOT NULL DEFAULT 1;
ALTER TABLE display_preferences ADD COLUMN scrollbar_density TEXT NOT NULL DEFAULT 'thin';
ALTER TABLE display_preferences ADD COLUMN monochrome INTEGER NOT NULL DEFAULT 0;
ALTER TABLE display_preferences ADD COLUMN folder_background TEXT NOT NULL DEFAULT '';
ALTER TABLE display_preferences ADD COLUMN message_background TEXT NOT NULL DEFAULT '';
ALTER TABLE display_preferences ADD COLUMN reader_background TEXT NOT NULL DEFAULT '';
@@ -1 +0,0 @@
ALTER TABLE drafts ADD COLUMN compose_format TEXT NOT NULL DEFAULT 'html';
@@ -1,4 +0,0 @@
ALTER TABLE mail_preferences ADD COLUMN sync_imap_keywords INTEGER NOT NULL DEFAULT 0;
ALTER TABLE tags ADD COLUMN imap_keyword TEXT NOT NULL DEFAULT '';
UPDATE tags SET imap_keyword = 'RustBox-' || replace(id, '-', '') WHERE imap_keyword = '';
CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_imap_keyword ON tags(imap_keyword) WHERE imap_keyword <> '';
@@ -1,5 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN compose_font_family TEXT NOT NULL DEFAULT 'system';
ALTER TABLE display_preferences ADD COLUMN reply_position TEXT NOT NULL DEFAULT 'above';
ALTER TABLE display_preferences ADD COLUMN quote_style TEXT NOT NULL DEFAULT 'standard';
ALTER TABLE display_preferences ADD COLUMN forward_mode TEXT NOT NULL DEFAULT 'inline';
ALTER TABLE display_preferences ADD COLUMN spellcheck_enabled INTEGER NOT NULL DEFAULT 1;
@@ -1,3 +0,0 @@
ALTER TABLE drafts ADD COLUMN remote_folder TEXT;
ALTER TABLE drafts ADD COLUMN remote_uid INTEGER;
ALTER TABLE drafts ADD COLUMN remote_uid_validity INTEGER;
@@ -1,24 +0,0 @@
-- OpenPGP keyring metadata. Secret key material is stored outside SQLite.
CREATE TABLE IF NOT EXISTS pgp_keys (
fingerprint TEXT PRIMARY KEY,
key_id TEXT NOT NULL,
email TEXT NOT NULL DEFAULT '',
user_id TEXT NOT NULL DEFAULT '',
public_key_armor TEXT NOT NULL,
secret_key_path TEXT,
has_secret INTEGER NOT NULL DEFAULT 0,
trust TEXT NOT NULL DEFAULT 'unknown',
source TEXT NOT NULL DEFAULT 'import',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_pgp_keys_email ON pgp_keys(lower(email));
CREATE TABLE IF NOT EXISTS pgp_preferences (
id INTEGER PRIMARY KEY CHECK (id = 1),
wkd_enabled INTEGER NOT NULL DEFAULT 1,
hkp_enabled INTEGER NOT NULL DEFAULT 0,
require_trusted_encryption INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT OR IGNORE INTO pgp_preferences (id) VALUES (1);
@@ -1,4 +0,0 @@
ALTER TABLE identities ADD COLUMN pgp_fingerprint TEXT;
ALTER TABLE identities ADD COLUMN pgp_sign_default INTEGER NOT NULL DEFAULT 0;
ALTER TABLE identities ADD COLUMN pgp_encrypt_default INTEGER NOT NULL DEFAULT 0;
ALTER TABLE identities ADD COLUMN pgp_inline_default INTEGER NOT NULL DEFAULT 0;
@@ -1,2 +0,0 @@
ALTER TABLE drafts ADD COLUMN pgp_mode TEXT NOT NULL DEFAULT 'none';
ALTER TABLE drafts ADD COLUMN pgp_inline INTEGER NOT NULL DEFAULT 0;
@@ -1 +0,0 @@
ALTER TABLE drafts ADD COLUMN pgp_attach_public_key INTEGER NOT NULL DEFAULT 0;
@@ -1 +0,0 @@
ALTER TABLE drafts ADD COLUMN pgp_attach_recipient_keys INTEGER NOT NULL DEFAULT 0;
@@ -1,4 +0,0 @@
CREATE TABLE IF NOT EXISTS trusted_senders (
email TEXT PRIMARY KEY COLLATE NOCASE,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -1,7 +0,0 @@
CREATE TABLE IF NOT EXISTS response_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL COLLATE NOCASE UNIQUE,
body TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -1 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN interface_skin TEXT NOT NULL DEFAULT 'rustbox';
@@ -1 +0,0 @@
ALTER TABLE response_templates ADD COLUMN format TEXT NOT NULL DEFAULT 'text';
@@ -1 +0,0 @@
ALTER TABLE display_preferences ADD COLUMN preset_scale_unlocked INTEGER NOT NULL DEFAULT 0;
-23
View File
@@ -1,23 +0,0 @@
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY NOT NULL,
account_id TEXT NOT NULL UNIQUE,
browser_id TEXT NOT NULL,
email TEXT NOT NULL,
password_ciphertext TEXT NOT NULL,
user_hash TEXT NOT NULL,
imap_host TEXT NOT NULL,
imap_port INTEGER NOT NULL,
imap_tls INTEGER NOT NULL,
imap_tls_verify INTEGER NOT NULL,
smtp_host TEXT NOT NULL,
smtp_port INTEGER NOT NULL,
smtp_tls INTEGER NOT NULL,
smtp_tls_verify INTEGER NOT NULL,
timeout_secs INTEGER NOT NULL,
created_at_unix INTEGER NOT NULL,
last_accessed_unix INTEGER NOT NULL,
user_agent TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_sessions_browser ON sessions(browser_id);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_hash);
CREATE INDEX IF NOT EXISTS idx_sessions_expiry ON sessions(last_accessed_unix, timeout_secs);
+344
View File
@@ -0,0 +1,344 @@
-- RustBox 0.1.1 SQLite schema.
-- One physical database contains global sessions plus isolated per-account tables.
-- Application tables are materialized from the tenant section by replacing {{p}}.
-- Baseline application schema version: 46. Future migrations start at V047.
-- RUSTBOX:GLOBAL-BEGIN
CREATE TABLE IF NOT EXISTS rustbox_accounts (
user_hash TEXT PRIMARY KEY NOT NULL,
table_prefix TEXT NOT NULL UNIQUE,
schema_version INTEGER NOT NULL DEFAULT 46,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY NOT NULL,
account_id TEXT NOT NULL UNIQUE,
browser_id TEXT NOT NULL,
email TEXT NOT NULL,
password_ciphertext TEXT NOT NULL,
user_hash TEXT NOT NULL,
imap_host TEXT NOT NULL,
imap_port INTEGER NOT NULL,
imap_tls INTEGER NOT NULL,
imap_tls_verify INTEGER NOT NULL,
smtp_host TEXT NOT NULL,
smtp_port INTEGER NOT NULL,
smtp_tls INTEGER NOT NULL,
smtp_tls_verify INTEGER NOT NULL,
timeout_secs INTEGER NOT NULL,
created_at_unix INTEGER NOT NULL,
last_accessed_unix INTEGER NOT NULL,
user_agent TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_sessions_browser ON sessions(browser_id);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_hash);
CREATE INDEX IF NOT EXISTS idx_sessions_expiry ON sessions(last_accessed_unix, timeout_secs);
-- RUSTBOX:GLOBAL-END
-- RUSTBOX:TENANT-BEGIN
-- {{p}} is replaced at runtime with a collision-checked per-account table prefix.
CREATE TABLE {{p}}calendar_events (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
location TEXT NOT NULL DEFAULT '',
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
all_day INTEGER NOT NULL DEFAULT 0,
recurrence_rule TEXT,
attendees TEXT NOT NULL DEFAULT '[]',
organizer TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'confirmed',
source TEXT NOT NULL DEFAULT 'manual',
source_uid TEXT,
source_message_uid INTEGER,
source_message_folder TEXT,
meeting_url TEXT,
color TEXT,
reminder_minutes INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE {{p}}calendar_settings (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
default_view TEXT NOT NULL DEFAULT 'month',
week_starts_on INTEGER NOT NULL DEFAULT 0,
working_hours_start TEXT NOT NULL DEFAULT '09:00',
working_hours_end TEXT NOT NULL DEFAULT '17:00',
time_format TEXT NOT NULL DEFAULT '12h',
zoom_link TEXT NOT NULL DEFAULT '',
teams_link TEXT NOT NULL DEFAULT '',
meet_link TEXT NOT NULL DEFAULT '',
caldav_url TEXT NOT NULL DEFAULT '',
caldav_username TEXT NOT NULL DEFAULT '',
caldav_password TEXT NOT NULL DEFAULT '',
caldav_enabled INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE {{p}}contact_group_members (
group_id TEXT NOT NULL,
contact_id TEXT NOT NULL,
PRIMARY KEY (group_id, contact_id),
FOREIGN KEY (group_id) REFERENCES {{p}}contact_groups(id) ON DELETE CASCADE,
FOREIGN KEY (contact_id) REFERENCES {{p}}contacts(id) ON DELETE CASCADE
);
CREATE TABLE {{p}}contact_groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE {{p}}contact_photos (
contact_id TEXT PRIMARY KEY,
content_type TEXT NOT NULL,
data BLOB NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (contact_id) REFERENCES {{p}}contacts(id) ON DELETE CASCADE
);
CREATE TABLE {{p}}contacts (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
company TEXT NOT NULL DEFAULT '',
notes TEXT NOT NULL DEFAULT '',
is_favorite INTEGER NOT NULL DEFAULT 0,
last_contacted TEXT,
contact_count INTEGER NOT NULL DEFAULT 0,
source TEXT NOT NULL DEFAULT 'manual',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
phone TEXT NOT NULL DEFAULT '', job_title TEXT NOT NULL DEFAULT '', address TEXT NOT NULL DEFAULT '', website TEXT NOT NULL DEFAULT '', birthday TEXT NOT NULL DEFAULT '', emails_json TEXT NOT NULL DEFAULT '[]', phones_json TEXT NOT NULL DEFAULT '[]', addresses_json TEXT NOT NULL DEFAULT '[]');
CREATE TABLE {{p}}display_preferences (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
density TEXT NOT NULL DEFAULT 'comfortable',
theme TEXT NOT NULL DEFAULT 'system',
language TEXT NOT NULL DEFAULT 'en',
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
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);
CREATE TABLE {{p}}draft_attachments (
id TEXT PRIMARY KEY,
draft_id TEXT NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
size INTEGER NOT NULL,
file_path TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (draft_id) REFERENCES {{p}}drafts(id) ON DELETE CASCADE
);
CREATE TABLE {{p}}drafts (
id TEXT PRIMARY KEY,
to_addresses TEXT NOT NULL DEFAULT '',
cc_addresses TEXT NOT NULL DEFAULT '',
bcc_addresses TEXT NOT NULL DEFAULT '',
subject TEXT NOT NULL DEFAULT '',
text_body TEXT NOT NULL DEFAULT '',
html_body TEXT,
in_reply_to TEXT,
references_header TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
request_mdn INTEGER NOT NULL DEFAULT 0, priority TEXT NOT NULL DEFAULT 'normal', compose_format TEXT NOT NULL DEFAULT 'html', remote_folder TEXT, remote_uid INTEGER, remote_uid_validity INTEGER, pgp_mode TEXT NOT NULL DEFAULT 'none', pgp_inline INTEGER NOT NULL DEFAULT 0, pgp_attach_public_key INTEGER NOT NULL DEFAULT 0, pgp_attach_recipient_keys INTEGER NOT NULL DEFAULT 0);
CREATE TABLE {{p}}folder_preferences (
folder_name TEXT PRIMARY KEY REFERENCES {{p}}folders(name) ON UPDATE CASCADE ON DELETE CASCADE,
flagged_on_top INTEGER CHECK(flagged_on_top IN (0, 1) OR flagged_on_top IS NULL),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE {{p}}folders (
name TEXT PRIMARY KEY,
delimiter TEXT,
parent TEXT,
flags TEXT NOT NULL DEFAULT '',
is_subscribed INTEGER NOT NULL DEFAULT 1,
total_count INTEGER NOT NULL DEFAULT 0,
unread_count INTEGER NOT NULL DEFAULT 0,
uid_validity INTEGER NOT NULL DEFAULT 0,
highest_modseq INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
messages_updated_at TEXT);
CREATE TABLE {{p}}identities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
display_name TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL,
signature_html TEXT NOT NULL DEFAULT '',
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
reply_to TEXT NOT NULL DEFAULT '', bcc TEXT NOT NULL DEFAULT '', organization TEXT NOT NULL DEFAULT '', signature_text TEXT NOT NULL DEFAULT '', signature_new INTEGER NOT NULL DEFAULT 1, signature_reply INTEGER NOT NULL DEFAULT 1, signature_forward INTEGER NOT NULL DEFAULT 1, signature_position TEXT NOT NULL DEFAULT 'above', pgp_fingerprint TEXT, pgp_sign_default INTEGER NOT NULL DEFAULT 0, pgp_encrypt_default INTEGER NOT NULL DEFAULT 0, pgp_inline_default INTEGER NOT NULL DEFAULT 0);
CREATE TABLE {{p}}known_addresses (
email TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
PRIMARY KEY (email)
);
CREATE TABLE {{p}}mail_preferences (
id INTEGER PRIMARY KEY CHECK (id = 1),
messages_per_page INTEGER NOT NULL DEFAULT 50,
mark_read_delay_ms INTEGER NOT NULL DEFAULT 0,
remote_images TEXT NOT NULL DEFAULT 'ask',
preferred_body TEXT NOT NULL DEFAULT 'auto',
thread_mode TEXT NOT NULL DEFAULT 'threaded',
collapse_quotes INTEGER NOT NULL DEFAULT 1,
show_snippets INTEGER NOT NULL DEFAULT 1,
default_sort TEXT NOT NULL DEFAULT 'date_desc',
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
flagged_on_top INTEGER NOT NULL DEFAULT 0, sync_imap_keywords INTEGER NOT NULL DEFAULT 0);
CREATE TABLE {{p}}meeting_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url_template TEXT NOT NULL,
icon TEXT NOT NULL DEFAULT '',
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE {{p}}message_tags (
tag_id TEXT NOT NULL,
message_uid INTEGER NOT NULL,
message_folder TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (tag_id, message_uid, message_folder),
FOREIGN KEY (tag_id) REFERENCES {{p}}tags(id) ON DELETE CASCADE,
FOREIGN KEY (message_uid, message_folder) REFERENCES {{p}}messages(uid, folder) ON DELETE CASCADE
);
CREATE TABLE {{p}}messages (
uid INTEGER NOT NULL,
folder TEXT NOT NULL,
message_id TEXT,
in_reply_to TEXT,
subject TEXT NOT NULL DEFAULT '',
from_address TEXT NOT NULL DEFAULT '',
from_name TEXT NOT NULL DEFAULT '',
to_addresses TEXT NOT NULL DEFAULT '',
cc_addresses TEXT NOT NULL DEFAULT '',
date TEXT NOT NULL,
flags TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
has_attachments INTEGER NOT NULL DEFAULT 0,
snippet TEXT NOT NULL DEFAULT '',
body_cached INTEGER NOT NULL DEFAULT 0,
body_html TEXT,
body_text TEXT,
references_header TEXT, date_epoch INTEGER NOT NULL DEFAULT 0, attachments_json TEXT, raw_headers TEXT, reaction TEXT, email_theme INTEGER,
PRIMARY KEY (uid, folder),
FOREIGN KEY (folder) REFERENCES {{p}}folders(name) ON DELETE CASCADE
);
CREATE TABLE {{p}}notification_preferences (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
enabled INTEGER NOT NULL DEFAULT 1,
sound INTEGER NOT NULL DEFAULT 0,
folders TEXT NOT NULL DEFAULT '["INBOX"]',
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
dnd_enabled INTEGER NOT NULL DEFAULT 0, dnd_start TEXT NOT NULL DEFAULT '22:00', dnd_end TEXT NOT NULL DEFAULT '07:00');
CREATE TABLE {{p}}pgp_keys (
fingerprint TEXT PRIMARY KEY,
key_id TEXT NOT NULL,
email TEXT NOT NULL DEFAULT '',
user_id TEXT NOT NULL DEFAULT '',
public_key_armor TEXT NOT NULL,
secret_key_path TEXT,
has_secret INTEGER NOT NULL DEFAULT 0,
trust TEXT NOT NULL DEFAULT 'unknown',
source TEXT NOT NULL DEFAULT 'import',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE {{p}}pgp_preferences (
id INTEGER PRIMARY KEY CHECK (id = 1),
wkd_enabled INTEGER NOT NULL DEFAULT 1,
hkp_enabled INTEGER NOT NULL DEFAULT 0,
require_trusted_encryption INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE {{p}}response_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL COLLATE NOCASE UNIQUE,
body TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
format TEXT NOT NULL DEFAULT 'text');
CREATE TABLE {{p}}saved_searches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
query_string TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE {{p}}special_folders (
role TEXT PRIMARY KEY CHECK(role IN ('sent','drafts','trash','junk','archive')),
folder_name TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE {{p}}tags (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
color TEXT NOT NULL DEFAULT '#6b7280',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
imap_keyword TEXT NOT NULL DEFAULT '');
CREATE TABLE {{p}}trusted_senders (
email TEXT PRIMARY KEY COLLATE NOCASE,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE {{p}}user_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE INDEX {{p}}idx_calendar_events_source_uid ON {{p}}calendar_events(source_uid);
CREATE INDEX {{p}}idx_calendar_events_time ON {{p}}calendar_events(start_time, end_time);
CREATE INDEX {{p}}idx_cgm_contact_id ON {{p}}contact_group_members(contact_id);
CREATE INDEX {{p}}idx_contacts_contact_count ON {{p}}contacts(contact_count DESC);
CREATE UNIQUE INDEX {{p}}idx_contacts_email ON {{p}}contacts(email);
CREATE INDEX {{p}}idx_contacts_last_contacted ON {{p}}contacts(last_contacted DESC);
CREATE INDEX {{p}}idx_contacts_name ON {{p}}contacts(name);
CREATE INDEX {{p}}idx_known_addresses_name ON {{p}}known_addresses(name);
CREATE INDEX {{p}}idx_message_tags_message ON {{p}}message_tags(message_uid, message_folder);
CREATE INDEX {{p}}idx_message_tags_tag ON {{p}}message_tags(tag_id);
CREATE INDEX {{p}}idx_messages_folder_date ON {{p}}messages(folder, date_epoch DESC);
CREATE INDEX {{p}}idx_messages_in_reply_to ON {{p}}messages(in_reply_to);
CREATE INDEX {{p}}idx_messages_message_id ON {{p}}messages(message_id);
CREATE INDEX {{p}}idx_pgp_keys_email ON {{p}}pgp_keys(lower(email));
CREATE UNIQUE INDEX {{p}}idx_saved_searches_name ON {{p}}saved_searches(name COLLATE NOCASE);
CREATE UNIQUE INDEX {{p}}idx_tags_imap_keyword ON {{p}}tags(imap_keyword) WHERE imap_keyword <> '';
-- RUSTBOX:TENANT-END
+16 -13
View File
@@ -1,4 +1,4 @@
# RustBox 0.1.0 - packaged production configuration (DEB/RPM)
# RustBox 0.1.1 - packaged production configuration (DEB/RPM)
#
# The active values below intentionally keep the existing package profile.
# Alternative storage/cluster examples are provided at the end of this file
@@ -13,9 +13,9 @@ environment = "production"
data_dir = "/var/lib/rustbox"
log_dir = "/var/log/rustbox"
# Sessions and "remember me".
# With no database/session backend keys configured, RustBox defaults to
# persistent SQLite sessions under data_dir and survives process restarts.
# Database, sessions and "remember me".
# With no database/session backend keys configured, the whole application uses
# SQLite at data_dir/rustbox.sqlite; sessions are stored in that database.
session_timeout_hours = 72
remember_me_enabled = true
remember_me_days = 30
@@ -74,21 +74,21 @@ pgp_allow_private_export = true
# Copy/uncomment only the keys required by the selected profile.
#
# session_encryption_key must represent the same 32-byte key on every node that
# shares a SQL/Redis session store. Keep it secret and outside source control.
# shares the configured SQL database or Redis session backend. Keep it secret and outside source control.
# -----------------------------------------------------------------------------
# Profile A: standalone SQLite sessions (default package behavior)
# Profile A: standalone SQLite application database (default package behavior)
# -----------------------------------------------------------------------------
# No extra keys are required. The equivalent explicit configuration is:
# database_backend = "sqlite"
# session_backend = "database"
# cluster_mode = false
#
# RustBox stores sessions in data_dir/rustbox.sqlite and creates/uses the local
# session encryption key for this single-node SQLite setup.
# RustBox stores account data and sessions in data_dir/rustbox.sqlite and
# creates/uses the local session encryption key for this single-node setup.
# -----------------------------------------------------------------------------
# Profile B: PostgreSQL-backed persistent sessions
# Profile B: PostgreSQL application database (data + database sessions)
# -----------------------------------------------------------------------------
# database_backend = "pgsql"
# database_url = "postgres://rustbox:change-me@127.0.0.1/rustbox"
@@ -97,7 +97,7 @@ pgp_allow_private_export = true
# session_encryption_key = "BASE64_32_BYTE_KEY"
# -----------------------------------------------------------------------------
# Profile C: MySQL-backed persistent sessions
# Profile C: MySQL application database (data + database sessions; MySQL 8.0.13+)
# -----------------------------------------------------------------------------
# database_backend = "mysql"
# database_url = "mysql://rustbox:change-me@127.0.0.1/rustbox"
@@ -106,7 +106,7 @@ pgp_allow_private_export = true
# session_encryption_key = "BASE64_32_BYTE_KEY"
# -----------------------------------------------------------------------------
# Profile D: Redis sessions, single application node
# Profile D: Redis sessions, single application node (account data stays in database_backend)
# -----------------------------------------------------------------------------
# session_backend = "redis"
# redis_url = "redis://127.0.0.1:6379/"
@@ -119,6 +119,9 @@ pgp_allow_private_export = true
# -----------------------------------------------------------------------------
# Profile E: Redis Sentinel sessions for multiple RustBox nodes
# -----------------------------------------------------------------------------
# Cluster nodes must also share the main application database.
# database_backend = "pgsql"
# database_url = "postgres://rustbox:change-me@db.internal/rustbox"
# session_backend = "redis"
# cluster_mode = true
# redis_sentinel_nodes = ["sentinel-a:26379", "sentinel-b:26379", "sentinel-c:26379"]
@@ -128,7 +131,7 @@ pgp_allow_private_export = true
# session_encryption_key = "BASE64_32_BYTE_KEY"
# -----------------------------------------------------------------------------
# Profile F: PostgreSQL sessions shared by a RustBox application cluster
# Profile F: PostgreSQL application database shared by a RustBox cluster
# -----------------------------------------------------------------------------
# database_backend = "pgsql"
# database_url = "postgres://rustbox:change-me@db.internal/rustbox"
@@ -137,7 +140,7 @@ pgp_allow_private_export = true
# session_encryption_key = "BASE64_32_BYTE_KEY"
# -----------------------------------------------------------------------------
# Profile G: MySQL sessions shared by a RustBox application cluster
# Profile G: MySQL application database shared by a RustBox cluster
# -----------------------------------------------------------------------------
# database_backend = "mysql"
# database_url = "mysql://rustbox:change-me@db.internal/rustbox"
-9
View File
@@ -1,9 +0,0 @@
RustBox: http://127.0.0.1:3001
Config: /home/mg/RustBox/config.dev.toml
Data: /home/mg/RustBox/.dev/data
RustBox configuration OK
{"timestamp":"2026-10-30T15:14:14.084058Z","level":"DEBUG","fields":{"message":"Purged expired sessions"},"target":"rustbox"}
{"timestamp":"2026-10-30T15:14:14.085996Z","level":"INFO","fields":{"message":"RustBox server starting","host":"127.0.0.1","port":"3001"},"target":"rustbox"}
{"timestamp":"2026-10-30T15:14:15.105150Z","level":"INFO","fields":{"message":"Shutdown signal received; stopping IDLE tasks"},"target":"rustbox"}
{"timestamp":"2026-10-30T15:14:15.105664Z","level":"INFO","fields":{"message":"RustBox server stopped"},"target":"rustbox"}
+12 -1
View File
@@ -6,13 +6,15 @@ cd "$ROOT"
UI_REVIEWED=0
ROOT_REVIEWED=0
OWNED_REVIEWED=0
SKIP_BUILD=0
for arg in "$@"; do
case "$arg" in
--ui-reviewed) UI_REVIEWED=1 ;;
--root-reviewed) ROOT_REVIEWED=1 ;;
--rustbox-owned-reviewed) OWNED_REVIEWED=1 ;;
--skip-build) SKIP_BUILD=1 ;;
*) echo "usage: $0 [--ui-reviewed] [--root-reviewed] [--skip-build]" >&2; exit 2 ;;
*) echo "usage: $0 [--ui-reviewed] [--root-reviewed] [--rustbox-owned-reviewed] [--skip-build]" >&2; exit 2 ;;
esac
done
@@ -49,6 +51,15 @@ if find . -path ./.git -prune -o -type f \( -name '*.ts' -o -name '*.tsx' \) -pr
exit 1
fi
if ! git diff --quiet "$BASE..$TARGET_SHA" -- backend/migrations/ backend/tests/; then
if [ "$OWNED_REVIEWED" -ne 1 ]; then
echo "error: Oxi changed backend migrations/tests that RustBox owns independently." >&2
echo "Review/port only relevant behavior, then rerun with --rustbox-owned-reviewed." >&2
git diff --name-status "$BASE..$TARGET_SHA" -- backend/migrations/ backend/tests/ >&2 || true
exit 1
fi
fi
if ! git diff --quiet "$BASE..$TARGET_SHA" -- frontend/; then
if [ "$UI_REVIEWED" -ne 1 ]; then
echo "error: Oxi frontend changed between $BASE and $TARGET_SHA." >&2
+5 -1
View File
@@ -34,7 +34,10 @@ tmp_mapped="$(mktemp)"
cleanup() { rm -f "$tmp_raw" "$tmp_mapped"; }
trap cleanup EXIT
git diff --binary --find-renames "$PARENT..$SHA" -- backend/ > "$tmp_raw"
git diff --binary --find-renames "$PARENT..$SHA" -- \
backend/ \
':(exclude)backend/migrations/**' \
':(exclude)backend/tests/**' > "$tmp_raw"
sed \
-e 's#^diff --git a/backend/#diff --git a/#' \
-e 's# b/backend/# b/#' \
@@ -56,4 +59,5 @@ fi
printf 'Ported backend portion of Oxi commit %s onto branch %s.\n' "$SHA" "$branch"
printf 'UPSTREAM_BASE was intentionally NOT changed.\n'
printf 'Review excluded upstream DB/tests separately: git show %s -- backend/migrations/ backend/tests/\n' "$SHA"
printf 'Review the original commit, especially frontend changes: git show %s -- frontend/\n' "$SHA"
+5 -2
View File
@@ -29,8 +29,11 @@ printf ' consumed: %s\n' "$BASE"
printf ' target: %s\n' "$TARGET_SHA"
printf ' commits: %s\n' "$(git rev-list --count "$BASE..$TARGET_SHA")"
printf '\nOxi backend changes that can be mapped into RustBox root:\n'
git diff --name-status "$BASE..$TARGET_SHA" -- backend/ || true
printf '\nOxi backend changes that can be mapped automatically into RustBox root:\n'
git diff --name-status "$BASE..$TARGET_SHA" -- backend/ ':(exclude)backend/migrations/**' ':(exclude)backend/tests/**' || true
printf '\nOxi backend DB migrations/tests requiring manual review (never auto-imported):\n'
git diff --name-status "$BASE..$TARGET_SHA" -- backend/migrations/ backend/tests/ || true
printf '\nOxi frontend changes requiring manual RustBox UI port:\n'
git diff --name-status "$BASE..$TARGET_SHA" -- frontend/ || true
+1 -1
View File
@@ -12,7 +12,7 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Initialize it first:" >&2
echo " git init" >&2
echo " git add ." >&2
echo " git commit -m 'RustBox 0.0.1 baseline'" >&2
echo " git commit -m 'RustBox 0.1.1 baseline'" >&2
exit 1
fi
+6 -1
View File
@@ -45,7 +45,10 @@ tmp_mapped="$(mktemp)"
cleanup() { rm -f "$tmp_raw" "$tmp_mapped"; }
trap cleanup EXIT
git diff --binary --find-renames "$BASE..$TARGET_SHA" -- backend/ > "$tmp_raw"
git diff --binary --find-renames "$BASE..$TARGET_SHA" -- \
backend/ \
':(exclude)backend/migrations/**' \
':(exclude)backend/tests/**' > "$tmp_raw"
sed \
-e 's#^diff --git a/backend/#diff --git a/#' \
-e 's# b/backend/# b/#' \
@@ -79,6 +82,8 @@ fi
printf '\nMapped Oxi backend %s -> %s into RustBox on branch %s.\n' "$BASE" "$TARGET_SHA" "$branch"
printf 'UPSTREAM_BASE remains at %s until the review is explicitly finished.\n' "$BASE"
printf '\nRustBox-owned upstream backend areas were NOT imported. Review them with:\n'
printf " git diff --name-status %s..%s -- backend/migrations/ backend/tests/\n" "$BASE" "$TARGET_SHA"
printf '\nFrontend changes were NOT imported. Review them with:\n'
printf ' git diff --name-status %s..%s -- frontend/\n' "$BASE" "$TARGET_SHA"
printf ' git diff %s..%s -- frontend/\n' "$BASE" "$TARGET_SHA"
+9 -15
View File
@@ -26,9 +26,8 @@ pub fn provision_user_data(data_dir: &str, user_hash: &str) -> Result<PathBuf, S
}
}
// User data is always SQLite and must only receive the user-schema
// migrations from db::pool. Session migrations are handled separately by
// session_store for the configured session database backend.
// Ensure this account has its isolated tables in the configured RustBox database.
// The same database backend stores sessions unless session_backend=redis.
let conn = crate::db::pool::open_user_db(data_dir, user_hash)
.map_err(|e| format!("failed to initialize user database: {e}"))?;
drop(conn);
@@ -75,8 +74,8 @@ mod tests {
assert!(user_dir.exists(), "user directory should exist");
assert!(
user_dir.join("db.sqlite").exists(),
"db.sqlite should exist"
tmp.path().join("rustbox.sqlite").exists(),
"global rustbox.sqlite should exist for the default SQLite backend"
);
assert!(
user_dir.join("tantivy").is_dir(),
@@ -94,7 +93,7 @@ mod tests {
let dir2 = provision_user_data(data_dir, &user_hash).expect("second call failed");
assert_eq!(dir1, dir2);
assert!(dir2.join("db.sqlite").exists());
assert!(tmp.path().join("rustbox.sqlite").exists());
assert!(dir2.join("tantivy").is_dir());
}
@@ -104,17 +103,12 @@ mod tests {
let data_dir = tmp.path().to_str().unwrap();
let user_hash = hash_email("meta@example.com");
let user_dir = provision_user_data(data_dir, &user_hash).expect("provisioning failed");
let conn =
rusqlite::Connection::open(user_dir.join("db.sqlite")).expect("failed to open db");
provision_user_data(data_dir, &user_hash).expect("provisioning failed");
let conn = crate::db::pool::open_user_db(data_dir, &user_hash)
.expect("failed to open account database");
let value: String = conn
.query_row(
crate::queries::sqlite::queries::auth_user_data::Q_001,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(auth_user_data::Q_001), [], |row| row.get(0))
.expect("failed to query user_meta");
assert_eq!(value, "1");
+19 -5
View File
@@ -492,11 +492,11 @@ impl AppConfig {
return Err("Redis sessions require REDIS_URL or REDIS_SENTINEL_NODES + REDIS_SENTINEL_MASTER".to_string());
}
}
if self.cluster_mode
&& self.session_backend == "database"
&& self.database_backend == "sqlite"
{
return Err("CLUSTER_MODE with database sessions requires mysql/pgsql, or configure SESSION_BACKEND=redis".to_string());
if self.cluster_mode && self.database_backend == "sqlite" {
return Err(
"CLUSTER_MODE requires a shared mysql/pgsql application database; Redis only replaces session persistence"
.to_string(),
);
}
if (self.cluster_mode
|| self.session_backend == "redis"
@@ -809,4 +809,18 @@ mod tests {
vec![TrustedProxy::Ip("127.0.0.1".parse().unwrap())]
);
}
#[test]
fn cluster_mode_rejects_local_sqlite_even_with_redis_sessions() {
let config = AppConfig {
database_backend: "sqlite".to_string(),
session_backend: "redis".to_string(),
redis_url: Some("redis://127.0.0.1:6379/".to_string()),
session_encryption_key: Some("configured-shared-key".to_string()),
cluster_mode: true,
..AppConfig::default()
};
let error = config.validate().unwrap_err();
assert!(error.contains("shared mysql/pgsql application database"));
}
}
+54 -51
View File
@@ -1,5 +1,5 @@
use crate::db::pool::{Connection, DbValue, ToDbValue, params};
use crate::queries::sqlite::queries::db_calendar::{EVENT_SELECT_COLS, SETTINGS_SELECT_COLS};
use rusqlite::{Connection, params};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -126,7 +126,7 @@ pub struct MeetingTemplate {
pub created_at: String,
}
fn row_to_event(row: &rusqlite::Row<'_>) -> rusqlite::Result<CalendarEvent> {
fn row_to_event(row: &crate::db::pool::Row<'_>) -> Result<CalendarEvent, crate::db::pool::DbError> {
let all_day_int: i32 = row.get(6)?;
Ok(CalendarEvent {
id: row.get(0)?,
@@ -152,7 +152,9 @@ fn row_to_event(row: &rusqlite::Row<'_>) -> rusqlite::Result<CalendarEvent> {
})
}
fn row_to_settings(row: &rusqlite::Row<'_>) -> rusqlite::Result<CalendarSettings> {
fn row_to_settings(
row: &crate::db::pool::Row<'_>,
) -> Result<CalendarSettings, crate::db::pool::DbError> {
let caldav_enabled_int: i32 = row.get(10)?;
Ok(CalendarSettings {
default_view: row.get(0)?,
@@ -170,7 +172,9 @@ fn row_to_settings(row: &rusqlite::Row<'_>) -> rusqlite::Result<CalendarSettings
})
}
fn row_to_template(row: &rusqlite::Row<'_>) -> rusqlite::Result<MeetingTemplate> {
fn row_to_template(
row: &crate::db::pool::Row<'_>,
) -> Result<MeetingTemplate, crate::db::pool::DbError> {
let is_default_int: i32 = row.get(4)?;
Ok(MeetingTemplate {
id: row.get(0)?,
@@ -187,7 +191,8 @@ pub fn list_events(
start: &str,
end: &str,
) -> Result<Vec<CalendarEvent>, String> {
let sql = crate::rustbox_query_db_calendar_001!();
let sql =
crate::dbq!(db_calendar::FORMAT_001).replace("{EVENT_SELECT_COLS}", EVENT_SELECT_COLS);
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("Failed to prepare list_events: {e}"))?;
@@ -203,7 +208,8 @@ pub fn list_events(
}
pub fn list_all_events(conn: &Connection) -> Result<Vec<CalendarEvent>, String> {
let sql = crate::rustbox_query_db_calendar_002!();
let sql =
crate::dbq!(db_calendar::FORMAT_002).replace("{EVENT_SELECT_COLS}", EVENT_SELECT_COLS);
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("Failed to prepare list_all_events: {e}"))?;
@@ -219,12 +225,13 @@ pub fn list_all_events(conn: &Connection) -> Result<Vec<CalendarEvent>, String>
}
pub fn get_event(conn: &Connection, id: &str) -> Result<Option<CalendarEvent>, String> {
let sql = crate::rustbox_query_db_calendar_003!();
let sql =
crate::dbq!(db_calendar::FORMAT_003).replace("{EVENT_SELECT_COLS}", EVENT_SELECT_COLS);
let result = conn.query_row(&sql, params![id], row_to_event);
match result {
Ok(e) => Ok(Some(e)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get event: {e}")),
}
}
@@ -232,15 +239,11 @@ pub fn get_event(conn: &Connection, id: &str) -> Result<Option<CalendarEvent>, S
pub fn create_event(conn: &Connection, data: &CreateEvent) -> Result<CalendarEvent, String> {
let id = Uuid::new_v4().to_string();
let now: String = conn
.query_row(
crate::queries::sqlite::queries::db_calendar::Q_001,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(db_calendar::Q_001), [], |row| row.get(0))
.map_err(|e| format!("Failed to get current time: {e}"))?;
conn.execute(
crate::queries::sqlite::queries::db_calendar::Q_002,
crate::dbq!(db_calendar::Q_002),
params![
id,
data.title,
@@ -275,14 +278,14 @@ pub fn update_event(
data: &UpdateEvent,
) -> Result<Option<CalendarEvent>, String> {
let mut sets = Vec::new();
let mut values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut values: Vec<DbValue> = Vec::new();
let mut idx = 1u32;
macro_rules! maybe_set {
($field:ident, $col:expr) => {
if let Some(ref v) = data.$field {
sets.push(format!("{} = ?{idx}", $col));
values.push(Box::new(v.clone()));
values.push(v.to_db_value());
idx += 1;
}
};
@@ -302,13 +305,13 @@ pub fn update_event(
if let Some(v) = data.all_day {
sets.push(format!("all_day = ?{idx}"));
values.push(Box::new(v as i32));
values.push((v as i32).to_db_value());
idx += 1;
}
if let Some(v) = data.reminder_minutes {
sets.push(format!("reminder_minutes = ?{idx}"));
values.push(Box::new(v));
values.push(v.to_db_value());
idx += 1;
}
@@ -318,12 +321,15 @@ pub fn update_event(
sets.push("updated_at = datetime('now')".to_string());
let sql = crate::rustbox_query_db_calendar_004!(sets.join(", "), idx);
values.push(Box::new(id.to_string()));
let sql = format!(
"UPDATE calendar_events SET {} WHERE id = ?{}",
sets.join(", "),
idx
);
values.push(id.to_db_value());
let params_refs: Vec<&dyn rusqlite::types::ToSql> = values.iter().map(|b| b.as_ref()).collect();
let updated = conn
.execute(&sql, params_refs.as_slice())
.execute(&sql, values)
.map_err(|e| format!("Failed to update event: {e}"))?;
if updated == 0 {
@@ -335,10 +341,7 @@ pub fn update_event(
pub fn delete_event(conn: &Connection, id: &str) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_calendar::Q_003,
params![id],
)
.execute(crate::dbq!(db_calendar::Q_003), params![id])
.map_err(|e| format!("Failed to delete event: {e}"))?;
Ok(deleted > 0)
}
@@ -347,21 +350,23 @@ pub fn find_event_by_source_uid(
conn: &Connection,
source_uid: &str,
) -> Result<Option<CalendarEvent>, String> {
let sql = crate::rustbox_query_db_calendar_005!();
let sql =
crate::dbq!(db_calendar::FORMAT_005).replace("{EVENT_SELECT_COLS}", EVENT_SELECT_COLS);
let result = conn.query_row(&sql, params![source_uid], row_to_event);
match result {
Ok(e) => Ok(Some(e)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to find event by source_uid: {e}")),
}
}
pub fn get_calendar_settings(conn: &Connection) -> Result<CalendarSettings, String> {
conn.execute(crate::queries::sqlite::queries::db_calendar::Q_004, [])
conn.execute(crate::dbq!(db_calendar::Q_004), [])
.map_err(|e| format!("Failed to ensure calendar_settings row: {e}"))?;
let sql = crate::rustbox_query_db_calendar_006!();
let sql = crate::dbq!(db_calendar::FORMAT_006)
.replace("{SETTINGS_SELECT_COLS}", SETTINGS_SELECT_COLS);
conn.query_row(&sql, [], row_to_settings)
.map_err(|e| format!("Failed to get calendar settings: {e}"))
}
@@ -370,18 +375,18 @@ pub fn update_calendar_settings(
conn: &Connection,
data: &UpdateCalendarSettings,
) -> Result<CalendarSettings, String> {
conn.execute(crate::queries::sqlite::queries::db_calendar::Q_005, [])
conn.execute(crate::dbq!(db_calendar::Q_005), [])
.map_err(|e| format!("Failed to ensure calendar_settings row: {e}"))?;
let mut sets = Vec::new();
let mut values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut values: Vec<DbValue> = Vec::new();
let mut idx = 1u32;
macro_rules! maybe_set {
($field:ident, $col:expr) => {
if let Some(ref v) = data.$field {
sets.push(format!("{} = ?{idx}", $col));
values.push(Box::new(v.clone()));
values.push(v.to_db_value());
idx += 1;
}
};
@@ -400,13 +405,13 @@ pub fn update_calendar_settings(
if let Some(v) = data.week_starts_on {
sets.push(format!("week_starts_on = ?{idx}"));
values.push(Box::new(v));
values.push(v.to_db_value());
idx += 1;
}
if let Some(v) = data.caldav_enabled {
sets.push(format!("caldav_enabled = ?{idx}"));
values.push(Box::new(v as i32));
values.push((v as i32).to_db_value());
#[allow(unused_assignments)]
{
idx += 1;
@@ -415,10 +420,11 @@ pub fn update_calendar_settings(
if !sets.is_empty() {
sets.push("updated_at = datetime('now')".to_string());
let sql = crate::rustbox_query_db_calendar_007!(sets.join(", "));
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
values.iter().map(|b| b.as_ref()).collect();
conn.execute(&sql, params_refs.as_slice())
let sql = format!(
"UPDATE calendar_settings SET {} WHERE id = 1",
sets.join(", ")
);
conn.execute(&sql, values)
.map_err(|e| format!("Failed to update calendar settings: {e}"))?;
}
@@ -427,7 +433,7 @@ pub fn update_calendar_settings(
pub fn list_meeting_templates(conn: &Connection) -> Result<Vec<MeetingTemplate>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_calendar::Q_006)
.prepare(crate::dbq!(db_calendar::Q_006))
.map_err(|e| format!("Failed to prepare list_meeting_templates: {e}"))?;
let rows = stmt
.query_map([], row_to_template)
@@ -446,24 +452,21 @@ pub fn create_meeting_template(
url_template: &str,
icon: &str,
) -> Result<MeetingTemplate, String> {
conn.execute(
crate::queries::sqlite::queries::db_calendar::Q_007,
params![name, url_template, icon],
)
.map_err(|e| format!("Failed to create meeting template: {e}"))?;
let id = conn
.insert_id(
crate::dbq!(db_calendar::Q_007),
params![name, url_template, icon],
)
.map_err(|e| format!("Failed to create meeting template: {e}"))?;
let id = conn.last_insert_rowid();
let sql = crate::queries::sqlite::queries::db_calendar::Q_008;
let sql = crate::dbq!(db_calendar::Q_008);
conn.query_row(sql, params![id], row_to_template)
.map_err(|e| format!("Failed to read created template: {e}"))
}
pub fn delete_meeting_template(conn: &Connection, id: i64) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_calendar::Q_009,
params![id],
)
.execute(crate::dbq!(db_calendar::Q_009), params![id])
.map_err(|e| format!("Failed to delete meeting template: {e}"))?;
Ok(deleted > 0)
}
+10 -19
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, params};
use crate::db::pool::{Connection, params};
use serde::Serialize;
use crate::db::contacts::Contact;
@@ -13,17 +13,14 @@ pub struct ContactGroup {
}
pub fn create_group(conn: &Connection, id: &str, name: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_contact_groups::Q_001,
params![id, name],
)
.map_err(|e| format!("Failed to create contact group: {e}"))?;
conn.execute(crate::dbq!(db_contact_groups::Q_001), params![id, name])
.map_err(|e| format!("Failed to create contact group: {e}"))?;
Ok(())
}
pub fn list_groups(conn: &Connection) -> Result<Vec<ContactGroup>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_contact_groups::Q_002)
.prepare(crate::dbq!(db_contact_groups::Q_002))
.map_err(|e| format!("Failed to prepare list_groups: {e}"))?;
let rows = stmt
@@ -47,27 +44,21 @@ pub fn list_groups(conn: &Connection) -> Result<Vec<ContactGroup>, String> {
pub fn update_group(conn: &Connection, id: &str, name: &str) -> Result<bool, String> {
let updated = conn
.execute(
crate::queries::sqlite::queries::db_contact_groups::Q_003,
params![name, id],
)
.execute(crate::dbq!(db_contact_groups::Q_003), params![name, id])
.map_err(|e| format!("Failed to update contact group: {e}"))?;
Ok(updated > 0)
}
pub fn delete_group(conn: &Connection, id: &str) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_contact_groups::Q_004,
params![id],
)
.execute(crate::dbq!(db_contact_groups::Q_004), params![id])
.map_err(|e| format!("Failed to delete contact group: {e}"))?;
Ok(deleted > 0)
}
pub fn add_member(conn: &Connection, group_id: &str, contact_id: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_contact_groups::Q_005,
crate::dbq!(db_contact_groups::Q_005),
params![group_id, contact_id],
)
.map_err(|e| format!("Failed to add group member: {e}"))?;
@@ -77,7 +68,7 @@ pub fn add_member(conn: &Connection, group_id: &str, contact_id: &str) -> Result
pub fn remove_member(conn: &Connection, group_id: &str, contact_id: &str) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_contact_groups::Q_006,
crate::dbq!(db_contact_groups::Q_006),
params![group_id, contact_id],
)
.map_err(|e| format!("Failed to remove group member: {e}"))?;
@@ -86,7 +77,7 @@ pub fn remove_member(conn: &Connection, group_id: &str, contact_id: &str) -> Res
pub fn list_group_members(conn: &Connection, group_id: &str) -> Result<Vec<Contact>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_contact_groups::Q_007)
.prepare(crate::dbq!(db_contact_groups::Q_007))
.map_err(|e| format!("Failed to prepare list_group_members: {e}"))?;
let rows = stmt
@@ -131,7 +122,7 @@ pub fn list_contact_groups(
contact_id: &str,
) -> Result<Vec<ContactGroup>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_contact_groups::Q_008)
.prepare(crate::dbq!(db_contact_groups::Q_008))
.map_err(|e| format!("Failed to prepare list_contact_groups: {e}"))?;
let rows = stmt
+33 -43
View File
@@ -1,5 +1,5 @@
use crate::db::pool::{Connection, params};
use crate::queries::sqlite::queries::db_contacts::CONTACT_SELECT_COLS;
use rusqlite::{Connection, params};
use serde::Serialize;
use uuid::Uuid;
@@ -33,7 +33,7 @@ pub struct Contact {
pub has_photo: bool,
}
fn row_to_contact(row: &rusqlite::Row<'_>) -> rusqlite::Result<Contact> {
fn row_to_contact(row: &crate::db::pool::Row<'_>) -> Result<Contact, crate::db::pool::DbError> {
Ok(Contact {
id: row.get(0)?,
email: row.get(1)?,
@@ -60,14 +60,14 @@ fn row_to_contact(row: &rusqlite::Row<'_>) -> rusqlite::Result<Contact> {
}
fn escape_like(term: &str) -> String {
term.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_")
term.replace('!', "!!")
.replace('%', "!%")
.replace('_', "!_")
}
pub fn upsert_contact(conn: &Connection, contact: &Contact) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_001,
crate::dbq!(db_contacts::Q_001),
params![
contact.id,
contact.email,
@@ -95,23 +95,25 @@ pub fn upsert_contact(conn: &Connection, contact: &Contact) -> Result<(), String
}
pub fn get_contact(conn: &Connection, id: &str) -> Result<Option<Contact>, String> {
let sql = crate::rustbox_query_db_contacts_001!();
let sql =
crate::dbq!(db_contacts::FORMAT_001).replace("{CONTACT_SELECT_COLS}", CONTACT_SELECT_COLS);
let result = conn.query_row(&sql, params![id], row_to_contact);
match result {
Ok(c) => Ok(Some(c)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get contact: {e}")),
}
}
pub fn get_contact_by_email(conn: &Connection, email: &str) -> Result<Option<Contact>, String> {
let sql = crate::rustbox_query_db_contacts_002!();
let sql =
crate::dbq!(db_contacts::FORMAT_002).replace("{CONTACT_SELECT_COLS}", CONTACT_SELECT_COLS);
let result = conn.query_row(&sql, params![email], row_to_contact);
match result {
Ok(c) => Ok(Some(c)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get contact by email: {e}")),
}
}
@@ -126,13 +128,15 @@ pub fn list_contacts(
Some(q) if !q.is_empty() => {
let escaped = escape_like(q);
let pattern = format!("%{escaped}%");
let sql = crate::rustbox_query_db_contacts_003!();
let sql = crate::dbq!(db_contacts::FORMAT_003)
.replace("{CONTACT_SELECT_COLS}", CONTACT_SELECT_COLS);
drop(pattern);
(sql, Some(escaped))
}
_ => {
let sql = crate::rustbox_query_db_contacts_004!();
let sql = crate::dbq!(db_contacts::FORMAT_004)
.replace("{CONTACT_SELECT_COLS}", CONTACT_SELECT_COLS);
(sql, None)
}
};
@@ -170,10 +174,7 @@ pub fn list_contacts(
pub fn delete_contact(conn: &Connection, id: &str) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_contacts::Q_002,
params![id],
)
.execute(crate::dbq!(db_contacts::Q_002), params![id])
.map_err(|e| format!("Failed to delete contact: {e}"))?;
Ok(deleted > 0)
}
@@ -182,7 +183,8 @@ pub fn search_contacts(conn: &Connection, query: &str, limit: u32) -> Result<Vec
let escaped = escape_like(query);
let pattern = format!("%{escaped}%");
let sql = crate::rustbox_query_db_contacts_005!();
let sql =
crate::dbq!(db_contacts::FORMAT_005).replace("{CONTACT_SELECT_COLS}", CONTACT_SELECT_COLS);
let mut stmt = conn
.prepare(&sql)
@@ -206,7 +208,7 @@ pub fn search_known_addresses(
let escaped = escape_like(query);
let pattern = format!("%{escaped}%");
let sql = crate::queries::sqlite::queries::db_contacts::Q_003;
let sql = crate::dbq!(db_contacts::Q_003);
let mut stmt = conn
.prepare(sql)
@@ -237,7 +239,7 @@ pub fn populate_known_addresses(
) -> Result<(), String> {
if !from_address.is_empty() {
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_004,
crate::dbq!(db_contacts::Q_004),
params![from_address, from_name],
)
.map_err(|e| format!("Failed to insert known address (from): {e}"))?;
@@ -262,11 +264,8 @@ fn insert_addresses_from_json(conn: &Connection, json: &str) -> Result<(), Strin
continue;
}
let name = entry.get("name").and_then(|v| v.as_str()).unwrap_or("");
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_005,
params![email, name],
)
.map_err(|e| format!("Failed to insert known address (json): {e}"))?;
conn.execute(crate::dbq!(db_contacts::Q_005), params![email, name])
.map_err(|e| format!("Failed to insert known address (json): {e}"))?;
}
Ok(())
@@ -274,22 +273,16 @@ fn insert_addresses_from_json(conn: &Connection, json: &str) -> Result<(), Strin
#[allow(dead_code)]
pub fn increment_contact_count(conn: &Connection, email: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_006,
params![email],
)
.map_err(|e| format!("Failed to increment contact count: {e}"))?;
conn.execute(crate::dbq!(db_contacts::Q_006), params![email])
.map_err(|e| format!("Failed to increment contact count: {e}"))?;
Ok(())
}
#[allow(dead_code)]
pub fn auto_add_contact(conn: &Connection, email: &str, name: &str) -> Result<(), String> {
let id = Uuid::new_v4().to_string();
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_007,
params![id, email, name],
)
.map_err(|e| format!("Failed to auto-add contact: {e}"))?;
conn.execute(crate::dbq!(db_contacts::Q_007), params![id, email, name])
.map_err(|e| format!("Failed to auto-add contact: {e}"))?;
Ok(())
}
@@ -300,7 +293,7 @@ pub fn set_contact_photo(
data: &[u8],
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_008,
crate::dbq!(db_contacts::Q_008),
params![contact_id, content_type, data],
)
.map_err(|e| format!("Failed to save contact photo: {e}"))?;
@@ -312,23 +305,20 @@ pub fn get_contact_photo(
contact_id: &str,
) -> Result<Option<(String, Vec<u8>)>, String> {
match conn.query_row(
crate::queries::sqlite::queries::db_contacts::Q_009,
crate::dbq!(db_contacts::Q_009),
params![contact_id],
|row| Ok((row.get(0)?, row.get(1)?)),
) {
Ok(value) => Ok(Some(value)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to load contact photo: {e}")),
}
}
pub fn delete_contact_photo(conn: &Connection, contact_id: &str) -> Result<bool, String> {
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_010,
params![contact_id],
)
.map(|n| n > 0)
.map_err(|e| format!("Failed to delete contact photo: {e}"))
conn.execute(crate::dbq!(db_contacts::Q_010), params![contact_id])
.map(|n| n > 0)
.map_err(|e| format!("Failed to delete contact photo: {e}"))
}
#[cfg(test)]
+73 -77
View File
@@ -1,4 +1,4 @@
use rusqlite::Connection;
use crate::db::pool::{Connection, DbValue, ToDbValue};
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, Serialize)]
@@ -184,70 +184,66 @@ fn defaults() -> DisplayPreferences {
}
pub fn get_preferences(conn: &Connection) -> Result<DisplayPreferences, String> {
let result = conn.query_row(
crate::queries::sqlite::queries::db_display_preferences::Q_001,
[],
|row| {
Ok(DisplayPreferences {
density: row.get(0)?,
theme: row.get(1)?,
language: row.get(2)?,
compose_format: row.get(3)?,
deep_index: row.get::<_, i32>(4)? != 0,
animation_mode: row.get(5)?,
preview_pane: row.get(6)?,
ui_scale: row.get(7)?,
color_preset: row.get(8)?,
accent_color: row.get(9)?,
background_color: row.get(10)?,
panel_color: row.get(11)?,
text_color: row.get(12)?,
font_family: row.get(13)?,
font_size: row.get(14)?,
row_height: row.get(15)?,
folder_pane_width: row.get(16)?,
message_pane_width: row.get(17)?,
border_radius: row.get(18)?,
contrast: row.get(19)?,
show_avatars: row.get::<_, i32>(20)? != 0,
show_attachment_icons: row.get::<_, i32>(21)? != 0,
show_flags: row.get::<_, i32>(22)? != 0,
reduce_motion: row.get::<_, i32>(23)? != 0,
date_format: row.get(24)?,
time_format: row.get(25)?,
compose_autosave_ms: row.get(26)?,
default_priority: row.get(27)?,
request_mdn_default: row.get::<_, i32>(28)? != 0,
attachment_reminder: row.get::<_, i32>(29)? != 0,
icon_size: row.get(30)?,
toolbar_density: row.get(31)?,
sidebar_item_height: row.get(32)?,
reader_max_width: row.get(33)?,
composer_width: row.get(34)?,
selection_color: row.get(35)?,
unread_font_weight: row.get(36)?,
unread_badge_style: row.get(37)?,
compact_headers: row.get::<_, i32>(38)? != 0,
show_borders: row.get::<_, i32>(39)? != 0,
scrollbar_density: row.get(40)?,
monochrome: row.get::<_, i32>(41)? != 0,
folder_background: row.get(42)?,
message_background: row.get(43)?,
reader_background: row.get(44)?,
compose_font_family: row.get(45)?,
reply_position: row.get(46)?,
quote_style: row.get(47)?,
forward_mode: row.get(48)?,
spellcheck_enabled: row.get::<_, i32>(49)? != 0,
interface_skin: row.get(50)?,
preset_scale_unlocked: row.get::<_, i32>(51)? != 0,
updated_at: row.get(52)?,
})
},
);
let result = conn.query_row(crate::dbq!(db_display_preferences::Q_001), [], |row| {
Ok(DisplayPreferences {
density: row.get(0)?,
theme: row.get(1)?,
language: row.get(2)?,
compose_format: row.get(3)?,
deep_index: row.get::<_, i32>(4)? != 0,
animation_mode: row.get(5)?,
preview_pane: row.get(6)?,
ui_scale: row.get(7)?,
color_preset: row.get(8)?,
accent_color: row.get(9)?,
background_color: row.get(10)?,
panel_color: row.get(11)?,
text_color: row.get(12)?,
font_family: row.get(13)?,
font_size: row.get(14)?,
row_height: row.get(15)?,
folder_pane_width: row.get(16)?,
message_pane_width: row.get(17)?,
border_radius: row.get(18)?,
contrast: row.get(19)?,
show_avatars: row.get::<_, i32>(20)? != 0,
show_attachment_icons: row.get::<_, i32>(21)? != 0,
show_flags: row.get::<_, i32>(22)? != 0,
reduce_motion: row.get::<_, i32>(23)? != 0,
date_format: row.get(24)?,
time_format: row.get(25)?,
compose_autosave_ms: row.get(26)?,
default_priority: row.get(27)?,
request_mdn_default: row.get::<_, i32>(28)? != 0,
attachment_reminder: row.get::<_, i32>(29)? != 0,
icon_size: row.get(30)?,
toolbar_density: row.get(31)?,
sidebar_item_height: row.get(32)?,
reader_max_width: row.get(33)?,
composer_width: row.get(34)?,
selection_color: row.get(35)?,
unread_font_weight: row.get(36)?,
unread_badge_style: row.get(37)?,
compact_headers: row.get::<_, i32>(38)? != 0,
show_borders: row.get::<_, i32>(39)? != 0,
scrollbar_density: row.get(40)?,
monochrome: row.get::<_, i32>(41)? != 0,
folder_background: row.get(42)?,
message_background: row.get(43)?,
reader_background: row.get(44)?,
compose_font_family: row.get(45)?,
reply_position: row.get(46)?,
quote_style: row.get(47)?,
forward_mode: row.get(48)?,
spellcheck_enabled: row.get::<_, i32>(49)? != 0,
interface_skin: row.get(50)?,
preset_scale_unlocked: row.get::<_, i32>(51)? != 0,
updated_at: row.get(52)?,
})
});
match result {
Ok(value) => Ok(value),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(defaults()),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(defaults()),
Err(error) => Err(format!("Failed to get display preferences: {error}")),
}
}
@@ -274,20 +270,17 @@ pub fn update_preferences(
conn: &Connection,
data: &UpdateDisplayPreferences,
) -> Result<DisplayPreferences, String> {
conn.execute(
crate::queries::sqlite::queries::db_display_preferences::Q_002,
[],
)
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
conn.execute(crate::dbq!(db_display_preferences::Q_002), [])
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
let mut sets = Vec::new();
let mut values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut values: Vec<DbValue> = Vec::new();
let mut idx = 1usize;
macro_rules! set_text {
($field:ident, $column:literal) => {
if let Some(ref value) = data.$field {
sets.push(format!(concat!($column, " = ?{}"), idx));
values.push(Box::new(value.clone()));
values.push(value.to_db_value());
idx += 1;
}
};
@@ -296,7 +289,7 @@ pub fn update_preferences(
($field:ident, $column:literal) => {
if let Some(value) = data.$field {
sets.push(format!(concat!($column, " = ?{}"), idx));
values.push(Box::new(value));
values.push(value.to_db_value());
idx += 1;
}
};
@@ -305,7 +298,7 @@ pub fn update_preferences(
($field:ident, $column:literal) => {
if let Some(value) = data.$field {
sets.push(format!(concat!($column, " = ?{}"), idx));
values.push(Box::new(value as i32));
values.push((value as i32).to_db_value());
idx += 1;
}
};
@@ -499,7 +492,7 @@ pub fn update_preferences(
set_bool!(deep_index, "deep_index");
if let Some(ref value) = data.animation_mode {
sets.push(format!("animation_mode = ?{}", idx));
values.push(Box::new(value.clone()));
values.push(value.to_db_value());
idx += 1;
}
set_text!(preview_pane, "preview_pane");
@@ -549,10 +542,13 @@ pub fn update_preferences(
return get_preferences(conn);
}
sets.push("updated_at = datetime('now')".to_string());
let sql = crate::rustbox_query_db_display_preferences_001!(sets.join(", "), idx);
values.push(Box::new(1_i32));
let refs: Vec<&dyn rusqlite::types::ToSql> = values.iter().map(|v| v.as_ref()).collect();
conn.execute(&sql, refs.as_slice())
let sql = format!(
"UPDATE display_preferences SET {} WHERE id = ?{}",
sets.join(", "),
idx
);
values.push(1_i32.to_db_value());
conn.execute(&sql, values)
.map_err(|e| format!("Failed to update display preferences: {e}"))?;
get_preferences(conn)
}
+34 -44
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, params};
use crate::db::pool::{Connection, params};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
@@ -72,7 +72,7 @@ pub fn upsert_draft(
) -> Result<(), String> {
validate_draft_id(id)?;
conn.execute(
crate::queries::sqlite::queries::db_drafts::Q_001,
crate::dbq!(db_drafts::Q_001),
params![
id,
to_addresses,
@@ -97,46 +97,42 @@ pub fn upsert_draft(
}
pub fn get_draft(conn: &Connection, id: &str) -> Result<Option<Draft>, String> {
let result = conn.query_row(
crate::queries::sqlite::queries::db_drafts::Q_002,
params![id],
|row| {
Ok(Draft {
id: row.get(0)?,
to_addresses: row.get(1)?,
cc_addresses: row.get(2)?,
bcc_addresses: row.get(3)?,
subject: row.get(4)?,
text_body: row.get(5)?,
html_body: row.get(6)?,
in_reply_to: row.get(7)?,
references_header: row.get(8)?,
request_mdn: row.get::<_, i64>(9)? != 0,
priority: row.get(10)?,
compose_format: row.get(11)?,
pgp_mode: row.get(12)?,
pgp_inline: row.get::<_, i64>(13)? != 0,
pgp_attach_public_key: row.get::<_, i64>(14)? != 0,
pgp_attach_recipient_keys: row.get::<_, i64>(15)? != 0,
remote_folder: row.get(16)?,
remote_uid: row.get(17)?,
remote_uid_validity: row.get(18)?,
created_at: row.get(19)?,
updated_at: row.get(20)?,
})
},
);
let result = conn.query_row(crate::dbq!(db_drafts::Q_002), params![id], |row| {
Ok(Draft {
id: row.get(0)?,
to_addresses: row.get(1)?,
cc_addresses: row.get(2)?,
bcc_addresses: row.get(3)?,
subject: row.get(4)?,
text_body: row.get(5)?,
html_body: row.get(6)?,
in_reply_to: row.get(7)?,
references_header: row.get(8)?,
request_mdn: row.get::<_, i64>(9)? != 0,
priority: row.get(10)?,
compose_format: row.get(11)?,
pgp_mode: row.get(12)?,
pgp_inline: row.get::<_, i64>(13)? != 0,
pgp_attach_public_key: row.get::<_, i64>(14)? != 0,
pgp_attach_recipient_keys: row.get::<_, i64>(15)? != 0,
remote_folder: row.get(16)?,
remote_uid: row.get(17)?,
remote_uid_validity: row.get(18)?,
created_at: row.get(19)?,
updated_at: row.get(20)?,
})
});
match result {
Ok(draft) => Ok(Some(draft)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get draft: {e}")),
}
}
pub fn list_drafts(conn: &Connection) -> Result<Vec<Draft>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_drafts::Q_003)
.prepare(crate::dbq!(db_drafts::Q_003))
.map_err(|e| format!("Failed to prepare list_drafts: {e}"))?;
let rows = stmt
@@ -183,7 +179,7 @@ pub fn set_remote_location(
) -> Result<(), String> {
let updated = conn
.execute(
crate::queries::sqlite::queries::db_drafts::Q_004,
crate::dbq!(db_drafts::Q_004),
params![id, folder, uid, uid_validity],
)
.map_err(|e| format!("Failed to update remote draft location: {e}"))?;
@@ -195,10 +191,7 @@ pub fn set_remote_location(
pub fn delete_draft(conn: &Connection, id: &str) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_drafts::Q_005,
params![id],
)
.execute(crate::dbq!(db_drafts::Q_005), params![id])
.map_err(|e| format!("Failed to delete draft: {e}"))?;
Ok(deleted > 0)
}
@@ -213,7 +206,7 @@ pub fn add_draft_attachment(
file_path: &str,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_drafts::Q_006,
crate::dbq!(db_drafts::Q_006),
params![id, draft_id, filename, content_type, size, file_path],
)
.map_err(|e| format!("Failed to add draft attachment: {e}"))?;
@@ -225,7 +218,7 @@ pub fn get_draft_attachments(
draft_id: &str,
) -> Result<Vec<DraftAttachment>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_drafts::Q_007)
.prepare(crate::dbq!(db_drafts::Q_007))
.map_err(|e| format!("Failed to prepare get_draft_attachments: {e}"))?;
let rows = stmt
@@ -251,10 +244,7 @@ pub fn get_draft_attachments(
pub fn delete_draft_attachment(conn: &Connection, id: &str) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_drafts::Q_008,
params![id],
)
.execute(crate::dbq!(db_drafts::Q_008), params![id])
.map_err(|e| format!("Failed to delete draft attachment: {e}"))?;
Ok(deleted > 0)
}
+19 -32
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, OptionalExtension, params};
use crate::db::pool::{Connection, OptionalExtension, params};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
@@ -13,11 +13,9 @@ pub struct UpdateFolderPreferences {
}
fn get_override(conn: &Connection, folder: &str) -> Result<Option<bool>, String> {
conn.query_row(
crate::queries::sqlite::queries::db_folder_preferences::Q_001,
[folder],
|row| row.get::<_, Option<i32>>(0),
)
conn.query_row(crate::dbq!(db_folder_preferences::Q_001), [folder], |row| {
row.get::<_, Option<i32>>(0)
})
.optional()
.map(|value| value.flatten().map(|value| value != 0))
.map_err(|error| format!("Failed to read folder preferences: {error}"))
@@ -28,33 +26,27 @@ pub fn effective_flagged_on_top(conn: &Connection, folder: &str) -> Result<bool,
return Ok(value);
}
let global = conn
.query_row(
crate::queries::sqlite::queries::db_folder_preferences::Q_002,
[],
|row| row.get::<_, i32>(0),
)
.query_row(crate::dbq!(db_folder_preferences::Q_002), [], |row| {
row.get::<_, i32>(0)
})
.unwrap_or(0);
Ok(global != 0)
}
pub fn get_preferences(conn: &Connection, folder: &str) -> Result<FolderPreferences, String> {
let exists: bool = conn
.query_row(
crate::queries::sqlite::queries::db_folder_preferences::Q_003,
[folder],
|row| row.get(0),
)
.query_row(crate::dbq!(db_folder_preferences::Q_003), [folder], |row| {
row.get(0)
})
.map_err(|error| format!("Failed to validate folder: {error}"))?;
if !exists {
return Err(format!("Folder does not exist: {folder}"));
}
let flagged_on_top = get_override(conn, folder)?;
let effective_flagged_on_top = flagged_on_top.unwrap_or(
conn.query_row(
crate::queries::sqlite::queries::db_folder_preferences::Q_004,
[],
|row| row.get::<_, i32>(0),
)
conn.query_row(crate::dbq!(db_folder_preferences::Q_004), [], |row| {
row.get::<_, i32>(0)
})
.unwrap_or(0)
!= 0,
);
@@ -70,11 +62,9 @@ pub fn update_preferences(
data: &UpdateFolderPreferences,
) -> Result<FolderPreferences, String> {
let exists: bool = conn
.query_row(
crate::queries::sqlite::queries::db_folder_preferences::Q_005,
[folder],
|row| row.get(0),
)
.query_row(crate::dbq!(db_folder_preferences::Q_005), [folder], |row| {
row.get(0)
})
.map_err(|error| format!("Failed to validate folder: {error}"))?;
if !exists {
return Err(format!("Folder does not exist: {folder}"));
@@ -83,17 +73,14 @@ pub fn update_preferences(
match data.flagged_on_top {
Some(value) => {
conn.execute(
crate::queries::sqlite::queries::db_folder_preferences::Q_006,
crate::dbq!(db_folder_preferences::Q_006),
params![folder, value as i32],
)
.map_err(|error| format!("Failed to update folder preferences: {error}"))?;
}
None => {
conn.execute(
crate::queries::sqlite::queries::db_folder_preferences::Q_007,
[folder],
)
.map_err(|error| format!("Failed to clear folder preferences: {error}"))?;
conn.execute(crate::dbq!(db_folder_preferences::Q_007), [folder])
.map_err(|error| format!("Failed to clear folder preferences: {error}"))?;
}
}
+63 -107
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, OptionalExtension, params};
use crate::db::pool::{Connection, DbValue, OptionalExtension, ToDbValue, params};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
@@ -28,7 +28,7 @@ pub fn upsert_folder(
highest_modseq: u64,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_001,
crate::dbq!(db_folders::Q_001),
params![
folder_name,
delimiter,
@@ -52,7 +52,7 @@ pub fn insert_folder_if_new(
flags_csv: &str,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_002,
crate::dbq!(db_folders::Q_002),
params![folder_name, delimiter, flags_csv],
)
.map_err(|e| format!("Failed to insert folder: {e}"))?;
@@ -61,7 +61,7 @@ pub fn insert_folder_if_new(
pub fn get_all_folders(conn: &Connection) -> Result<Vec<CachedFolder>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_folders::Q_003)
.prepare(crate::dbq!(db_folders::Q_003))
.map_err(|e| format!("Failed to prepare get_all_folders: {e}"))?;
let rows = stmt
@@ -92,28 +92,28 @@ pub fn get_all_folders(conn: &Connection) -> Result<Vec<CachedFolder>, String> {
pub fn remove_stale_folders(conn: &Connection, current_names: &[String]) -> Result<usize, String> {
if current_names.is_empty() {
let deleted = conn
.execute(crate::queries::sqlite::queries::db_folders::Q_004, [])
.execute(crate::dbq!(db_folders::Q_004), [])
.map_err(|e| format!("Failed to delete all folders: {e}"))?;
return Ok(deleted);
}
let placeholders: Vec<String> = (1..=current_names.len()).map(|i| format!("?{i}")).collect();
let sql = crate::rustbox_query_db_folders_001!(placeholders.join(", "));
let sql = format!(
"DELETE FROM folders WHERE name NOT IN ({})",
placeholders.join(", ")
);
let params: Vec<&dyn rusqlite::types::ToSql> = current_names
.iter()
.map(|n| n as &dyn rusqlite::types::ToSql)
.collect();
let params: Vec<DbValue> = current_names.iter().map(ToDbValue::to_db_value).collect();
let deleted = conn
.execute(&sql, params.as_slice())
.execute(&sql, params)
.map_err(|e| format!("Failed to remove stale folders: {e}"))?;
Ok(deleted)
}
pub fn touch_all_folders(conn: &Connection) -> Result<(), String> {
conn.execute(crate::queries::sqlite::queries::db_folders::Q_005, [])
conn.execute(crate::dbq!(db_folders::Q_005), [])
.map_err(|e| format!("Failed to touch folders: {e}"))?;
Ok(())
}
@@ -125,7 +125,7 @@ pub fn update_folder_status(
total_count: u32,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_006,
crate::dbq!(db_folders::Q_006),
params![uid_validity, total_count, name],
)
.map_err(|e| format!("Failed to update folder status: {e}"))?;
@@ -140,7 +140,7 @@ pub fn update_folder_sync_status(
highest_modseq: u64,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_007,
crate::dbq!(db_folders::Q_007),
params![uid_validity, total_count, highest_modseq as i64, name],
)
.map_err(|e| format!("Failed to update folder sync status: {e}"))?;
@@ -150,35 +150,29 @@ pub fn update_folder_sync_status(
pub fn refresh_unread_count(conn: &Connection, folder_name: &str) -> Result<(), String> {
let unread: u32 = conn
.query_row(
crate::queries::sqlite::queries::db_folders::Q_008,
crate::dbq!(db_folders::Q_008),
params![folder_name],
|row| row.get(0),
)
.map_err(|e| format!("Failed to count unread messages: {e}"))?;
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_009,
params![unread, folder_name],
)
.map_err(|e| format!("Failed to update unread_count: {e}"))?;
conn.execute(crate::dbq!(db_folders::Q_009), params![unread, folder_name])
.map_err(|e| format!("Failed to update unread_count: {e}"))?;
Ok(())
}
pub fn adjust_unread_count(conn: &Connection, folder_name: &str, delta: i32) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_010,
params![delta, folder_name],
)
.map_err(|e| format!("Failed to adjust unread_count: {e}"))?;
conn.execute(crate::dbq!(db_folders::Q_010), params![delta, folder_name])
.map_err(|e| format!("Failed to adjust unread_count: {e}"))?;
Ok(())
}
pub fn is_folder_cache_fresh(conn: &Connection, max_age_secs: u32) -> Result<bool, String> {
let fresh: bool = conn
.query_row(
crate::queries::sqlite::queries::db_folders::Q_011,
params![format!("-{max_age_secs} seconds")],
crate::dbq!(db_folders::Q_011),
params![max_age_secs],
|row| row.get(0),
)
.map_err(|e| format!("Failed to check folder cache freshness: {e}"))?;
@@ -192,8 +186,8 @@ pub fn is_folder_fresh(
) -> Result<bool, String> {
let fresh: bool = conn
.query_row(
crate::queries::sqlite::queries::db_folders::Q_012,
params![folder_name, format!("-{max_age_secs} seconds")],
crate::dbq!(db_folders::Q_012),
params![folder_name, max_age_secs],
|row| row.get(0),
)
.map_err(|e| format!("Failed to check folder freshness: {e}"))?;
@@ -206,7 +200,7 @@ pub fn is_folder_messages_invalidated(
) -> Result<bool, String> {
let invalidated: bool = conn
.query_row(
crate::queries::sqlite::queries::db_folders::Q_013,
crate::dbq!(db_folders::Q_013),
params![folder_name],
|row| row.get(0),
)
@@ -215,53 +209,40 @@ pub fn is_folder_messages_invalidated(
}
pub fn invalidate_folder_freshness(conn: &Connection, folder_name: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_014,
params![folder_name],
)
.map_err(|e| format!("Failed to invalidate folder freshness: {e}"))?;
conn.execute(crate::dbq!(db_folders::Q_014), params![folder_name])
.map_err(|e| format!("Failed to invalidate folder freshness: {e}"))?;
Ok(())
}
pub fn get_folder(conn: &Connection, name: &str) -> Result<Option<CachedFolder>, String> {
let result = conn.query_row(
crate::queries::sqlite::queries::db_folders::Q_015,
params![name],
|row| {
let is_subscribed_int: i32 = row.get(4)?;
let highest_modseq_int: i64 = row.get(8)?;
Ok(CachedFolder {
name: row.get(0)?,
delimiter: row.get(1)?,
parent: row.get(2)?,
flags: row.get(3)?,
is_subscribed: is_subscribed_int != 0,
total_count: row.get(5)?,
unread_count: row.get(6)?,
uid_validity: row.get(7)?,
highest_modseq: highest_modseq_int as u64,
})
},
);
let result = conn.query_row(crate::dbq!(db_folders::Q_015), params![name], |row| {
let is_subscribed_int: i32 = row.get(4)?;
let highest_modseq_int: i64 = row.get(8)?;
Ok(CachedFolder {
name: row.get(0)?,
delimiter: row.get(1)?,
parent: row.get(2)?,
flags: row.get(3)?,
is_subscribed: is_subscribed_int != 0,
total_count: row.get(5)?,
unread_count: row.get(6)?,
uid_validity: row.get(7)?,
highest_modseq: highest_modseq_int as u64,
})
});
match result {
Ok(folder) => Ok(Some(folder)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get folder: {e}")),
}
}
pub fn delete_folder_and_messages(conn: &Connection, folder_name: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_016,
params![folder_name],
)
.map_err(|e| format!("Failed to delete folder messages: {e}"))?;
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_017,
params![folder_name],
)
.map_err(|e| format!("Failed to delete folder: {e}"))?;
conn.execute(crate::dbq!(db_folders::Q_016), params![folder_name])
.map_err(|e| format!("Failed to delete folder messages: {e}"))?;
conn.execute(crate::dbq!(db_folders::Q_017), params![folder_name])
.map_err(|e| format!("Failed to delete folder: {e}"))?;
Ok(())
}
@@ -273,44 +254,26 @@ pub fn rename_folder_in_cache(
let tx = conn
.unchecked_transaction()
.map_err(|e| format!("Failed to start folder rename transaction: {e}"))?;
tx.execute_batch(crate::queries::sqlite::queries::db_folders::Q_018)
tx.execute_batch(crate::dbq!(db_folders::Q_018))
.map_err(|e| format!("Failed to defer folder rename foreign keys: {e}"))?;
let inserted = tx
.execute(
crate::queries::sqlite::queries::db_folders::Q_019,
params![new_name, old_name],
)
.execute(crate::dbq!(db_folders::Q_019), params![new_name, old_name])
.map_err(|e| format!("Failed to create renamed folder cache entry: {e}"))?;
if inserted == 0 {
return Err(format!("Folder not found in cache: {old_name}"));
}
tx.execute(
crate::queries::sqlite::queries::db_folders::Q_020,
params![new_name, old_name],
)
.map_err(|e| format!("Failed to rename messages folder: {e}"))?;
tx.execute(
crate::queries::sqlite::queries::db_folders::Q_021,
params![new_name, old_name],
)
.map_err(|e| format!("Failed to rename message tag folder references: {e}"))?;
tx.execute(
crate::queries::sqlite::queries::db_folders::Q_022,
params![new_name, old_name],
)
.ok();
tx.execute(
crate::queries::sqlite::queries::db_folders::Q_023,
params![new_name, old_name],
)
.map_err(|e| format!("Failed to update child folder parent: {e}"))?;
tx.execute(
crate::queries::sqlite::queries::db_folders::Q_024,
params![old_name],
)
.map_err(|e| format!("Failed to remove old folder cache entry: {e}"))?;
tx.execute(crate::dbq!(db_folders::Q_020), params![new_name, old_name])
.map_err(|e| format!("Failed to rename messages folder: {e}"))?;
tx.execute(crate::dbq!(db_folders::Q_021), params![new_name, old_name])
.map_err(|e| format!("Failed to rename message tag folder references: {e}"))?;
tx.execute(crate::dbq!(db_folders::Q_022), params![new_name, old_name])
.ok();
tx.execute(crate::dbq!(db_folders::Q_023), params![new_name, old_name])
.map_err(|e| format!("Failed to update child folder parent: {e}"))?;
tx.execute(crate::dbq!(db_folders::Q_024), params![old_name])
.map_err(|e| format!("Failed to remove old folder cache entry: {e}"))?;
tx.commit()
.map_err(|e| format!("Failed to commit folder rename: {e}"))?;
Ok(())
@@ -320,11 +283,9 @@ pub fn find_folder_by_attribute(
conn: &Connection,
attribute: &str,
) -> Result<Option<String>, String> {
conn.query_row(
crate::queries::sqlite::queries::db_folders::Q_025,
[attribute],
|row| row.get(0),
)
conn.query_row(crate::dbq!(db_folders::Q_025), [attribute], |row| {
row.get(0)
})
.optional()
.map_err(|e| e.to_string())
}
@@ -442,17 +403,12 @@ mod tests {
fn sync_metadata_preserves_message_sync_timestamp() {
let conn = open_test_db();
insert_folder_if_new(&conn, "Sent", None, "\\Sent").unwrap();
conn.execute(crate::queries::sqlite::queries::db_folders::Q_026, [])
.unwrap();
conn.execute(crate::dbq!(db_folders::Q_026), []).unwrap();
update_folder_sync_status(&conn, "Sent", 1, 5, 42).unwrap();
let timestamp: String = conn
.query_row(
crate::queries::sqlite::queries::db_folders::Q_027,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(db_folders::Q_027), [], |row| row.get(0))
.unwrap();
assert_eq!(timestamp, "2000-01-01 00:00:00");
assert!(!is_folder_fresh(&conn, "Sent", 30).unwrap());
+9 -12
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, OptionalExtension, Transaction, TransactionBehavior};
use crate::db::pool::{Connection, OptionalExtension, Transaction};
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) struct HeaderSync {
@@ -8,11 +8,9 @@ pub(crate) struct HeaderSync {
impl HeaderSync {
pub(crate) fn capture(conn: &Connection) -> Result<Self, String> {
let generation = conn
.query_row(
crate::queries::sqlite::queries::db_header_sync::Q_001,
[],
|row| row.get::<_, String>(0),
)
.query_row(crate::dbq!(db_header_sync::Q_001), [], |row| {
row.get::<_, String>(0)
})
.optional()
.map_err(|e| e.to_string())?
.map(|value| value.parse::<u64>().map_err(|e| e.to_string()))
@@ -21,18 +19,17 @@ impl HeaderSync {
Ok(Self { generation })
}
pub(crate) fn begin_write(self, conn: &Connection) -> Result<Transaction<'_>, String> {
let transaction = Transaction::new_unchecked(conn, TransactionBehavior::Immediate)
.map_err(|e| e.to_string())?;
if Self::capture(conn)?.generation != self.generation {
pub(crate) fn begin_write(self, conn: &Connection) -> Result<Transaction, String> {
let transaction = conn.unchecked_transaction().map_err(|e| e.to_string())?;
if Self::capture(&transaction)?.generation != self.generation {
return Err("Header sync superseded by cache removal; retry on next sync".into());
}
Ok(transaction)
}
}
pub(crate) fn invalidate(conn: &Transaction<'_>) -> Result<(), String> {
conn.execute(crate::queries::sqlite::queries::db_header_sync::Q_002, [])
pub(crate) fn invalidate(conn: &Transaction) -> Result<(), String> {
conn.execute(crate::dbq!(db_header_sync::Q_002), [])
.map_err(|e| e.to_string())?;
Ok(())
}
+47 -49
View File
@@ -1,5 +1,5 @@
use crate::db::pool::{Connection, DbValue, ToDbValue, params};
use crate::queries::sqlite::queries::db_identities::IDENTITY_SELECT_COLS;
use rusqlite::{Connection, params};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
@@ -95,7 +95,7 @@ fn validate_signature_position(value: &str) -> Result<(), String> {
}
}
fn row_to_identity(row: &rusqlite::Row<'_>) -> rusqlite::Result<Identity> {
fn row_to_identity(row: &crate::db::pool::Row<'_>) -> Result<Identity, crate::db::pool::DbError> {
Ok(Identity {
id: row.get(0)?,
display_name: row.get(1)?,
@@ -120,7 +120,8 @@ fn row_to_identity(row: &rusqlite::Row<'_>) -> rusqlite::Result<Identity> {
}
pub fn list_identities(conn: &Connection) -> Result<Vec<Identity>, String> {
let sql = crate::rustbox_query_db_identities_001!();
let sql = crate::dbq!(db_identities::FORMAT_001)
.replace("{IDENTITY_SELECT_COLS}", IDENTITY_SELECT_COLS);
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("Failed to prepare list_identities: {e}"))?;
@@ -132,20 +133,22 @@ pub fn list_identities(conn: &Connection) -> Result<Vec<Identity>, String> {
}
pub fn get_identity(conn: &Connection, id: i64) -> Result<Option<Identity>, String> {
let sql = crate::rustbox_query_db_identities_002!();
let sql = crate::dbq!(db_identities::FORMAT_002)
.replace("{IDENTITY_SELECT_COLS}", IDENTITY_SELECT_COLS);
match conn.query_row(&sql, params![id], row_to_identity) {
Ok(identity) => Ok(Some(identity)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get identity: {e}")),
}
}
#[allow(dead_code)]
pub fn get_default_identity(conn: &Connection) -> Result<Option<Identity>, String> {
let sql = crate::rustbox_query_db_identities_003!();
let sql = crate::dbq!(db_identities::FORMAT_003)
.replace("{IDENTITY_SELECT_COLS}", IDENTITY_SELECT_COLS);
match conn.query_row(&sql, [], row_to_identity) {
Ok(identity) => Ok(Some(identity)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get default identity: {e}")),
}
}
@@ -155,29 +158,29 @@ pub fn create_identity(conn: &Connection, data: &CreateIdentity) -> Result<Ident
if data.is_default {
clear_default(conn)?;
}
conn.execute(
crate::queries::sqlite::queries::db_identities::Q_001,
params![
data.display_name,
data.email,
data.signature_html,
data.is_default as i32,
data.reply_to,
data.bcc,
data.organization,
data.signature_text,
data.signature_new as i32,
data.signature_reply as i32,
data.signature_forward as i32,
data.signature_position,
data.pgp_fingerprint,
data.pgp_sign_default as i32,
data.pgp_encrypt_default as i32,
data.pgp_inline_default as i32
],
)
.map_err(|e| format!("Failed to create identity: {e}"))?;
let id = conn.last_insert_rowid();
let id = conn
.insert_id(
crate::dbq!(db_identities::Q_001),
params![
data.display_name,
data.email,
data.signature_html,
data.is_default as i32,
data.reply_to,
data.bcc,
data.organization,
data.signature_text,
data.signature_new as i32,
data.signature_reply as i32,
data.signature_forward as i32,
data.signature_position,
data.pgp_fingerprint,
data.pgp_sign_default as i32,
data.pgp_encrypt_default as i32,
data.pgp_inline_default as i32
],
)
.map_err(|e| format!("Failed to create identity: {e}"))?;
get_identity(conn, id)?.ok_or_else(|| "Failed to read back created identity".to_string())
}
@@ -196,13 +199,13 @@ pub fn update_identity(
validate_signature_position(value)?;
}
let mut sets = Vec::new();
let mut values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut values: Vec<DbValue> = Vec::new();
let mut idx = 1usize;
macro_rules! text {
($field:ident,$column:literal) => {
if let Some(ref value) = data.$field {
sets.push(format!(concat!($column, " = ?{}"), idx));
values.push(Box::new(value.clone()));
values.push(value.to_db_value());
idx += 1;
}
};
@@ -211,7 +214,7 @@ pub fn update_identity(
($field:ident,$column:literal) => {
if let Some(value) = data.$field {
sets.push(format!(concat!($column, " = ?{}"), idx));
values.push(Box::new(value as i32));
values.push((value as i32).to_db_value());
idx += 1;
}
};
@@ -230,7 +233,7 @@ pub fn update_identity(
text!(signature_position, "signature_position");
if let Some(ref value) = data.pgp_fingerprint {
sets.push(format!("pgp_fingerprint = ?{}", idx));
values.push(Box::new(value.clone()));
values.push(value.to_db_value());
idx += 1;
}
boolean!(pgp_sign_default, "pgp_sign_default");
@@ -240,38 +243,33 @@ pub fn update_identity(
return get_identity(conn, id);
}
sets.push("updated_at = datetime('now')".to_string());
let sql = crate::rustbox_query_db_identities_004!(sets.join(", "), idx);
values.push(Box::new(id));
let refs: Vec<&dyn rusqlite::types::ToSql> =
values.iter().map(|value| value.as_ref()).collect();
conn.execute(&sql, refs.as_slice())
let sql = format!(
"UPDATE identities SET {} WHERE id = ?{}",
sets.join(", "),
idx
);
values.push(id.to_db_value());
conn.execute(&sql, values)
.map_err(|e| format!("Failed to update identity: {e}"))?;
get_identity(conn, id)
}
pub fn delete_identity(conn: &Connection, id: i64) -> Result<bool, String> {
Ok(conn
.execute(
crate::queries::sqlite::queries::db_identities::Q_002,
params![id],
)
.execute(crate::dbq!(db_identities::Q_002), params![id])
.map_err(|e| format!("Failed to delete identity: {e}"))?
> 0)
}
pub fn has_identities(conn: &Connection) -> Result<bool, String> {
let count: i64 = conn
.query_row(
crate::queries::sqlite::queries::db_identities::Q_003,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(db_identities::Q_003), [], |row| row.get(0))
.map_err(|e| format!("Failed to count identities: {e}"))?;
Ok(count > 0)
}
fn clear_default(conn: &Connection) -> Result<(), String> {
conn.execute(crate::queries::sqlite::queries::db_identities::Q_004, [])
conn.execute(crate::dbq!(db_identities::Q_004), [])
.map_err(|e| format!("Failed to clear default identity: {e}"))?;
Ok(())
}
+20 -27
View File
@@ -1,4 +1,4 @@
use rusqlite::Connection;
use crate::db::pool::{Connection, params};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
@@ -31,25 +31,21 @@ pub struct UpdateMailPreferences {
}
pub fn get_preferences(conn: &Connection) -> Result<MailPreferences, String> {
conn.query_row(
crate::queries::sqlite::queries::db_mail_preferences::Q_001,
[],
|row| {
Ok(MailPreferences {
messages_per_page: row.get::<_, u32>(0)?,
mark_read_delay_ms: row.get::<_, u64>(1)?,
remote_images: row.get(2)?,
preferred_body: row.get(3)?,
thread_mode: row.get(4)?,
collapse_quotes: row.get::<_, i32>(5)? != 0,
show_snippets: row.get::<_, i32>(6)? != 0,
default_sort: row.get(7)?,
flagged_on_top: row.get::<_, i32>(8)? != 0,
sync_imap_keywords: row.get::<_, i32>(9)? != 0,
updated_at: row.get(10)?,
})
},
)
conn.query_row(crate::dbq!(db_mail_preferences::Q_001), [], |row| {
Ok(MailPreferences {
messages_per_page: row.get::<_, u32>(0)?,
mark_read_delay_ms: row.get::<_, u64>(1)?,
remote_images: row.get(2)?,
preferred_body: row.get(3)?,
thread_mode: row.get(4)?,
collapse_quotes: row.get::<_, i32>(5)? != 0,
show_snippets: row.get::<_, i32>(6)? != 0,
default_sort: row.get(7)?,
flagged_on_top: row.get::<_, i32>(8)? != 0,
sync_imap_keywords: row.get::<_, i32>(9)? != 0,
updated_at: row.get(10)?,
})
})
.map_err(|error| format!("Failed to read mail preferences: {error}"))
}
@@ -57,11 +53,8 @@ pub fn update_preferences(
conn: &Connection,
data: &UpdateMailPreferences,
) -> Result<MailPreferences, String> {
conn.execute(
crate::queries::sqlite::queries::db_mail_preferences::Q_002,
[],
)
.map_err(|error| format!("Failed to initialize mail preferences: {error}"))?;
conn.execute(crate::dbq!(db_mail_preferences::Q_002), [])
.map_err(|error| format!("Failed to initialize mail preferences: {error}"))?;
if let Some(value) = data.messages_per_page
&& !(20..=200).contains(&value)
@@ -130,8 +123,8 @@ pub fn update_preferences(
.unwrap_or(current.sync_imap_keywords);
conn.execute(
crate::queries::sqlite::queries::db_mail_preferences::Q_003,
rusqlite::params![
crate::dbq!(db_mail_preferences::Q_003),
params![
messages_per_page,
mark_read_delay_ms,
remote_images,
+76 -74
View File
@@ -1,5 +1,7 @@
use crate::db::pool::{
Connection, DatabaseBackend, DbValue, ToDbValue, configured_backend, params,
};
use crate::queries::sqlite::queries::db_messages::MSG_SELECT_COLS;
use rusqlite::{Connection, params};
use serde::Serialize;
#[derive(Debug, Clone)]
@@ -35,7 +37,9 @@ pub struct CachedMessage {
pub reaction: Option<String>,
}
fn row_to_cached_message(row: &rusqlite::Row<'_>) -> rusqlite::Result<CachedMessage> {
fn row_to_cached_message(
row: &crate::db::pool::Row<'_>,
) -> Result<CachedMessage, crate::db::pool::DbError> {
let has_attachments_int: i32 = row.get(13)?;
Ok(CachedMessage {
uid: row.get(0)?,
@@ -79,7 +83,7 @@ pub fn upsert_message(
) -> Result<(), String> {
let date_epoch = parse_date_to_epoch(date);
conn.execute(
crate::queries::sqlite::queries::db_messages::Q_001,
crate::dbq!(db_messages::Q_001),
params![
uid,
folder,
@@ -105,37 +109,30 @@ pub fn upsert_message(
}
pub fn remove_after_mutation(conn: &Connection, folder: &str, uid: u32) -> Result<(), String> {
let transaction =
rusqlite::Transaction::new_unchecked(conn, rusqlite::TransactionBehavior::Immediate)
.map_err(|e| e.to_string())?;
delete_message(conn, folder, uid)?;
let transaction = conn.unchecked_transaction().map_err(|e| e.to_string())?;
delete_message(&transaction, folder, uid)?;
super::header_sync::invalidate(&transaction)?;
super::folders::invalidate_folder_freshness(conn, folder)?;
super::folders::invalidate_folder_freshness(&transaction, folder)?;
transaction.commit().map_err(|e| e.to_string())
}
pub fn delete_folder_messages(conn: &Connection, folder: &str) -> Result<usize, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_messages::Q_002,
params![folder],
)
.execute(crate::dbq!(db_messages::Q_002), params![folder])
.map_err(|e| format!("Failed to delete folder messages: {e}"))?;
Ok(deleted)
}
pub fn max_uid(conn: &Connection, folder: &str) -> Result<u32, String> {
conn.query_row(
crate::queries::sqlite::queries::db_messages::Q_003,
params![folder],
|row| row.get(0),
)
conn.query_row(crate::dbq!(db_messages::Q_003), params![folder], |row| {
row.get(0)
})
.map_err(|e| format!("Failed to get max uid: {e}"))
}
pub fn list_index_source_messages(conn: &Connection) -> Result<Vec<IndexSourceMessage>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_messages::Q_004)
.prepare(crate::dbq!(db_messages::Q_004))
.map_err(|e| format!("Failed to prepare index rebuild query: {e}"))?;
let rows = stmt
@@ -253,7 +250,7 @@ pub fn get_messages(
per_page: u32,
) -> Result<Vec<CachedMessage>, String> {
let offset = page * per_page;
let sql = crate::rustbox_query_db_messages_001!();
let sql = crate::dbq!(db_messages::FORMAT_001).replace("{MSG_SELECT_COLS}", MSG_SELECT_COLS);
let mut stmt = conn
.prepare(&sql)
@@ -298,13 +295,20 @@ pub fn get_messages_sorted(
_ => return Err(format!("Unsupported message sort: {sort}")),
};
let order_by = if flagged_on_top {
format!("CASE WHEN instr(flags, '\\Flagged') > 0 THEN 0 ELSE 1 END ASC, {base_order}")
let flagged_test = match configured_backend() {
DatabaseBackend::Mysql => "LOCATE(CONCAT(CHAR(92), 'Flagged'), flags) > 0",
DatabaseBackend::Pgsql => "strpos(flags, '\\Flagged') > 0",
DatabaseBackend::Sqlite => "instr(flags, '\\Flagged') > 0",
};
format!("CASE WHEN {flagged_test} THEN 0 ELSE 1 END ASC, {base_order}")
} else {
base_order.to_string()
};
let offset = page * per_page;
let sql = crate::rustbox_query_db_messages_002!(&order_by);
let sql = crate::dbq!(db_messages::FORMAT_002)
.replace("{MSG_SELECT_COLS}", MSG_SELECT_COLS)
.replace("{order_by}", &order_by);
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("Failed to prepare sorted get_messages: {e}"))?;
@@ -316,21 +320,15 @@ pub fn get_messages_sorted(
}
pub fn count_messages(conn: &Connection, folder: &str) -> Result<u32, String> {
conn.query_row(
crate::queries::sqlite::queries::db_messages::Q_005,
params![folder],
|row| row.get(0),
)
conn.query_row(crate::dbq!(db_messages::Q_005), params![folder], |row| {
row.get(0)
})
.map_err(|e| format!("Failed to count messages: {e}"))
}
pub fn count_all_messages(conn: &Connection) -> Result<usize, String> {
let count: i64 = conn
.query_row(
crate::queries::sqlite::queries::db_messages::Q_006,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(db_messages::Q_006), [], |row| row.get(0))
.map_err(|e| format!("Failed to count cached messages: {e}"))?;
usize::try_from(count).map_err(|_| "Cached message count is out of range".to_string())
}
@@ -342,7 +340,7 @@ pub fn update_message_flags(
flags_csv: &str,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_messages::Q_007,
crate::dbq!(db_messages::Q_007),
params![flags_csv, folder, uid],
)
.map_err(|e| format!("Failed to update message flags: {e}"))?;
@@ -361,7 +359,7 @@ pub fn cache_message_body(
email_theme: Option<i32>,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_messages::Q_008,
crate::dbq!(db_messages::Q_008),
params![
html,
text,
@@ -383,7 +381,7 @@ pub fn update_email_theme(
email_theme: i32,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_messages::Q_009,
crate::dbq!(db_messages::Q_009),
params![email_theme, folder, uid],
)
.map_err(|e| format!("Failed to update email theme: {e}"))?;
@@ -404,7 +402,7 @@ pub fn get_cached_body(
uid: u32,
) -> Result<Option<CachedBody>, String> {
let result = conn.query_row(
crate::queries::sqlite::queries::db_messages::Q_010,
crate::dbq!(db_messages::Q_010),
params![folder, uid],
|row| {
let cached: i32 = row.get(0)?;
@@ -424,7 +422,7 @@ pub fn get_cached_body(
match result {
Ok(body) => Ok(body),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get cached body: {e}")),
}
}
@@ -434,29 +432,26 @@ pub fn get_single_message(
folder: &str,
uid: u32,
) -> Result<Option<CachedMessage>, String> {
let sql = crate::rustbox_query_db_messages_003!();
let sql = crate::dbq!(db_messages::FORMAT_003).replace("{MSG_SELECT_COLS}", MSG_SELECT_COLS);
let result = conn.query_row(&sql, params![folder, uid], row_to_cached_message);
match result {
Ok(msg) => Ok(Some(msg)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get single message: {e}")),
}
}
pub fn delete_message(conn: &Connection, folder: &str, uid: u32) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_messages::Q_011,
params![folder, uid],
)
.map_err(|e| format!("Failed to delete message: {e}"))?;
conn.execute(crate::dbq!(db_messages::Q_011), params![folder, uid])
.map_err(|e| format!("Failed to delete message: {e}"))?;
Ok(())
}
pub fn get_unindexed_messages(conn: &Connection, limit: u32) -> Result<Vec<(String, u32)>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_messages::Q_012)
.prepare(crate::dbq!(db_messages::Q_012))
.map_err(|e| format!("Failed to prepare get_unindexed_messages: {e}"))?;
let rows = stmt
@@ -477,7 +472,7 @@ pub fn get_all_uids_and_flags(
folder: &str,
) -> Result<Vec<(u32, String)>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_messages::Q_013)
.prepare(crate::dbq!(db_messages::Q_013))
.map_err(|e| format!("Failed to prepare get_all_uids_and_flags: {e}"))?;
let rows = stmt
.query_map(params![folder], |row| {
@@ -496,9 +491,12 @@ pub fn get_thread_messages(
conn: &Connection,
target_message_id: &str,
) -> Result<Vec<CachedMessage>, String> {
let escaped_target = target_message_id.replace("%", "\\%").replace("_", "\\_");
let escaped_target = target_message_id
.replace('!', "!!")
.replace("%", "!%")
.replace("_", "!_");
let like_pattern = format!("%{}%", escaped_target);
let sql = crate::rustbox_query_db_messages_004!();
let sql = crate::dbq!(db_messages::FORMAT_004).replace("{MSG_SELECT_COLS}", MSG_SELECT_COLS);
let mut stmt = conn
.prepare(&sql)
@@ -606,7 +604,7 @@ pub fn search_messages_sqlite(
limit: usize,
) -> Result<Vec<CachedMessage>, String> {
let mut conditions = Vec::new();
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut param_values: Vec<DbValue> = Vec::new();
let mut idx = 1u32;
conditions.push("LOWER(folder) NOT IN ('trash', 'spam', 'junk')".to_string());
@@ -616,19 +614,19 @@ pub fn search_messages_sqlite(
conditions.push(format!(
"(subject LIKE ?{idx} OR from_name LIKE ?{idx} OR from_address LIKE ?{idx} OR to_addresses LIKE ?{idx})"
));
param_values.push(Box::new(pattern));
param_values.push(pattern.to_db_value());
idx += 1;
}
if let Some(subject) = subject.filter(|value| !value.trim().is_empty()) {
conditions.push(format!("subject LIKE ?{idx}"));
param_values.push(Box::new(format!("%{subject}%")));
param_values.push(format!("%{subject}%").to_db_value());
idx += 1;
}
if let Some(f) = folder {
conditions.push(format!("folder = ?{idx}"));
param_values.push(Box::new(f.to_string()));
param_values.push(f.to_db_value());
idx += 1;
}
@@ -643,47 +641,57 @@ pub fn search_messages_sqlite(
} else {
format!("%{f}%")
};
param_values.push(Box::new(val));
param_values.push(val.to_db_value());
idx += 1;
}
if let Some(t) = to {
conditions.push(format!("to_addresses LIKE ?{idx}"));
param_values.push(Box::new(format!("%{t}%")));
param_values.push(format!("%{t}%").to_db_value());
idx += 1;
}
if let Some(df) = date_from {
conditions.push(format!("date_epoch >= ?{idx}"));
param_values.push(Box::new(df));
param_values.push(df.to_db_value());
idx += 1;
}
if let Some(dt) = date_to {
conditions.push(format!("date_epoch <= ?{idx}"));
param_values.push(Box::new(dt));
param_values.push(dt.to_db_value());
idx += 1;
}
if let Some(ha) = has_attachment {
conditions.push(format!("has_attachments = ?{idx}"));
param_values.push(Box::new(ha as i32));
param_values.push((ha as i32).to_db_value());
idx += 1;
}
if let Some(want_unread) = unread {
let seen = match configured_backend() {
DatabaseBackend::Mysql => "LOCATE(CONCAT(CHAR(92), 'Seen'), flags)",
DatabaseBackend::Pgsql => "strpos(flags, '\\Seen')",
DatabaseBackend::Sqlite => "instr(flags, '\\Seen')",
};
conditions.push(if want_unread {
"flags NOT LIKE '%\\Seen%'".to_string()
format!("{seen} = 0")
} else {
"flags LIKE '%\\Seen%'".to_string()
format!("{seen} > 0")
});
}
if let Some(want_flagged) = flagged {
let flagged = match configured_backend() {
DatabaseBackend::Mysql => "LOCATE(CONCAT(CHAR(92), 'Flagged'), flags)",
DatabaseBackend::Pgsql => "strpos(flags, '\\Flagged')",
DatabaseBackend::Sqlite => "instr(flags, '\\Flagged')",
};
conditions.push(if want_flagged {
"flags LIKE '%\\Flagged%'".to_string()
format!("{flagged} > 0")
} else {
"flags NOT LIKE '%\\Flagged%'".to_string()
format!("{flagged} = 0")
});
}
@@ -695,21 +703,19 @@ pub fn search_messages_sqlite(
format!("WHERE {}", conditions.join(" AND "))
};
let sql = crate::rustbox_query_db_messages_005!(&where_clause, param_values.len() + 1);
let sql = format!(
"SELECT {MSG_SELECT_COLS} FROM messages {where_clause} ORDER BY date_epoch DESC LIMIT ?{}",
param_values.len() + 1
);
param_values.push(Box::new(limit as i64));
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
param_values.iter().map(|b| b.as_ref()).collect();
param_values.push((limit as i64).to_db_value());
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("Failed to prepare search query: {e}"))?;
let rows = stmt
.query_map(rusqlite::params_from_iter(params_refs), |row| {
row_to_cached_message(row)
})
.query_map(param_values, row_to_cached_message)
.map_err(|e| format!("Failed to execute search query: {e}"))?;
let mut results = Vec::new();
@@ -720,12 +726,8 @@ pub fn search_messages_sqlite(
}
pub fn cached_storage_bytes(conn: &Connection) -> Result<Option<u64>, String> {
conn.query_row(
crate::queries::sqlite::queries::db_messages::Q_014,
[],
|row| row.get(0),
)
.map_err(|e| e.to_string())
conn.query_row(crate::dbq!(db_messages::Q_014), [], |row| row.get(0))
.map_err(|e| e.to_string())
}
#[cfg(test)]
+32 -41
View File
@@ -1,4 +1,4 @@
use rusqlite::Connection;
use crate::db::pool::{Connection, DbValue, ToDbValue};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
@@ -23,28 +23,24 @@ pub struct UpdateNotificationPreferences {
}
pub fn get_preferences(conn: &Connection) -> Result<NotificationPreferences, String> {
let result = conn.query_row(
crate::queries::sqlite::queries::db_notification_preferences::Q_001,
[],
|row| {
let enabled_int: i32 = row.get(0)?;
let sound_int: i32 = row.get(1)?;
let folders_json: String = row.get(2)?;
let dnd_enabled_int: i32 = row.get(3)?;
let dnd_start: String = row.get(4)?;
let dnd_end: String = row.get(5)?;
let updated_at: String = row.get(6)?;
Ok((
enabled_int,
sound_int,
folders_json,
dnd_enabled_int,
dnd_start,
dnd_end,
updated_at,
))
},
);
let result = conn.query_row(crate::dbq!(db_notification_preferences::Q_001), [], |row| {
let enabled_int: i32 = row.get(0)?;
let sound_int: i32 = row.get(1)?;
let folders_json: String = row.get(2)?;
let dnd_enabled_int: i32 = row.get(3)?;
let dnd_start: String = row.get(4)?;
let dnd_end: String = row.get(5)?;
let updated_at: String = row.get(6)?;
Ok((
enabled_int,
sound_int,
folders_json,
dnd_enabled_int,
dnd_start,
dnd_end,
updated_at,
))
});
match result {
Ok((
@@ -68,7 +64,7 @@ pub fn get_preferences(conn: &Connection) -> Result<NotificationPreferences, Str
updated_at,
})
}
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(NotificationPreferences {
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(NotificationPreferences {
enabled: true,
sound: false,
folders: vec!["INBOX".to_string()],
@@ -85,36 +81,33 @@ pub fn update_preferences(
conn: &Connection,
data: &UpdateNotificationPreferences,
) -> Result<NotificationPreferences, String> {
conn.execute(
crate::queries::sqlite::queries::db_notification_preferences::Q_002,
[],
)
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
conn.execute(crate::dbq!(db_notification_preferences::Q_002), [])
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
let mut sets = Vec::new();
let mut values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut values: Vec<DbValue> = Vec::new();
let mut idx = 1;
if let Some(enabled) = data.enabled {
sets.push(format!("enabled = ?{idx}"));
values.push(Box::new(enabled as i32));
values.push((enabled as i32).to_db_value());
idx += 1;
}
if let Some(sound) = data.sound {
sets.push(format!("sound = ?{idx}"));
values.push(Box::new(sound as i32));
values.push((sound as i32).to_db_value());
idx += 1;
}
if let Some(ref folders) = data.folders {
let folders_json = serde_json::to_string(folders)
.map_err(|e| format!("Failed to serialize folders: {e}"))?;
sets.push(format!("folders = ?{idx}"));
values.push(Box::new(folders_json));
values.push(folders_json.to_db_value());
idx += 1;
}
if let Some(dnd_enabled) = data.dnd_enabled {
sets.push(format!("dnd_enabled = ?{idx}"));
values.push(Box::new(dnd_enabled as i32));
values.push((dnd_enabled as i32).to_db_value());
idx += 1;
}
if let Some(ref dnd_start) = data.dnd_start {
@@ -122,7 +115,7 @@ pub fn update_preferences(
return Err("Invalid DND start time".to_string());
}
sets.push(format!("dnd_start = ?{idx}"));
values.push(Box::new(dnd_start.clone()));
values.push(dnd_start.to_db_value());
idx += 1;
}
if let Some(ref dnd_end) = data.dnd_end {
@@ -130,7 +123,7 @@ pub fn update_preferences(
return Err("Invalid DND end time".to_string());
}
sets.push(format!("dnd_end = ?{idx}"));
values.push(Box::new(dnd_end.clone()));
values.push(dnd_end.to_db_value());
idx += 1;
}
@@ -140,12 +133,10 @@ pub fn update_preferences(
sets.push("updated_at = datetime('now')".to_string());
let set_clause = sets.join(", ");
let sql = crate::rustbox_query_db_notification_preferences_001!(&set_clause, idx);
values.push(Box::new(1_i32));
let sql = format!("UPDATE notification_preferences SET {set_clause} WHERE id = ?{idx}");
values.push(1_i32.to_db_value());
let params_refs: Vec<&dyn rusqlite::types::ToSql> = values.iter().map(|v| v.as_ref()).collect();
conn.execute(&sql, params_refs.as_slice())
conn.execute(&sql, values)
.map_err(|e| format!("Failed to update notification preferences: {e}"))?;
get_preferences(conn)
+14 -14
View File
@@ -1,5 +1,5 @@
use crate::db::pool::{Connection, params};
use crate::queries::sqlite::queries::db_pgp::KEY_COLS;
use rusqlite::{Connection, params};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
@@ -31,7 +31,7 @@ pub struct PgpPreferences {
pub require_trusted_encryption: bool,
}
fn row_to_key(row: &rusqlite::Row<'_>) -> rusqlite::Result<PgpKeyInternal> {
fn row_to_key(row: &crate::db::pool::Row<'_>) -> Result<PgpKeyInternal, crate::db::pool::DbError> {
Ok(PgpKeyInternal {
record: PgpKeyRecord {
fingerprint: row.get(0)?,
@@ -51,7 +51,7 @@ fn row_to_key(row: &rusqlite::Row<'_>) -> rusqlite::Result<PgpKeyInternal> {
pub fn list_keys(conn: &Connection) -> Result<Vec<PgpKeyRecord>, String> {
let mut stmt = conn
.prepare(&crate::rustbox_query_db_pgp_001!())
.prepare(&crate::dbq!(db_pgp::FORMAT_001).replace("{KEY_COLS}", KEY_COLS))
.map_err(|e| format!("Failed to prepare PGP key list: {e}"))?;
let rows = stmt
.query_map([], row_to_key)
@@ -65,17 +65,17 @@ pub fn list_keys(conn: &Connection) -> Result<Vec<PgpKeyRecord>, String> {
pub fn get_key(conn: &Connection, fingerprint: &str) -> Result<Option<PgpKeyInternal>, String> {
let normalized = normalize_fingerprint(fingerprint);
let sql = crate::rustbox_query_db_pgp_002!();
let sql = crate::dbq!(db_pgp::FORMAT_002).replace("{KEY_COLS}", KEY_COLS);
match conn.query_row(&sql, params![normalized], row_to_key) {
Ok(v) => Ok(Some(v)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get PGP key: {e}")),
}
}
pub fn find_keys_by_email(conn: &Connection, email: &str) -> Result<Vec<PgpKeyRecord>, String> {
let mut stmt = conn
.prepare(&crate::rustbox_query_db_pgp_003!())
.prepare(&crate::dbq!(db_pgp::FORMAT_003).replace("{KEY_COLS}", KEY_COLS))
.map_err(|e| format!("Failed to prepare PGP email lookup: {e}"))?;
let rows = stmt
.query_map(params![email.trim()], row_to_key)
@@ -93,7 +93,7 @@ pub fn upsert_key(
secret_key_path: Option<&str>,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_pgp::Q_001,
crate::dbq!(db_pgp::Q_001),
params![
record.fingerprint,
record.key_id,
@@ -113,7 +113,7 @@ pub fn upsert_key(
pub fn delete_key(conn: &Connection, fingerprint: &str) -> Result<bool, String> {
Ok(conn
.execute(
crate::queries::sqlite::queries::db_pgp::Q_002,
crate::dbq!(db_pgp::Q_002),
params![normalize_fingerprint(fingerprint)],
)
.map_err(|e| format!("Failed to delete PGP key: {e}"))?
@@ -126,7 +126,7 @@ pub fn set_trust(conn: &Connection, fingerprint: &str, trust: &str) -> Result<bo
}
Ok(conn
.execute(
crate::queries::sqlite::queries::db_pgp::Q_003,
crate::dbq!(db_pgp::Q_003),
params![normalize_fingerprint(fingerprint), trust],
)
.map_err(|e| format!("Failed to update PGP trust: {e}"))?
@@ -134,7 +134,7 @@ pub fn set_trust(conn: &Connection, fingerprint: &str, trust: &str) -> Result<bo
}
pub fn get_preferences(conn: &Connection) -> Result<PgpPreferences, String> {
conn.query_row(crate::queries::sqlite::queries::db_pgp::Q_004, [], |row| {
conn.query_row(crate::dbq!(db_pgp::Q_004), [], |row| {
Ok(PgpPreferences {
wkd_enabled: row.get::<_, i32>(0)? != 0,
hkp_enabled: row.get::<_, i32>(1)? != 0,
@@ -149,7 +149,7 @@ pub fn update_preferences(
prefs: &PgpPreferences,
) -> Result<PgpPreferences, String> {
conn.execute(
crate::queries::sqlite::queries::db_pgp::Q_005,
crate::dbq!(db_pgp::Q_005),
params![
prefs.wkd_enabled as i32,
prefs.hkp_enabled as i32,
@@ -172,17 +172,17 @@ pub fn find_key_internal_by_email(
conn: &Connection,
email: &str,
) -> Result<Option<PgpKeyInternal>, String> {
let sql = crate::rustbox_query_db_pgp_004!();
let sql = crate::dbq!(db_pgp::FORMAT_004).replace("{KEY_COLS}", KEY_COLS);
match conn.query_row(&sql, params![email.trim()], row_to_key) {
Ok(value) => Ok(Some(value)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to query PGP key by email: {e}")),
}
}
pub fn list_secret_keys(conn: &Connection) -> Result<Vec<PgpKeyInternal>, String> {
let mut stmt = conn
.prepare(&crate::rustbox_query_db_pgp_005!())
.prepare(&crate::dbq!(db_pgp::FORMAT_005).replace("{KEY_COLS}", KEY_COLS))
.map_err(|e| format!("Failed to prepare PGP secret key list: {e}"))?;
let rows = stmt
.query_map([], row_to_key)
+2361 -290
View File
File diff suppressed because it is too large. Load diff
+20 -23
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, params};
use crate::db::pool::{Connection, params};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
@@ -11,7 +11,7 @@ pub struct ResponseTemplate {
pub updated_at: String,
}
fn from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<ResponseTemplate> {
fn from_row(row: &crate::db::pool::Row<'_>) -> Result<ResponseTemplate, crate::db::pool::DbError> {
Ok(ResponseTemplate {
id: row.get(0)?,
name: row.get(1)?,
@@ -24,7 +24,7 @@ fn from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<ResponseTemplate> {
pub fn list(conn: &Connection) -> Result<Vec<ResponseTemplate>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_response_templates::Q_001)
.prepare(crate::dbq!(db_response_templates::Q_001))
.map_err(|e| format!("Failed to prepare response templates: {e}"))?;
let rows = stmt
.query_map([], from_row)
@@ -35,12 +35,12 @@ pub fn list(conn: &Connection) -> Result<Vec<ResponseTemplate>, String> {
pub fn get(conn: &Connection, id: i64) -> Result<Option<ResponseTemplate>, String> {
match conn.query_row(
crate::queries::sqlite::queries::db_response_templates::Q_002,
crate::dbq!(db_response_templates::Q_002),
params![id],
from_row,
) {
Ok(value) => Ok(Some(value)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get response template: {e}")),
}
}
@@ -66,19 +66,19 @@ pub fn create(
"text" | "" => "text",
_ => return Err("Invalid response template format".to_string()),
};
conn.execute(
crate::queries::sqlite::queries::db_response_templates::Q_003,
params![name, body, format],
)
.map_err(|e| {
if e.to_string().contains("UNIQUE constraint failed") {
"A response template with this name already exists".to_string()
} else {
format!("Failed to create response template: {e}")
}
})?;
get(conn, conn.last_insert_rowid())?
.ok_or_else(|| "Failed to read response template".to_string())
let id = conn
.insert_id(
crate::dbq!(db_response_templates::Q_003),
params![name, body, format],
)
.map_err(|e| {
if e.to_string().contains("UNIQUE constraint failed") {
"A response template with this name already exists".to_string()
} else {
format!("Failed to create response template: {e}")
}
})?;
get(conn, id)?.ok_or_else(|| "Failed to read response template".to_string())
}
pub fn update(
@@ -105,7 +105,7 @@ pub fn update(
};
let changed = conn
.execute(
crate::queries::sqlite::queries::db_response_templates::Q_004,
crate::dbq!(db_response_templates::Q_004),
params![name, body, format, id],
)
.map_err(|e| {
@@ -123,10 +123,7 @@ pub fn update(
pub fn delete(conn: &Connection, id: i64) -> Result<bool, String> {
Ok(conn
.execute(
crate::queries::sqlite::queries::db_response_templates::Q_005,
params![id],
)
.execute(crate::dbq!(db_response_templates::Q_005), params![id])
.map_err(|e| format!("Failed to delete response template: {e}"))?
> 0)
}
+13 -16
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, params};
use crate::db::pool::{Connection, params};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
@@ -10,7 +10,7 @@ pub struct SavedSearch {
pub updated_at: String,
}
fn row_to_saved(row: &rusqlite::Row<'_>) -> rusqlite::Result<SavedSearch> {
fn row_to_saved(row: &crate::db::pool::Row<'_>) -> Result<SavedSearch, crate::db::pool::DbError> {
Ok(SavedSearch {
id: row.get(0)?,
name: row.get(1)?,
@@ -22,7 +22,7 @@ fn row_to_saved(row: &rusqlite::Row<'_>) -> rusqlite::Result<SavedSearch> {
pub fn list(conn: &Connection) -> Result<Vec<SavedSearch>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_saved_searches::Q_001)
.prepare(crate::dbq!(db_saved_searches::Q_001))
.map_err(|e| format!("Failed to prepare saved searches: {e}"))?;
let rows = stmt
.query_map([], row_to_saved)
@@ -37,12 +37,12 @@ pub fn create(conn: &Connection, name: &str, query_string: &str) -> Result<Saved
if name.is_empty() || query_string.is_empty() {
return Err("Saved search name and query are required".to_string());
}
conn.execute(
crate::queries::sqlite::queries::db_saved_searches::Q_002,
params![name, query_string],
)
.map_err(|e| format!("Failed to save search: {e}"))?;
let id = conn.last_insert_rowid();
let id = conn
.insert_id(
crate::dbq!(db_saved_searches::Q_002),
params![name, query_string],
)
.map_err(|e| format!("Failed to save search: {e}"))?;
get(conn, id)?.ok_or_else(|| "Failed to read saved search".to_string())
}
@@ -59,7 +59,7 @@ pub fn update(
}
let changed = conn
.execute(
crate::queries::sqlite::queries::db_saved_searches::Q_003,
crate::dbq!(db_saved_searches::Q_003),
params![name, query_string, id],
)
.map_err(|e| format!("Failed to update saved search: {e}"))?;
@@ -71,22 +71,19 @@ pub fn update(
pub fn get(conn: &Connection, id: i64) -> Result<Option<SavedSearch>, String> {
match conn.query_row(
crate::queries::sqlite::queries::db_saved_searches::Q_004,
crate::dbq!(db_saved_searches::Q_004),
params![id],
row_to_saved,
) {
Ok(value) => Ok(Some(value)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get saved search: {e}")),
}
}
pub fn delete(conn: &Connection, id: i64) -> Result<bool, String> {
Ok(conn
.execute(
crate::queries::sqlite::queries::db_saved_searches::Q_005,
params![id],
)
.execute(crate::dbq!(db_saved_searches::Q_005), params![id])
.map_err(|e| format!("Failed to delete saved search: {e}"))?
> 0)
}
+13 -23
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, OptionalExtension, params};
use crate::db::pool::{Connection, OptionalExtension, params};
use serde::{Deserialize, Serialize};
const ROLES: &[&str] = &["sent", "drafts", "trash", "junk", "archive"];
@@ -20,11 +20,9 @@ pub fn get_role(conn: &Connection, role: &str) -> Result<Option<String>, String>
if !valid_role(role) {
return Err(format!("Invalid special folder role: {role}"));
}
conn.query_row(
crate::queries::sqlite::queries::db_special_folders::Q_001,
[role],
|row| row.get(0),
)
conn.query_row(crate::dbq!(db_special_folders::Q_001), [role], |row| {
row.get(0)
})
.optional()
.map_err(|e| format!("Failed to read special folder {role}: {e}"))
}
@@ -46,27 +44,22 @@ pub fn set_role(conn: &Connection, role: &str, folder_name: Option<&str>) -> Res
match folder_name.map(str::trim).filter(|value| !value.is_empty()) {
Some(folder) => {
let exists: bool = conn
.query_row(
crate::queries::sqlite::queries::db_special_folders::Q_002,
[folder],
|row| row.get(0),
)
.query_row(crate::dbq!(db_special_folders::Q_002), [folder], |row| {
row.get(0)
})
.map_err(|e| format!("Failed to validate folder {folder}: {e}"))?;
if !exists {
return Err(format!("Folder does not exist: {folder}"));
}
conn.execute(
crate::queries::sqlite::queries::db_special_folders::Q_003,
crate::dbq!(db_special_folders::Q_003),
params![role, folder],
)
.map_err(|e| format!("Failed to set special folder {role}: {e}"))?;
}
None => {
conn.execute(
crate::queries::sqlite::queries::db_special_folders::Q_004,
[role],
)
.map_err(|e| format!("Failed to clear special folder {role}: {e}"))?;
conn.execute(crate::dbq!(db_special_folders::Q_004), [role])
.map_err(|e| format!("Failed to clear special folder {role}: {e}"))?;
}
}
Ok(())
@@ -87,7 +80,7 @@ pub fn rename_folder_reference(
new_name: &str,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_special_folders::Q_005,
crate::dbq!(db_special_folders::Q_005),
params![new_name, old_name],
)
.map_err(|e| format!("Failed to update special folder reference: {e}"))?;
@@ -95,11 +88,8 @@ pub fn rename_folder_reference(
}
pub fn clear_folder_reference(conn: &Connection, folder_name: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_special_folders::Q_006,
[folder_name],
)
.map_err(|e| format!("Failed to clear special folder reference: {e}"))?;
conn.execute(crate::dbq!(db_special_folders::Q_006), [folder_name])
.map_err(|e| format!("Failed to clear special folder reference: {e}"))?;
Ok(())
}
+26 -43
View File
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use rusqlite::{Connection, params};
use crate::db::pool::{Connection, DbValue, ToDbValue, params};
use serde::Serialize;
use crate::db::messages::CachedMessage;
@@ -33,7 +33,7 @@ pub fn imap_keyword_for_tag_id(id: &str) -> String {
pub fn create_tag(conn: &Connection, id: &str, name: &str, color: &str) -> Result<(), String> {
let imap_keyword = imap_keyword_for_tag_id(id);
conn.execute(
crate::queries::sqlite::queries::db_tags::Q_001,
crate::dbq!(db_tags::Q_001),
params![id, name, color, imap_keyword],
)
.map_err(|e| format!("Failed to create tag: {e}"))?;
@@ -42,7 +42,7 @@ pub fn create_tag(conn: &Connection, id: &str, name: &str, color: &str) -> Resul
pub fn list_tags(conn: &Connection) -> Result<Vec<Tag>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_tags::Q_002)
.prepare(crate::dbq!(db_tags::Q_002))
.map_err(|e| format!("Failed to prepare list_tags: {e}"))?;
let rows = stmt
@@ -76,7 +76,7 @@ pub fn get_message_refs_by_tag(
tag_id: &str,
) -> Result<Vec<(u32, String)>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_tags::Q_003)
.prepare(crate::dbq!(db_tags::Q_003))
.map_err(|e| format!("Failed to prepare tag message refs: {e}"))?;
let rows = stmt
.query_map(params![tag_id], |row| Ok((row.get(0)?, row.get(1)?)))
@@ -146,17 +146,14 @@ pub fn reconcile_imap_keywords(
pub fn update_tag(conn: &Connection, id: &str, name: &str, color: &str) -> Result<bool, String> {
let updated = conn
.execute(
crate::queries::sqlite::queries::db_tags::Q_004,
params![name, color, id],
)
.execute(crate::dbq!(db_tags::Q_004), params![name, color, id])
.map_err(|e| format!("Failed to update tag: {e}"))?;
Ok(updated > 0)
}
pub fn delete_tag(conn: &Connection, id: &str) -> Result<bool, String> {
let deleted = conn
.execute(crate::queries::sqlite::queries::db_tags::Q_005, params![id])
.execute(crate::dbq!(db_tags::Q_005), params![id])
.map_err(|e| format!("Failed to delete tag: {e}"))?;
Ok(deleted > 0)
}
@@ -167,11 +164,8 @@ pub fn add_tag_to_message(
uid: u32,
folder: &str,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_tags::Q_006,
params![tag_id, uid, folder],
)
.map_err(|e| format!("Failed to add tag to message: {e}"))?;
conn.execute(crate::dbq!(db_tags::Q_006), params![tag_id, uid, folder])
.map_err(|e| format!("Failed to add tag to message: {e}"))?;
Ok(())
}
@@ -182,10 +176,7 @@ pub fn remove_tag_from_message(
folder: &str,
) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_tags::Q_007,
params![tag_id, uid, folder],
)
.execute(crate::dbq!(db_tags::Q_007), params![tag_id, uid, folder])
.map_err(|e| format!("Failed to remove tag from message: {e}"))?;
Ok(deleted > 0)
}
@@ -196,7 +187,7 @@ pub fn get_message_tags(
folder: &str,
) -> Result<Vec<MessageTag>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_tags::Q_008)
.prepare(crate::dbq!(db_tags::Q_008))
.map_err(|e| format!("Failed to prepare get_message_tags: {e}"))?;
let rows = stmt
@@ -236,22 +227,22 @@ pub fn get_tags_for_messages(
})
.collect();
let sql = crate::rustbox_query_db_tags_001!(conditions.join(" OR "));
let sql = format!(
"SELECT mt.message_uid, mt.message_folder, t.id, t.name, t.color FROM message_tags mt JOIN tags t ON t.id = mt.tag_id WHERE {} ORDER BY t.name ASC",
conditions.join(" OR ")
);
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("Failed to prepare get_tags_for_messages: {e}"))?;
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut param_values: Vec<DbValue> = Vec::new();
for &(uid, folder) in messages {
param_values.push(Box::new(uid));
param_values.push(Box::new(folder.to_string()));
param_values.push(uid.to_db_value());
param_values.push(folder.to_db_value());
}
let params_ref: Vec<&dyn rusqlite::types::ToSql> =
param_values.iter().map(|p| p.as_ref()).collect();
let rows = stmt
.query_map(params_ref.as_slice(), |row| {
.query_map(param_values, |row| {
let uid: u32 = row.get(0)?;
let folder: String = row.get(1)?;
let tag = MessageTag {
@@ -279,7 +270,7 @@ pub fn get_messages_by_tag(
) -> Result<Vec<CachedMessage>, String> {
let offset = page * per_page;
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_tags::Q_009)
.prepare(crate::dbq!(db_tags::Q_009))
.map_err(|e| format!("Failed to prepare get_messages_by_tag: {e}"))?;
let rows = stmt
@@ -314,11 +305,9 @@ pub fn get_messages_by_tag(
}
pub fn count_messages_by_tag(conn: &Connection, tag_id: &str) -> Result<u32, String> {
conn.query_row(
crate::queries::sqlite::queries::db_tags::Q_010,
params![tag_id],
|row| row.get(0),
)
conn.query_row(crate::dbq!(db_tags::Q_010), params![tag_id], |row| {
row.get(0)
})
.map_err(|e| format!("Failed to count messages by tag: {e}"))
}
@@ -328,16 +317,10 @@ mod tests {
use crate::db::pool::open_test_db;
fn insert_test_message(conn: &Connection, folder: &str, uid: u32) {
conn.execute(
crate::queries::sqlite::queries::db_tags::Q_011,
params![folder],
)
.unwrap();
conn.execute(
crate::queries::sqlite::queries::db_tags::Q_012,
params![uid, folder],
)
.unwrap();
conn.execute(crate::dbq!(db_tags::Q_011), params![folder])
.unwrap();
conn.execute(crate::dbq!(db_tags::Q_012), params![uid, folder])
.unwrap();
}
#[test]
+6 -12
View File
@@ -1,8 +1,8 @@
use rusqlite::Connection;
use crate::db::pool::Connection;
pub fn list(conn: &Connection) -> Result<Vec<String>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_trusted_senders::Q_001)
.prepare(crate::dbq!(db_trusted_senders::Q_001))
.map_err(|e| format!("Failed to prepare trusted sender list: {e}"))?;
let rows = stmt
.query_map([], |row| row.get::<_, String>(0))
@@ -12,20 +12,14 @@ pub fn list(conn: &Connection) -> Result<Vec<String>, String> {
}
pub fn add(conn: &Connection, email: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_trusted_senders::Q_002,
[email],
)
.map_err(|e| format!("Failed to trust sender: {e}"))?;
conn.execute(crate::dbq!(db_trusted_senders::Q_002), [email])
.map_err(|e| format!("Failed to trust sender: {e}"))?;
Ok(())
}
pub fn remove(conn: &Connection, email: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_trusted_senders::Q_003,
[email],
)
.map_err(|e| format!("Failed to remove trusted sender: {e}"))?;
conn.execute(crate::dbq!(db_trusted_senders::Q_003), [email])
.map_err(|e| format!("Failed to remove trusted sender: {e}"))?;
Ok(())
}
+3
View File
@@ -54,6 +54,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
format!("invalid RustBox configuration: {message}"),
)
})?;
db::pool::configure(&loaded_config)
.await
.map_err(std::io::Error::other)?;
let config = Arc::new(loaded_config);
let persistence = session_store::from_config(&config)
+1 -1
View File
@@ -126,7 +126,7 @@ async fn https_get_address(
format!("{host}:{port}")
};
let request = format!(
"GET {target} HTTP/1.1\r\nHost: {host_header}\r\nUser-Agent: RustBox/0.1.0\r\nAccept: application/octet-stream, application/pgp-keys, text/plain\r\nConnection: close\r\n\r\n"
"GET {target} HTTP/1.1\r\nHost: {host_header}\r\nUser-Agent: RustBox/0.1.1\r\nAccept: application/octet-stream, application/pgp-keys, text/plain\r\nConnection: close\r\n\r\n"
);
timeout(duration, stream.write_all(request.as_bytes()))
.await
+71 -70
View File
@@ -11,7 +11,7 @@ pub mod db_calendar {
working_hours_end, time_format, zoom_link, teams_link, meet_link, caldav_url, \
caldav_username, caldav_enabled, updated_at";
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_001: &str = "SELECT CAST(CURRENT_TIMESTAMP AS CHAR)";
pub const Q_002: &str = "INSERT INTO calendar_events
(id, title, description, location, start_time, end_time, all_day,
recurrence_rule, attendees, organizer, status, source, source_uid,
@@ -45,7 +45,7 @@ pub mod db_calendar {
pub mod db_contact_groups {
pub const Q_001: &str = "INSERT INTO contact_groups (id, name) VALUES (?, ?)";
pub const Q_002: &str = "SELECT g.id, g.name, g.created_at, g.updated_at,
(SELECT COUNT(*) FROM contact_group_members m WHERE m.group_id = g.id) AS member_count
(SELECT CAST(COUNT(*) AS SIGNED) FROM contact_group_members m WHERE m.group_id = g.id) AS member_count
FROM contact_groups g
ORDER BY g.name ASC";
pub const Q_003: &str =
@@ -62,7 +62,7 @@ pub mod db_contact_groups {
WHERE m.group_id = ?
ORDER BY c.name ASC";
pub const Q_008: &str = "SELECT g.id, g.name, g.created_at, g.updated_at,
(SELECT COUNT(*) FROM contact_group_members m2 WHERE m2.group_id = g.id) AS member_count
(SELECT CAST(COUNT(*) AS SIGNED) FROM contact_group_members m2 WHERE m2.group_id = g.id) AS member_count
FROM contact_groups g
JOIN contact_group_members m ON m.group_id = g.id
WHERE m.contact_id = ?
@@ -75,29 +75,30 @@ pub mod db_contacts {
(id, email, name, company, notes, phone, job_title, address, website, birthday, is_favorite, last_contacted,
contact_count, source, created_at, updated_at, emails_json, phones_json, addresses_json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
email=excluded.email, name=excluded.name, company=excluded.company, notes=excluded.notes,
phone=excluded.phone, job_title=excluded.job_title, address=excluded.address, website=excluded.website, birthday=excluded.birthday,
is_favorite=excluded.is_favorite, last_contacted=excluded.last_contacted, contact_count=excluded.contact_count,
source=excluded.source, updated_at=excluded.updated_at, emails_json=excluded.emails_json,
phones_json=excluded.phones_json, addresses_json=excluded.addresses_json";
ON DUPLICATE KEY UPDATE
email=VALUES(email), name=VALUES(name), company=VALUES(company), notes=VALUES(notes),
phone=VALUES(phone), job_title=VALUES(job_title), address=VALUES(address), website=VALUES(website), birthday=VALUES(birthday),
is_favorite=VALUES(is_favorite), last_contacted=VALUES(last_contacted), contact_count=VALUES(contact_count),
source=VALUES(source), updated_at=VALUES(updated_at), emails_json=VALUES(emails_json),
phones_json=VALUES(phones_json), addresses_json=VALUES(addresses_json)";
pub const Q_002: &str = "DELETE FROM contacts WHERE id = ?";
pub const Q_003: &str = r#"
SELECT email, name
FROM known_addresses
WHERE (email LIKE ? ESCAPE '\' OR name LIKE ? ESCAPE '\')
WHERE (email LIKE ?1 ESCAPE '!' OR name LIKE ?1 ESCAPE '!')
AND email NOT IN (SELECT email FROM contacts)
AND NOT EXISTS (
SELECT 1 FROM contacts c, json_each(c.emails_json) alt
SELECT 1 FROM contacts c
JOIN JSON_TABLE(COALESCE(NULLIF(c.emails_json, ''), '[]'), '$[*]' COLUMNS(value VARCHAR(320) PATH '$')) alt
WHERE lower(alt.value) = lower(known_addresses.email)
)
ORDER BY email ASC
LIMIT ?
LIMIT ?2
"#;
pub const Q_004: &str = "INSERT INTO known_addresses (email, name) VALUES (?, ?)
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != ''";
ON DUPLICATE KEY UPDATE name = IF(VALUES(name) <> '', VALUES(name), name)";
pub const Q_005: &str = "INSERT INTO known_addresses (email, name) VALUES (?, ?)
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != ''";
ON DUPLICATE KEY UPDATE name = IF(VALUES(name) <> '', VALUES(name), name)";
pub const Q_006: &str = "UPDATE contacts
SET contact_count = contact_count + 1,
last_contacted = CURRENT_TIMESTAMP,
@@ -105,24 +106,24 @@ pub mod db_contacts {
WHERE email = ?";
pub const Q_007: &str = "INSERT IGNORE INTO contacts (id, email, name, source)
VALUES (?, ?, ?, 'auto')";
pub const Q_008: &str = "INSERT INTO contact_photos (contact_id, content_type, data, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(contact_id) DO UPDATE SET content_type=excluded.content_type, data=excluded.data, updated_at=CURRENT_TIMESTAMP";
pub const Q_008: &str = "INSERT INTO contact_photos (contact_id, content_type, data, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE content_type=VALUES(content_type), data=VALUES(data), updated_at=CURRENT_TIMESTAMP";
pub const Q_009: &str = "SELECT content_type, data FROM contact_photos WHERE contact_id = ?";
pub const Q_010: &str = "DELETE FROM contact_photos WHERE contact_id = ?";
pub const FORMAT_001: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts WHERE id = ?";
pub const FORMAT_002: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts \
WHERE lower(email) = lower(?) \
OR EXISTS (SELECT 1 FROM json_each(contacts.emails_json) WHERE lower(value) = lower(?))";
WHERE lower(email) = lower(?1) \
OR EXISTS (SELECT 1 FROM JSON_TABLE(COALESCE(NULLIF(contacts.emails_json, ''), '[]'), '$[*]' COLUMNS(value VARCHAR(320) PATH '$')) alt WHERE lower(alt.value) = lower(?1))";
pub const FORMAT_003: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ? ESCAPE '\\' OR email LIKE ? ESCAPE '\\' OR emails_json LIKE ? ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ? OFFSET ?";
LIMIT ?2 OFFSET ?3";
pub const FORMAT_004: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
ORDER BY name ASC
LIMIT ? OFFSET ?";
pub const FORMAT_005: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ? ESCAPE '\\' OR email LIKE ? ESCAPE '\\' OR emails_json LIKE ? ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?";
LIMIT ?2";
}
pub mod db_display_preferences {
@@ -134,29 +135,28 @@ pub mod db_display_preferences {
pub mod db_drafts {
pub const Q_001: &str = "INSERT INTO drafts (id, to_addresses, cc_addresses, bcc_addresses, subject, text_body, html_body, in_reply_to, references_header, request_mdn, priority, compose_format, pgp_mode, pgp_inline, pgp_attach_public_key, pgp_attach_recipient_keys, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(id) DO UPDATE SET
to_addresses = excluded.to_addresses,
cc_addresses = excluded.cc_addresses,
bcc_addresses = excluded.bcc_addresses,
subject = excluded.subject,
text_body = excluded.text_body,
html_body = excluded.html_body,
in_reply_to = excluded.in_reply_to,
references_header = excluded.references_header,
request_mdn = excluded.request_mdn,
priority = excluded.priority,
compose_format = excluded.compose_format,
pgp_mode = excluded.pgp_mode,
pgp_inline = excluded.pgp_inline,
pgp_attach_public_key = excluded.pgp_attach_public_key,
pgp_attach_recipient_keys = excluded.pgp_attach_recipient_keys,
ON DUPLICATE KEY UPDATE
to_addresses = VALUES(to_addresses),
cc_addresses = VALUES(cc_addresses),
bcc_addresses = VALUES(bcc_addresses),
subject = VALUES(subject),
text_body = VALUES(text_body),
html_body = VALUES(html_body),
in_reply_to = VALUES(in_reply_to),
references_header = VALUES(references_header),
request_mdn = VALUES(request_mdn),
priority = VALUES(priority),
compose_format = VALUES(compose_format),
pgp_mode = VALUES(pgp_mode),
pgp_inline = VALUES(pgp_inline),
pgp_attach_public_key = VALUES(pgp_attach_public_key),
pgp_attach_recipient_keys = VALUES(pgp_attach_recipient_keys),
updated_at = CURRENT_TIMESTAMP";
pub const Q_002: &str = "SELECT id, to_addresses, cc_addresses, bcc_addresses, subject, text_body, html_body, in_reply_to, references_header, request_mdn, priority, compose_format, pgp_mode, pgp_inline, pgp_attach_public_key, pgp_attach_recipient_keys, remote_folder, remote_uid, remote_uid_validity, created_at, updated_at
FROM drafts WHERE id = ?";
pub const Q_003: &str = "SELECT id, to_addresses, cc_addresses, bcc_addresses, subject, text_body, html_body, in_reply_to, references_header, request_mdn, priority, compose_format, pgp_mode, pgp_inline, pgp_attach_public_key, pgp_attach_recipient_keys, remote_folder, remote_uid, remote_uid_validity, created_at, updated_at
FROM drafts ORDER BY updated_at DESC";
pub const Q_004: &str =
"UPDATE drafts SET remote_folder = ?, remote_uid = ?, remote_uid_validity = ? WHERE id = ?";
pub const Q_004: &str = "UPDATE drafts SET remote_folder = ?2, remote_uid = ?3, remote_uid_validity = ?4 WHERE id = ?1";
pub const Q_005: &str = "DELETE FROM drafts WHERE id = ?";
pub const Q_006: &str =
"INSERT INTO draft_attachments (id, draft_id, filename, content_type, size, file_path)
@@ -173,7 +173,7 @@ pub mod db_folder_preferences {
pub const Q_003: &str = "SELECT EXISTS(SELECT 1 FROM folders WHERE name = ?)";
pub const Q_004: &str = "SELECT flagged_on_top FROM mail_preferences WHERE id = 1";
pub const Q_005: &str = "SELECT EXISTS(SELECT 1 FROM folders WHERE name = ?)";
pub const Q_006: &str = "INSERT INTO folder_preferences(folder_name, flagged_on_top, updated_at) VALUES(?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT(folder_name) DO UPDATE SET flagged_on_top = excluded.flagged_on_top, updated_at = CURRENT_TIMESTAMP";
pub const Q_006: &str = "INSERT INTO folder_preferences(folder_name, flagged_on_top, updated_at) VALUES(?, ?, CURRENT_TIMESTAMP)\n ON DUPLICATE KEY UPDATE flagged_on_top = VALUES(flagged_on_top), updated_at = CURRENT_TIMESTAMP";
pub const Q_007: &str = "DELETE FROM folder_preferences WHERE folder_name = ?";
}
@@ -196,17 +196,16 @@ pub mod db_folders {
pub const Q_007: &str =
"UPDATE folders SET uid_validity = ?, total_count = ?, highest_modseq = ?
WHERE name = ?";
pub const Q_008: &str =
"SELECT COUNT(*) FROM messages WHERE folder = ? AND flags NOT LIKE '%\\Seen%'";
pub const Q_008: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages WHERE folder = ? AND LOCATE(CONCAT(CHAR(92), 'Seen'), flags) = 0";
pub const Q_009: &str = "UPDATE folders SET unread_count = ? WHERE name = ?";
pub const Q_010: &str = "UPDATE folders SET unread_count = GREATEST(0, CAST(unread_count AS SIGNED) + ?) WHERE name = ?";
pub const Q_011: &str = "SELECT EXISTS(
SELECT 1 FROM folders
WHERE updated_at > datetime('now', ?)
WHERE updated_at > DATE_FORMAT(DATE_SUB(CURRENT_TIMESTAMP, INTERVAL ? SECOND), '%Y-%m-%d %H:%i:%s')
)";
pub const Q_012: &str = "SELECT EXISTS(
SELECT 1 FROM folders
WHERE name = ? AND messages_updated_at > datetime('now', ?)
WHERE name = ? AND messages_updated_at > DATE_FORMAT(DATE_SUB(CURRENT_TIMESTAMP, INTERVAL ? SECOND), '%Y-%m-%d %H:%i:%s')
)";
pub const Q_013: &str = "SELECT messages_updated_at IS NULL FROM folders WHERE name = ?";
pub const Q_014: &str = "UPDATE folders SET messages_updated_at = NULL WHERE name = ?";
@@ -216,7 +215,7 @@ pub mod db_folders {
WHERE name = ?";
pub const Q_016: &str = "DELETE FROM messages WHERE folder = ?";
pub const Q_017: &str = "DELETE FROM folders WHERE name = ?";
pub const Q_018: &str = "PRAGMA defer_foreign_keys = ON;";
pub const Q_018: &str = "SELECT 1";
pub const Q_019: &str = "INSERT INTO folders (name, delimiter, parent, flags, is_subscribed, total_count, unread_count, uid_validity, highest_modseq, updated_at, messages_updated_at)
SELECT ?, delimiter, parent, flags, is_subscribed, total_count, unread_count, uid_validity, highest_modseq, CURRENT_TIMESTAMP, messages_updated_at
FROM folders WHERE name = ?";
@@ -237,14 +236,14 @@ pub mod db_header_sync {
pub const Q_001: &str = "SELECT value FROM user_meta WHERE key = 'header_sync_generation'";
pub const Q_002: &str =
"INSERT INTO user_meta (key, value) VALUES ('header_sync_generation', '1')
ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1";
ON DUPLICATE KEY UPDATE value = CAST(value AS SIGNED) + 1";
}
pub mod db_identities {
pub const IDENTITY_SELECT_COLS: &str = "id, display_name, email, signature_html, is_default, created_at, updated_at, reply_to, bcc, organization, signature_text, signature_new, signature_reply, signature_forward, signature_position, pgp_fingerprint, pgp_sign_default, pgp_encrypt_default, pgp_inline_default";
pub const Q_001: &str = "INSERT INTO identities (display_name,email,signature_html,is_default,reply_to,bcc,organization,signature_text,signature_new,signature_reply,signature_forward,signature_position,pgp_fingerprint,pgp_sign_default,pgp_encrypt_default,pgp_inline_default) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
pub const Q_002: &str = "DELETE FROM identities WHERE id = ?";
pub const Q_003: &str = "SELECT COUNT(*) FROM identities";
pub const Q_003: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM identities";
pub const Q_004: &str =
"UPDATE identities SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE is_default = 1";
pub const FORMAT_001: &str =
@@ -273,8 +272,8 @@ pub mod db_messages {
pub const Q_004: &str = "SELECT uid, folder, subject, from_address, from_name, to_addresses, \
COALESCE(NULLIF(body_text, ''), snippet, ''), date_epoch, has_attachments \
FROM messages ORDER BY folder, uid";
pub const Q_005: &str = "SELECT COUNT(*) FROM messages WHERE folder = ?";
pub const Q_006: &str = "SELECT COUNT(*) FROM messages";
pub const Q_005: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages WHERE folder = ?";
pub const Q_006: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages";
pub const Q_007: &str = "UPDATE messages SET flags = ? WHERE folder = ? AND uid = ?";
pub const Q_008: &str = "UPDATE messages
SET body_html = ?, body_text = ?, body_cached = 1,
@@ -291,7 +290,7 @@ pub mod db_messages {
ORDER BY date_epoch DESC
LIMIT ?";
pub const Q_013: &str = "SELECT uid, flags FROM messages WHERE folder = ?";
pub const Q_014: &str = "SELECT SUM(size) FROM messages";
pub const Q_014: &str = "SELECT CAST(SUM(size) AS SIGNED) FROM messages";
pub const FORMAT_001: &str = "SELECT {MSG_SELECT_COLS}
FROM messages
WHERE folder = ?
@@ -307,9 +306,9 @@ pub mod db_messages {
WHERE folder = ? AND uid = ?";
pub const FORMAT_004: &str = "SELECT {MSG_SELECT_COLS}
FROM messages
WHERE message_id = ?
OR in_reply_to = ?
OR references_header LIKE ? ESCAPE '\\'
WHERE message_id = ?1
OR in_reply_to = ?1
OR references_header LIKE ?2 ESCAPE '!'
ORDER BY date_epoch ASC";
pub const FORMAT_005: &str = "SELECT {MSG_SELECT_COLS}
FROM messages
@@ -326,9 +325,9 @@ pub mod db_notification_preferences {
pub mod db_pgp {
pub const KEY_COLS: &str = "fingerprint,key_id,email,user_id,public_key_armor,secret_key_path,has_secret,trust,source,created_at,updated_at";
pub const Q_001: &str = "INSERT INTO pgp_keys (fingerprint,key_id,email,user_id,public_key_armor,secret_key_path,has_secret,trust,source) VALUES (?,?,?,?,?,?,?,?,?)\n ON CONFLICT(fingerprint) DO UPDATE SET key_id=excluded.key_id,email=CASE WHEN excluded.email='' THEN pgp_keys.email ELSE excluded.email END,user_id=CASE WHEN excluded.user_id='' THEN pgp_keys.user_id ELSE excluded.user_id END,public_key_armor=excluded.public_key_armor,secret_key_path=COALESCE(excluded.secret_key_path,pgp_keys.secret_key_path),has_secret=MAX(pgp_keys.has_secret,excluded.has_secret),trust=excluded.trust,source=excluded.source,updated_at=CURRENT_TIMESTAMP";
pub const Q_001: &str = "INSERT INTO pgp_keys (fingerprint,key_id,email,user_id,public_key_armor,secret_key_path,has_secret,trust,source) VALUES (?,?,?,?,?,?,?,?,?)\n ON DUPLICATE KEY UPDATE key_id=VALUES(key_id),email=CASE WHEN VALUES(email)='' THEN email ELSE VALUES(email) END,user_id=CASE WHEN VALUES(user_id)='' THEN user_id ELSE VALUES(user_id) END,public_key_armor=VALUES(public_key_armor),secret_key_path=COALESCE(VALUES(secret_key_path),secret_key_path),has_secret=GREATEST(has_secret,VALUES(has_secret)),trust=VALUES(trust),source=VALUES(source),updated_at=CURRENT_TIMESTAMP";
pub const Q_002: &str = "DELETE FROM pgp_keys WHERE replace(upper(fingerprint),' ','')=?";
pub const Q_003: &str = "UPDATE pgp_keys SET trust=?,updated_at=CURRENT_TIMESTAMP WHERE replace(upper(fingerprint),' ','')=?";
pub const Q_003: &str = "UPDATE pgp_keys SET trust=?2,updated_at=CURRENT_TIMESTAMP WHERE replace(upper(fingerprint),' ','')=?1";
pub const Q_004: &str =
"SELECT wkd_enabled,hkp_enabled,require_trusted_encryption FROM pgp_preferences WHERE id=1";
pub const Q_005: &str = "UPDATE pgp_preferences SET wkd_enabled=?,hkp_enabled=?,require_trusted_encryption=?,updated_at=CURRENT_TIMESTAMP WHERE id=1";
@@ -343,11 +342,12 @@ pub mod db_pgp {
pub mod db_pool {
pub const Q_001: &str = "SELECT 0";
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_002: &str = "SELECT CAST(CURRENT_TIMESTAMP AS CHAR)";
pub const Q_003: &str = "SELECT 1";
pub const Q_004: &str = "SELECT 1";
pub const Q_005: &str = "SHOW TABLES";
pub const Q_006: &str = "SELECT 1";
pub const Q_007: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM information_schema.tables WHERE table_schema = DATABASE()";
}
pub mod db_response_templates {
@@ -374,7 +374,7 @@ pub mod db_saved_searches {
pub mod db_special_folders {
pub const Q_001: &str = "SELECT folder_name FROM special_folders WHERE role = ?";
pub const Q_002: &str = "SELECT EXISTS(SELECT 1 FROM folders WHERE name = ?)";
pub const Q_003: &str = "INSERT INTO special_folders(role, folder_name, updated_at) VALUES(?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT(role) DO UPDATE SET folder_name = excluded.folder_name, updated_at = CURRENT_TIMESTAMP";
pub const Q_003: &str = "INSERT INTO special_folders(role, folder_name, updated_at) VALUES(?, ?, CURRENT_TIMESTAMP)\n ON DUPLICATE KEY UPDATE folder_name = VALUES(folder_name), updated_at = CURRENT_TIMESTAMP";
pub const Q_004: &str = "DELETE FROM special_folders WHERE role = ?";
pub const Q_005: &str = "UPDATE special_folders SET folder_name = ?, updated_at = CURRENT_TIMESTAMP WHERE folder_name = ?";
pub const Q_006: &str = "DELETE FROM special_folders WHERE folder_name = ?";
@@ -384,7 +384,7 @@ pub mod db_tags {
pub const Q_001: &str = "INSERT INTO tags (id, name, color, imap_keyword) VALUES (?, ?, ?, ?)";
pub const Q_002: &str =
"SELECT t.id, t.name, t.color, t.imap_keyword, t.created_at, t.updated_at,
(SELECT COUNT(*) FROM message_tags mt WHERE mt.tag_id = t.id) AS message_count
(SELECT CAST(COUNT(*) AS SIGNED) FROM message_tags mt WHERE mt.tag_id = t.id) AS message_count
FROM tags t
ORDER BY t.name ASC";
pub const Q_003: &str = "SELECT message_uid, message_folder FROM message_tags WHERE tag_id = ?";
@@ -409,7 +409,7 @@ pub mod db_tags {
WHERE mt.tag_id = ?
ORDER BY m.date_epoch DESC
LIMIT ? OFFSET ?";
pub const Q_010: &str = "SELECT COUNT(*) FROM message_tags WHERE tag_id = ?";
pub const Q_010: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM message_tags WHERE tag_id = ?";
pub const Q_011: &str = "INSERT IGNORE INTO folders (name) VALUES (?)";
pub const Q_012: &str = "INSERT INTO messages (uid, folder, subject, from_address, from_name, to_addresses, date, flags, size, has_attachments, snippet)
VALUES (?, ?, 'Test', 'test@ex.com', 'Test', '[]', '2024-01-01', '', 100, 0, '')";
@@ -427,23 +427,23 @@ pub mod db_trusted_senders {
}
pub mod routes_contacts {
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_003: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_001: &str = "SELECT CAST(CURRENT_TIMESTAMP AS CHAR)";
pub const Q_002: &str = "SELECT CAST(CURRENT_TIMESTAMP AS CHAR)";
pub const Q_003: &str = "SELECT CAST(CURRENT_TIMESTAMP AS CHAR)";
}
pub mod routes_diagnostics {
pub const Q_001: &str = "SELECT COUNT(*),
COALESCE(SUM(CASE WHEN messages_updated_at IS NULL THEN 1 ELSE 0 END), 0),
CASE WHEN MAX(updated_at) IS NULL THEN NULL ELSE replace(MAX(updated_at), ' ', 'T') || 'Z' END,
CASE WHEN MAX(messages_updated_at) IS NULL THEN NULL ELSE replace(MAX(messages_updated_at), ' ', 'T') || 'Z' END
pub const Q_001: &str = "SELECT CAST(COUNT(*) AS SIGNED),
CAST(COALESCE(SUM(CASE WHEN messages_updated_at IS NULL THEN 1 ELSE 0 END), 0) AS SIGNED),
CASE WHEN MAX(updated_at) IS NULL THEN NULL ELSE CONCAT(replace(MAX(updated_at), ' ', 'T'), 'Z') END,
CASE WHEN MAX(messages_updated_at) IS NULL THEN NULL ELSE CONCAT(replace(MAX(messages_updated_at), ' ', 'T'), 'Z') END
FROM folders";
pub const Q_002: &str = "SELECT COUNT(*) FROM messages";
pub const Q_003: &str = "SELECT COUNT(*) FROM messages WHERE body_cached = 1";
pub const Q_002: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages";
pub const Q_003: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages WHERE body_cached = 1";
}
pub mod routes_folder_mgmt {
pub const Q_001: &str = "UPDATE messages SET flags = CASE WHEN flags = '' THEN ? ELSE flags || ',' || ? END WHERE folder = ? AND instr(flags, ?) = 0";
pub const Q_001: &str = "UPDATE messages SET flags = CASE WHEN flags = '' THEN ?2 ELSE CONCAT(flags, ',', ?2) END WHERE folder = ?1 AND LOCATE(?2, flags) = 0";
pub const Q_002: &str = "UPDATE folders SET unread_count = 0 WHERE name = ?";
pub const Q_003: &str = "SELECT uid FROM messages WHERE folder = ?";
pub const Q_004: &str = "UPDATE folders SET total_count = 0, unread_count = 0, messages_updated_at = CURRENT_TIMESTAMP WHERE name = ?";
@@ -479,7 +479,8 @@ pub mod sessions {
"SELECT * FROM sessions WHERE browser_id = ? ORDER BY created_at_unix";
pub const BY_USER: &str =
"SELECT * FROM sessions WHERE user_hash = ? ORDER BY last_accessed_unix DESC";
pub const BROWSER_EXISTS: &str = "SELECT COUNT(*) FROM sessions WHERE browser_id = ?";
pub const BROWSER_EXISTS: &str =
"SELECT CAST(COUNT(*) AS SIGNED) FROM sessions WHERE browser_id = ?";
pub const DELETE_ACCOUNT: &str = "DELETE FROM sessions WHERE account_id = ?";
pub const DELETE_BROWSER: &str = "DELETE FROM sessions WHERE browser_id = ?";
pub const DELETE_EXPIRED: &str =
+72 -57
View File
@@ -10,7 +10,7 @@ pub mod db_calendar {
pub const SETTINGS_SELECT_COLS: &str = "default_view, week_starts_on, working_hours_start, \
working_hours_end, time_format, zoom_link, teams_link, meet_link, caldav_url, \
caldav_username, caldav_enabled, updated_at";
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP::text";
pub const Q_002: &str = "INSERT INTO calendar_events
(id, title, description, location, start_time, end_time, all_day,
recurrence_rule, attendees, organizer, status, source, source_uid,
@@ -18,8 +18,10 @@ pub mod db_calendar {
reminder_minutes, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)";
pub const Q_003: &str = "DELETE FROM calendar_events WHERE id = $1";
pub const Q_004: &str = "INSERT OR IGNORE INTO calendar_settings (id) VALUES (1)";
pub const Q_005: &str = "INSERT OR IGNORE INTO calendar_settings (id) VALUES (1)";
pub const Q_004: &str =
"INSERT INTO calendar_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING";
pub const Q_005: &str =
"INSERT INTO calendar_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING";
pub const Q_006: &str = "SELECT id, name, url_template, icon, is_default, created_at
FROM meeting_templates ORDER BY is_default DESC, name ASC";
pub const Q_007: &str =
@@ -48,10 +50,9 @@ pub mod db_contact_groups {
FROM contact_groups g
ORDER BY g.name ASC";
pub const Q_003: &str =
"UPDATE contact_groups SET name = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2";
"UPDATE contact_groups SET name = $1, updated_at = CURRENT_TIMESTAMP::text WHERE id = $2";
pub const Q_004: &str = "DELETE FROM contact_groups WHERE id = $1";
pub const Q_005: &str =
"INSERT OR IGNORE INTO contact_group_members (group_id, contact_id) VALUES ($1, $2)";
pub const Q_005: &str = "INSERT INTO contact_group_members (group_id, contact_id) VALUES ($1, $2) ON CONFLICT (group_id, contact_id) DO NOTHING";
pub const Q_006: &str =
"DELETE FROM contact_group_members WHERE group_id = $1 AND contact_id = $2";
pub const Q_007: &str = "SELECT c.id, c.email, c.name, c.company, c.notes, c.phone, c.job_title, c.address, c.website, c.birthday, c.is_favorite,
@@ -84,10 +85,11 @@ pub mod db_contacts {
pub const Q_003: &str = r#"
SELECT email, name
FROM known_addresses
WHERE (email LIKE $1 ESCAPE '\' OR name LIKE $1 ESCAPE '\')
WHERE (email LIKE $1 ESCAPE '!' OR name LIKE $1 ESCAPE '!')
AND email NOT IN (SELECT email FROM contacts)
AND NOT EXISTS (
SELECT 1 FROM contacts c, json_each(c.emails_json) alt
SELECT 1 FROM contacts c
CROSS JOIN LATERAL jsonb_array_elements_text(COALESCE(NULLIF(c.emails_json, ''), '[]')::jsonb) AS alt(value)
WHERE lower(alt.value) = lower(known_addresses.email)
)
ORDER BY email ASC
@@ -99,40 +101,41 @@ pub mod db_contacts {
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != ''";
pub const Q_006: &str = "UPDATE contacts
SET contact_count = contact_count + 1,
last_contacted = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
last_contacted = CURRENT_TIMESTAMP::text,
updated_at = CURRENT_TIMESTAMP::text
WHERE email = $1";
pub const Q_007: &str = "INSERT OR IGNORE INTO contacts (id, email, name, source)
VALUES ($1, $2, $3, 'auto')";
pub const Q_008: &str = "INSERT INTO contact_photos (contact_id, content_type, data, updated_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP) ON CONFLICT(contact_id) DO UPDATE SET content_type=excluded.content_type, data=excluded.data, updated_at=CURRENT_TIMESTAMP";
pub const Q_007: &str = "INSERT INTO contacts (id, email, name, source)
VALUES ($1, $2, $3, 'auto') ON CONFLICT (id) DO NOTHING";
pub const Q_008: &str = "INSERT INTO contact_photos (contact_id, content_type, data, updated_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP::text) ON CONFLICT(contact_id) DO UPDATE SET content_type=excluded.content_type, data=excluded.data, updated_at=CURRENT_TIMESTAMP::text";
pub const Q_009: &str = "SELECT content_type, data FROM contact_photos WHERE contact_id = $1";
pub const Q_010: &str = "DELETE FROM contact_photos WHERE contact_id = $1";
pub const FORMAT_001: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts WHERE id = $1";
pub const FORMAT_002: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts \
WHERE lower(email) = lower($1) \
OR EXISTS (SELECT 1 FROM json_each(contacts.emails_json) WHERE lower(value) = lower($1))";
OR EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(NULLIF(contacts.emails_json, ''), '[]')::jsonb) AS alt(value) WHERE lower(alt.value) = lower($1))";
pub const FORMAT_003: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE $1 ESCAPE '\\' OR email LIKE $1 ESCAPE '\\' OR emails_json LIKE $1 ESCAPE '\\'
WHERE name LIKE $1 ESCAPE '!' OR email LIKE $1 ESCAPE '!' OR emails_json LIKE $1 ESCAPE '!'
ORDER BY name ASC
LIMIT $2 OFFSET $3";
pub const FORMAT_004: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
ORDER BY name ASC
LIMIT $1 OFFSET $2";
pub const FORMAT_005: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE $1 ESCAPE '\\' OR email LIKE $1 ESCAPE '\\' OR emails_json LIKE $1 ESCAPE '\\'
WHERE name LIKE $1 ESCAPE '!' OR email LIKE $1 ESCAPE '!' OR emails_json LIKE $1 ESCAPE '!'
ORDER BY name ASC
LIMIT $2";
}
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_002: &str = "INSERT OR IGNORE INTO display_preferences (id) VALUES (1)";
pub const Q_002: &str =
"INSERT INTO display_preferences (id) VALUES (1) ON CONFLICT (id) DO NOTHING";
pub const FORMAT_001: &str = "UPDATE display_preferences SET {} WHERE id = ${}";
}
pub mod db_drafts {
pub const Q_001: &str = "INSERT INTO drafts (id, to_addresses, cc_addresses, bcc_addresses, subject, text_body, html_body, in_reply_to, references_header, request_mdn, priority, compose_format, pgp_mode, pgp_inline, pgp_attach_public_key, pgp_attach_recipient_keys, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, CURRENT_TIMESTAMP)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, CURRENT_TIMESTAMP::text)
ON CONFLICT(id) DO UPDATE SET
to_addresses = excluded.to_addresses,
cc_addresses = excluded.cc_addresses,
@@ -149,7 +152,7 @@ pub mod db_drafts {
pgp_inline = excluded.pgp_inline,
pgp_attach_public_key = excluded.pgp_attach_public_key,
pgp_attach_recipient_keys = excluded.pgp_attach_recipient_keys,
updated_at = CURRENT_TIMESTAMP";
updated_at = CURRENT_TIMESTAMP::text";
pub const Q_002: &str = "SELECT id, to_addresses, cc_addresses, bcc_addresses, subject, text_body, html_body, in_reply_to, references_header, request_mdn, priority, compose_format, pgp_mode, pgp_inline, pgp_attach_public_key, pgp_attach_recipient_keys, remote_folder, remote_uid, remote_uid_validity, created_at, updated_at
FROM drafts WHERE id = $1";
pub const Q_003: &str = "SELECT id, to_addresses, cc_addresses, bcc_addresses, subject, text_body, html_body, in_reply_to, references_header, request_mdn, priority, compose_format, pgp_mode, pgp_inline, pgp_attach_public_key, pgp_attach_recipient_keys, remote_folder, remote_uid, remote_uid_validity, created_at, updated_at
@@ -171,40 +174,43 @@ pub mod db_folder_preferences {
pub const Q_003: &str = "SELECT EXISTS(SELECT 1 FROM folders WHERE name = $1)";
pub const Q_004: &str = "SELECT flagged_on_top FROM mail_preferences WHERE id = 1";
pub const Q_005: &str = "SELECT EXISTS(SELECT 1 FROM folders WHERE name = $1)";
pub const Q_006: &str = "INSERT INTO folder_preferences(folder_name, flagged_on_top, updated_at) VALUES($1, $2, CURRENT_TIMESTAMP)\n ON CONFLICT(folder_name) DO UPDATE SET flagged_on_top = excluded.flagged_on_top, updated_at = CURRENT_TIMESTAMP";
pub const Q_006: &str = "INSERT INTO folder_preferences(folder_name, flagged_on_top, updated_at) VALUES($1, $2, CURRENT_TIMESTAMP::text)\n ON CONFLICT(folder_name) DO UPDATE SET flagged_on_top = excluded.flagged_on_top, updated_at = CURRENT_TIMESTAMP::text";
pub const Q_007: &str = "DELETE FROM folder_preferences WHERE folder_name = $1";
}
pub mod db_folders {
pub const Q_001: &str = "INSERT OR REPLACE INTO folders
pub const Q_001: &str = "INSERT INTO folders
(name, delimiter, parent, flags, is_subscribed,
total_count, unread_count, uid_validity, highest_modseq, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP)";
pub const Q_002: &str = "INSERT OR IGNORE INTO folders
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP::text)
ON CONFLICT (name) DO UPDATE SET delimiter=excluded.delimiter, parent=excluded.parent, flags=excluded.flags,
is_subscribed=excluded.is_subscribed, total_count=excluded.total_count, unread_count=excluded.unread_count,
uid_validity=excluded.uid_validity, highest_modseq=excluded.highest_modseq, updated_at=CURRENT_TIMESTAMP::text";
pub const Q_002: &str = "INSERT INTO folders
(name, delimiter, flags, is_subscribed, updated_at)
VALUES ($1, $2, $3, 1, CURRENT_TIMESTAMP)";
VALUES ($1, $2, $3, 1, CURRENT_TIMESTAMP::text) ON CONFLICT (name) DO NOTHING";
pub const Q_003: &str = "SELECT name, delimiter, parent, flags, is_subscribed,
total_count, unread_count, uid_validity, highest_modseq
FROM folders
ORDER BY name";
pub const Q_004: &str = "DELETE FROM folders";
pub const Q_005: &str = "UPDATE folders SET updated_at = CURRENT_TIMESTAMP";
pub const Q_006: &str = "UPDATE folders SET uid_validity = $1, total_count = $2, messages_updated_at = CURRENT_TIMESTAMP
pub const Q_005: &str = "UPDATE folders SET updated_at = CURRENT_TIMESTAMP::text";
pub const Q_006: &str = "UPDATE folders SET uid_validity = $1, total_count = $2, messages_updated_at = CURRENT_TIMESTAMP::text
WHERE name = $3";
pub const Q_007: &str =
"UPDATE folders SET uid_validity = $1, total_count = $2, highest_modseq = $3
WHERE name = $4";
pub const Q_008: &str =
"SELECT COUNT(*) FROM messages WHERE folder = $1 AND flags NOT LIKE '%\\Seen%'";
"SELECT COUNT(*) FROM messages WHERE folder = $1 AND strpos(flags, chr(92) || 'Seen') = 0";
pub const Q_009: &str = "UPDATE folders SET unread_count = $1 WHERE name = $2";
pub const Q_010: &str = "UPDATE folders SET unread_count = GREATEST(0, CAST(unread_count AS INTEGER) + $1) WHERE name = $2";
pub const Q_011: &str = "SELECT EXISTS(
SELECT 1 FROM folders
WHERE updated_at > datetime('now', $1)
WHERE updated_at > to_char(CURRENT_TIMESTAMP - ($1 * INTERVAL '1 second'), 'YYYY-MM-DD HH24:MI:SS')
)";
pub const Q_012: &str = "SELECT EXISTS(
SELECT 1 FROM folders
WHERE name = $1 AND messages_updated_at > datetime('now', $2)
WHERE name = $1 AND messages_updated_at > to_char(CURRENT_TIMESTAMP - ($2 * INTERVAL '1 second'), 'YYYY-MM-DD HH24:MI:SS')
)";
pub const Q_013: &str = "SELECT messages_updated_at IS NULL FROM folders WHERE name = $1";
pub const Q_014: &str = "UPDATE folders SET messages_updated_at = NULL WHERE name = $1";
@@ -214,9 +220,9 @@ pub mod db_folders {
WHERE name = $1";
pub const Q_016: &str = "DELETE FROM messages WHERE folder = $1";
pub const Q_017: &str = "DELETE FROM folders WHERE name = $1";
pub const Q_018: &str = "PRAGMA defer_foreign_keys = ON;";
pub const Q_018: &str = "SELECT 1";
pub const Q_019: &str = "INSERT INTO folders (name, delimiter, parent, flags, is_subscribed, total_count, unread_count, uid_validity, highest_modseq, updated_at, messages_updated_at)
SELECT $1, delimiter, parent, flags, is_subscribed, total_count, unread_count, uid_validity, highest_modseq, CURRENT_TIMESTAMP, messages_updated_at
SELECT $1, delimiter, parent, flags, is_subscribed, total_count, unread_count, uid_validity, highest_modseq, CURRENT_TIMESTAMP::text, messages_updated_at
FROM folders WHERE name = $2";
pub const Q_020: &str = "UPDATE messages SET folder = $1 WHERE folder = $2";
pub const Q_021: &str = "UPDATE message_tags SET message_folder = $1 WHERE message_folder = $2";
@@ -243,8 +249,7 @@ pub mod db_identities {
pub const Q_001: &str = "INSERT INTO identities (display_name,email,signature_html,is_default,reply_to,bcc,organization,signature_text,signature_new,signature_reply,signature_forward,signature_position,pgp_fingerprint,pgp_sign_default,pgp_encrypt_default,pgp_inline_default) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)";
pub const Q_002: &str = "DELETE FROM identities WHERE id = $1";
pub const Q_003: &str = "SELECT COUNT(*) FROM identities";
pub const Q_004: &str =
"UPDATE identities SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE is_default = 1";
pub const Q_004: &str = "UPDATE identities SET is_default = 0, updated_at = CURRENT_TIMESTAMP::text WHERE is_default = 1";
pub const FORMAT_001: &str =
"SELECT {IDENTITY_SELECT_COLS} FROM identities ORDER BY is_default DESC, email ASC";
pub const FORMAT_002: &str = "SELECT {IDENTITY_SELECT_COLS} FROM identities WHERE id = $1";
@@ -255,17 +260,23 @@ pub mod db_identities {
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, updated_at FROM mail_preferences WHERE id = 1";
pub const Q_002: &str = "INSERT OR IGNORE INTO mail_preferences (id) VALUES (1)";
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, updated_at = CURRENT_TIMESTAMP WHERE id = 1";
pub const Q_002: &str =
"INSERT INTO mail_preferences (id) VALUES (1) 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, updated_at = CURRENT_TIMESTAMP::text WHERE id = 1";
}
pub mod db_messages {
pub const MSG_SELECT_COLS: &str = "uid, folder, message_id, in_reply_to, references_header, subject, from_address, from_name, to_addresses, cc_addresses, date, flags, size, has_attachments, snippet, reaction";
pub const Q_001: &str = "INSERT OR REPLACE INTO messages
pub const Q_001: &str = "INSERT INTO messages
(uid, folder, message_id, in_reply_to, references_header,
subject, from_address, from_name, to_addresses, cc_addresses,
date, flags, size, has_attachments, snippet, date_epoch, reaction)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)";
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
ON CONFLICT (uid, folder) DO UPDATE SET message_id=excluded.message_id, in_reply_to=excluded.in_reply_to,
references_header=excluded.references_header, subject=excluded.subject, from_address=excluded.from_address,
from_name=excluded.from_name, to_addresses=excluded.to_addresses, cc_addresses=excluded.cc_addresses,
date=excluded.date, flags=excluded.flags, size=excluded.size, has_attachments=excluded.has_attachments,
snippet=excluded.snippet, date_epoch=excluded.date_epoch, reaction=excluded.reaction";
pub const Q_002: &str = "DELETE FROM messages WHERE folder = $1";
pub const Q_003: &str = "SELECT COALESCE(MAX(uid), 0) FROM messages WHERE folder = $1";
pub const Q_004: &str = "SELECT uid, folder, subject, from_address, from_name, to_addresses, \
@@ -289,7 +300,7 @@ pub mod db_messages {
ORDER BY date_epoch DESC
LIMIT $1";
pub const Q_013: &str = "SELECT uid, flags FROM messages WHERE folder = $1";
pub const Q_014: &str = "SELECT SUM(size) FROM messages";
pub const Q_014: &str = "SELECT SUM(size)::bigint FROM messages";
pub const FORMAT_001: &str = "SELECT {MSG_SELECT_COLS}
FROM messages
WHERE folder = $1
@@ -307,7 +318,7 @@ pub mod db_messages {
FROM messages
WHERE message_id = $1
OR in_reply_to = $1
OR references_header LIKE $2 ESCAPE '\\'
OR references_header LIKE $2 ESCAPE '!'
ORDER BY date_epoch ASC";
pub const FORMAT_005: &str = "SELECT {MSG_SELECT_COLS}
FROM messages
@@ -318,19 +329,20 @@ pub mod db_messages {
pub mod db_notification_preferences {
pub const Q_001: &str = "SELECT enabled, sound, folders, dnd_enabled, dnd_start, dnd_end, updated_at FROM notification_preferences WHERE id = 1";
pub const Q_002: &str = "INSERT OR IGNORE INTO notification_preferences (id) VALUES (1)";
pub const Q_002: &str =
"INSERT INTO notification_preferences (id) VALUES (1) ON CONFLICT (id) DO NOTHING";
pub const FORMAT_001: &str =
"UPDATE notification_preferences SET {set_clause} WHERE id = ${idx}";
}
pub mod db_pgp {
pub const KEY_COLS: &str = "fingerprint,key_id,email,user_id,public_key_armor,secret_key_path,has_secret,trust,source,created_at,updated_at";
pub const Q_001: &str = "INSERT INTO pgp_keys (fingerprint,key_id,email,user_id,public_key_armor,secret_key_path,has_secret,trust,source) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)\n ON CONFLICT(fingerprint) DO UPDATE SET key_id=excluded.key_id,email=CASE WHEN excluded.email='' THEN pgp_keys.email ELSE excluded.email END,user_id=CASE WHEN excluded.user_id='' THEN pgp_keys.user_id ELSE excluded.user_id END,public_key_armor=excluded.public_key_armor,secret_key_path=COALESCE(excluded.secret_key_path,pgp_keys.secret_key_path),has_secret=MAX(pgp_keys.has_secret,excluded.has_secret),trust=excluded.trust,source=excluded.source,updated_at=CURRENT_TIMESTAMP";
pub const Q_001: &str = "INSERT INTO pgp_keys (fingerprint,key_id,email,user_id,public_key_armor,secret_key_path,has_secret,trust,source) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)\n ON CONFLICT(fingerprint) DO UPDATE SET key_id=excluded.key_id,email=CASE WHEN excluded.email='' THEN pgp_keys.email ELSE excluded.email END,user_id=CASE WHEN excluded.user_id='' THEN pgp_keys.user_id ELSE excluded.user_id END,public_key_armor=excluded.public_key_armor,secret_key_path=COALESCE(excluded.secret_key_path,pgp_keys.secret_key_path),has_secret=GREATEST(pgp_keys.has_secret,excluded.has_secret),trust=excluded.trust,source=excluded.source,updated_at=CURRENT_TIMESTAMP::text";
pub const Q_002: &str = "DELETE FROM pgp_keys WHERE replace(upper(fingerprint),' ','')=$1";
pub const Q_003: &str = "UPDATE pgp_keys SET trust=$2,updated_at=CURRENT_TIMESTAMP WHERE replace(upper(fingerprint),' ','')=$1";
pub const Q_003: &str = "UPDATE pgp_keys SET trust=$2,updated_at=CURRENT_TIMESTAMP::text WHERE replace(upper(fingerprint),' ','')=$1";
pub const Q_004: &str =
"SELECT wkd_enabled,hkp_enabled,require_trusted_encryption FROM pgp_preferences WHERE id=1";
pub const Q_005: &str = "UPDATE pgp_preferences SET wkd_enabled=$1,hkp_enabled=$2,require_trusted_encryption=$3,updated_at=CURRENT_TIMESTAMP WHERE id=1";
pub const Q_005: &str = "UPDATE pgp_preferences SET wkd_enabled=$1,hkp_enabled=$2,require_trusted_encryption=$3,updated_at=CURRENT_TIMESTAMP::text WHERE id=1";
pub const FORMAT_001: &str =
"SELECT {KEY_COLS} FROM pgp_keys ORDER BY has_secret DESC, lower(email), fingerprint";
pub const FORMAT_002: &str =
@@ -342,11 +354,13 @@ pub mod db_pgp {
pub mod db_pool {
pub const Q_001: &str = "SELECT 0";
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP::text";
pub const Q_003: &str = "SELECT 1";
pub const Q_004: &str = "SELECT 1";
pub const Q_005: &str = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = current_schema() ORDER BY tablename";
pub const Q_006: &str = "SELECT 1";
pub const Q_007: &str =
"SELECT COUNT(*) FROM pg_catalog.pg_tables WHERE schemaname = current_schema()";
}
pub mod db_response_templates {
@@ -355,7 +369,7 @@ pub mod db_response_templates {
pub const Q_002: &str =
"SELECT id,name,body,format,created_at,updated_at FROM response_templates WHERE id=$1";
pub const Q_003: &str = "INSERT INTO response_templates(name,body,format) VALUES ($1,$2,$3)";
pub const Q_004: &str = "UPDATE response_templates SET name=$1,body=$2,format=$3,updated_at=CURRENT_TIMESTAMP WHERE id=$4";
pub const Q_004: &str = "UPDATE response_templates SET name=$1,body=$2,format=$3,updated_at=CURRENT_TIMESTAMP::text WHERE id=$4";
pub const Q_005: &str = "DELETE FROM response_templates WHERE id=$1";
}
@@ -363,7 +377,7 @@ pub mod db_saved_searches {
pub const Q_001: &str =
"SELECT id,name,query_string,created_at,updated_at FROM saved_searches ORDER BY name ";
pub const Q_002: &str = "INSERT INTO saved_searches(name,query_string) VALUES ($1,$2)";
pub const Q_003: &str = "UPDATE saved_searches SET name=$1,query_string=$2,updated_at=CURRENT_TIMESTAMP WHERE id=$3";
pub const Q_003: &str = "UPDATE saved_searches SET name=$1,query_string=$2,updated_at=CURRENT_TIMESTAMP::text WHERE id=$3";
pub const Q_004: &str =
"SELECT id,name,query_string,created_at,updated_at FROM saved_searches WHERE id=$1";
pub const Q_005: &str = "DELETE FROM saved_searches WHERE id=$1";
@@ -372,9 +386,9 @@ pub mod db_saved_searches {
pub mod db_special_folders {
pub const Q_001: &str = "SELECT folder_name FROM special_folders WHERE role = $1";
pub const Q_002: &str = "SELECT EXISTS(SELECT 1 FROM folders WHERE name = $1)";
pub const Q_003: &str = "INSERT INTO special_folders(role, folder_name, updated_at) VALUES($1, $2, CURRENT_TIMESTAMP)\n ON CONFLICT(role) DO UPDATE SET folder_name = excluded.folder_name, updated_at = CURRENT_TIMESTAMP";
pub const Q_003: &str = "INSERT INTO special_folders(role, folder_name, updated_at) VALUES($1, $2, CURRENT_TIMESTAMP::text)\n ON CONFLICT(role) DO UPDATE SET folder_name = excluded.folder_name, updated_at = CURRENT_TIMESTAMP::text";
pub const Q_004: &str = "DELETE FROM special_folders WHERE role = $1";
pub const Q_005: &str = "UPDATE special_folders SET folder_name = $1, updated_at = CURRENT_TIMESTAMP WHERE folder_name = $2";
pub const Q_005: &str = "UPDATE special_folders SET folder_name = $1, updated_at = CURRENT_TIMESTAMP::text WHERE folder_name = $2";
pub const Q_006: &str = "DELETE FROM special_folders WHERE folder_name = $1";
}
@@ -389,9 +403,9 @@ pub mod db_tags {
pub const Q_003: &str =
"SELECT message_uid, message_folder FROM message_tags WHERE tag_id = $1";
pub const Q_004: &str =
"UPDATE tags SET name = $1, color = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3";
"UPDATE tags SET name = $1, color = $2, updated_at = CURRENT_TIMESTAMP::text WHERE id = $3";
pub const Q_005: &str = "DELETE FROM tags WHERE id = $1";
pub const Q_006: &str = "INSERT OR IGNORE INTO message_tags (tag_id, message_uid, message_folder) VALUES ($1, $2, $3)";
pub const Q_006: &str = "INSERT INTO message_tags (tag_id, message_uid, message_folder) VALUES ($1, $2, $3) ON CONFLICT (tag_id, message_uid, message_folder) DO NOTHING";
pub const Q_007: &str =
"DELETE FROM message_tags WHERE tag_id = $1 AND message_uid = $2 AND message_folder = $3";
pub const Q_008: &str = "SELECT t.id, t.name, t.color
@@ -409,7 +423,7 @@ pub mod db_tags {
ORDER BY m.date_epoch DESC
LIMIT $2 OFFSET $3";
pub const Q_010: &str = "SELECT COUNT(*) FROM message_tags WHERE tag_id = $1";
pub const Q_011: &str = "INSERT OR IGNORE INTO folders (name) VALUES ($1)";
pub const Q_011: &str = "INSERT INTO folders (name) VALUES ($1) ON CONFLICT (name) DO NOTHING";
pub const Q_012: &str = "INSERT INTO messages (uid, folder, subject, from_address, from_name, to_addresses, date, flags, size, has_attachments, snippet)
VALUES ($1, $2, 'Test', 'test@ex.com', 'Test', '[]', '2024-01-01', '', 100, 0, '')";
pub const FORMAT_001: &str = "SELECT mt.message_uid, mt.message_folder, t.id, t.name, t.color
@@ -421,14 +435,15 @@ pub mod db_tags {
pub mod db_trusted_senders {
pub const Q_001: &str = "SELECT email FROM trusted_senders ORDER BY lower(email)";
pub const Q_002: &str = "INSERT OR IGNORE INTO trusted_senders(email) VALUES($1)";
pub const Q_002: &str =
"INSERT INTO trusted_senders(email) VALUES($1) ON CONFLICT (email) DO NOTHING";
pub const Q_003: &str = "DELETE FROM trusted_senders WHERE email = $1 ";
}
pub mod routes_contacts {
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_003: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP::text";
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP::text";
pub const Q_003: &str = "SELECT CURRENT_TIMESTAMP::text";
}
pub mod routes_diagnostics {
@@ -442,10 +457,10 @@ pub mod routes_diagnostics {
}
pub mod routes_folder_mgmt {
pub const Q_001: &str = "UPDATE messages SET flags = CASE WHEN flags = '' THEN $2 ELSE flags || ',' || $2 END WHERE folder = $1 AND instr(flags, $2) = 0";
pub const Q_001: &str = "UPDATE messages SET flags = CASE WHEN flags = '' THEN $2 ELSE flags || ',' || $2 END WHERE folder = $1 AND POSITION($2 IN flags) = 0";
pub const Q_002: &str = "UPDATE folders SET unread_count = 0 WHERE name = $1";
pub const Q_003: &str = "SELECT uid FROM messages WHERE folder = $1";
pub const Q_004: &str = "UPDATE folders SET total_count = 0, unread_count = 0, messages_updated_at = CURRENT_TIMESTAMP WHERE name = $1";
pub const Q_004: &str = "UPDATE folders SET total_count = 0, unread_count = 0, messages_updated_at = CURRENT_TIMESTAMP::text WHERE name = $1";
}
pub mod routes_messages_mod {
+14 -12
View File
@@ -84,7 +84,7 @@ pub mod db_contacts {
pub const Q_003: &str = r#"
SELECT email, name
FROM known_addresses
WHERE (email LIKE ?1 ESCAPE '\' OR name LIKE ?1 ESCAPE '\')
WHERE (email LIKE ?1 ESCAPE '!' OR name LIKE ?1 ESCAPE '!')
AND email NOT IN (SELECT email FROM contacts)
AND NOT EXISTS (
SELECT 1 FROM contacts c, json_each(c.emails_json) alt
@@ -112,14 +112,14 @@ pub mod db_contacts {
WHERE lower(email) = lower(?1) \
OR EXISTS (SELECT 1 FROM json_each(contacts.emails_json) WHERE lower(value) = lower(?1))";
pub const FORMAT_003: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?2 OFFSET ?3";
pub const FORMAT_004: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
ORDER BY name ASC
LIMIT ?1 OFFSET ?2";
pub const FORMAT_005: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?2";
}
@@ -200,11 +200,11 @@ pub mod db_folders {
pub const Q_010: &str = "UPDATE folders SET unread_count = MAX(0, CAST(unread_count AS INTEGER) + ?1) WHERE name = ?2";
pub const Q_011: &str = "SELECT EXISTS(
SELECT 1 FROM folders
WHERE updated_at > datetime('now', ?1)
WHERE updated_at > datetime('now', printf('-%d seconds', ?1))
)";
pub const Q_012: &str = "SELECT EXISTS(
SELECT 1 FROM folders
WHERE name = ?1 AND messages_updated_at > datetime('now', ?2)
WHERE name = ?1 AND messages_updated_at > datetime('now', printf('-%d seconds', ?2))
)";
pub const Q_013: &str = "SELECT messages_updated_at IS NULL FROM folders WHERE name = ?1";
pub const Q_014: &str = "UPDATE folders SET messages_updated_at = NULL WHERE name = ?1";
@@ -307,7 +307,7 @@ pub mod db_messages {
FROM messages
WHERE message_id = ?1
OR in_reply_to = ?1
OR references_header LIKE ?2 ESCAPE '\\'
OR references_header LIKE ?2 ESCAPE '!'
ORDER BY date_epoch ASC";
pub const FORMAT_005: &str = "SELECT {MSG_SELECT_COLS}
FROM messages
@@ -347,6 +347,8 @@ pub mod db_pool {
pub const Q_004: &str = "PRAGMA foreign_keys=ON;";
pub const Q_005: &str = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
pub const Q_006: &str = "PRAGMA foreign_keys";
pub const Q_007: &str =
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'";
pub fn set_user_version(version: u32) -> String {
format!("PRAGMA user_version = {version};")
@@ -532,11 +534,11 @@ macro_rules! rustbox_query_db_contacts_002 {
#[macro_export]
macro_rules! rustbox_query_db_contacts_003 {
() => { format!("SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?2 OFFSET ?3") };
($($args:tt)+) => { format!("SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?2 OFFSET ?3", $($args)+) };
}
@@ -554,11 +556,11 @@ macro_rules! rustbox_query_db_contacts_004 {
#[macro_export]
macro_rules! rustbox_query_db_contacts_005 {
() => { format!("SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?2") };
($($args:tt)+) => { format!("SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?2", $($args)+) };
}
@@ -643,13 +645,13 @@ macro_rules! rustbox_query_db_messages_004 {
FROM messages
WHERE message_id = ?1
OR in_reply_to = ?1
OR references_header LIKE ?2 ESCAPE '\\'
OR references_header LIKE ?2 ESCAPE '!'
ORDER BY date_epoch ASC") };
($($args:tt)+) => { format!("SELECT {MSG_SELECT_COLS}
FROM messages
WHERE message_id = ?1
OR in_reply_to = ?1
OR references_header LIKE ?2 ESCAPE '\\'
OR references_header LIKE ?2 ESCAPE '!'
ORDER BY date_epoch ASC", $($args)+) };
}
+16 -11
View File
@@ -318,20 +318,20 @@ async fn sync_condstore(
let mut folder_changed = !changed.is_empty();
for (uid, mut flags) in changed {
flags.sort();
db::messages::update_message_flags(conn, &name, uid, &flags.join(","))?;
db::messages::update_message_flags(&transaction, &name, uid, &flags.join(","))?;
if sync_keywords {
db::tags::import_imap_keywords(conn, uid, &name, &flags)?;
db::tags::import_imap_keywords(&transaction, uid, &name, &flags)?;
}
}
if let Some(imap_uids) = imap_uids {
for (uid, _) in db::messages::get_all_uids_and_flags(conn, &name)? {
for (uid, _) in db::messages::get_all_uids_and_flags(&transaction, &name)? {
if !imap_uids.contains(&uid) {
db::messages::delete_message(conn, &name, uid)?;
db::messages::delete_message(&transaction, &name, uid)?;
folder_changed = true;
}
}
}
let max_uid = db::messages::max_uid(conn, &name)?;
let max_uid = db::messages::max_uid(&transaction, &name)?;
let new_messages = status.uid_next > max_uid.saturating_add(1);
let modseq = if new_modseq > 0 {
new_modseq
@@ -339,14 +339,14 @@ async fn sync_condstore(
status.highest_modseq
};
db::folders::update_folder_sync_status(
conn,
&transaction,
&name,
status.uid_validity,
status.exists,
modseq,
)?;
if new_messages {
db::folders::invalidate_folder_freshness(conn, &name)?;
db::folders::invalidate_folder_freshness(&transaction, &name)?;
folder_changed = true;
}
transaction.commit().map_err(|e| e.to_string())?;
@@ -448,7 +448,7 @@ async fn sync_full(
for (uid, cached_flags_csv) in &cached {
match imap_map.get(uid) {
None => {
db::messages::delete_message(conn, folder_name, *uid)?;
db::messages::delete_message(&transaction, folder_name, *uid)?;
folder_changed = true;
tracing::debug!(
folder = %folder_name,
@@ -467,7 +467,7 @@ async fn sync_full(
if cached_normalized != imap_flags_csv {
db::messages::update_message_flags(
conn,
&transaction,
folder_name,
*uid,
&imap_flags_csv,
@@ -475,14 +475,19 @@ async fn sync_full(
folder_changed = true;
}
if sync_keywords {
db::tags::import_imap_keywords(conn, *uid, folder_name, imap_flags)?;
db::tags::import_imap_keywords(
&transaction,
*uid,
folder_name,
imap_flags,
)?;
}
}
}
}
db::folders::update_folder_sync_status(
conn,
&transaction,
folder_name,
status.uid_validity,
status.exists,
+1 -1
View File
@@ -174,7 +174,7 @@ pub async fn export_ics(
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
let mut body = String::from(
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//RustBox//Calendar 0.1.0//EN\r\nCALSCALE:GREGORIAN\r\n",
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//RustBox//Calendar 0.1.1//EN\r\nCALSCALE:GREGORIAN\r\n",
);
for event in &events {
append_ics_event(&mut body, event);
+3 -15
View File
@@ -184,11 +184,7 @@ pub async fn create_contact_handler(
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
let now: String = conn
.query_row(
crate::queries::sqlite::queries::routes_contacts::Q_001,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(routes_contacts::Q_001), [], |row| row.get(0))
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
let id = body.id.unwrap_or_else(|| Uuid::new_v4().to_string());
let email = body.email.trim().to_string();
@@ -361,11 +357,7 @@ pub async fn merge_contacts_handler(
target.last_contacted = source.last_contacted.clone();
}
target.updated_at = tx
.query_row(
crate::queries::sqlite::queries::routes_contacts::Q_002,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(routes_contacts::Q_002), [], |row| row.get(0))
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
for group in db::contact_groups::list_contact_groups(&tx, &source.id)
@@ -966,11 +958,7 @@ pub async fn import_contacts_handler(
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
let now: String = conn
.query_row(
crate::queries::sqlite::queries::routes_contacts::Q_003,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(routes_contacts::Q_003), [], |row| row.get(0))
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
let parsed = parse_vcards(&vcf_content);
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff. Show more