1 parent
ecc201ef50
commit
698e3c4a1b
124 files changed
+4593
-2721
No files matched your search
@@ -1,5 +1,19 @@
|
|||||||
# RustBox changelog
|
# 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
|
### 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.
|
- 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.
|
- 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
@@ -3076,7 +3076,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustbox"
|
name = "rustbox"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-imap",
|
"async-imap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustbox"
|
name = "rustbox"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88"
|
rust-version = "1.88"
|
||||||
default-run = "rustbox"
|
default-run = "rustbox"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# RustBox attribution / notice
|
# 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`
|
- Upstream: `c0h1b4/oxi`
|
||||||
- Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d`
|
- Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d`
|
||||||
|
|||||||
@@ -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`.
|
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.
|
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
|
```toml
|
||||||
# Default: SQLite under data_dir.
|
# Default: the whole application uses SQLite under data_dir.
|
||||||
database_backend = "sqlite"
|
database_backend = "sqlite"
|
||||||
# database_url = "sqlite:///var/lib/rustbox/rustbox.sqlite?mode=rwc"
|
# database_url = "sqlite:///var/lib/rustbox/rustbox.sqlite?mode=rwc"
|
||||||
session_backend = "database"
|
session_backend = "database"
|
||||||
|
|
||||||
# Shared SQL example:
|
# Full PostgreSQL backend:
|
||||||
# database_backend = "pgsql"
|
# database_backend = "pgsql"
|
||||||
# database_url = "postgres://rustbox:secret@db/rustbox"
|
# database_url = "postgres://rustbox:secret@db/rustbox"
|
||||||
# cluster_mode = true
|
# session_backend = "database"
|
||||||
# session_encryption_key = "<base64-32-byte-key>"
|
# 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"
|
# session_backend = "redis"
|
||||||
# redis_url = "redis://redis:6379/"
|
# redis_url = "redis://redis:6379/"
|
||||||
|
|
||||||
@@ -101,7 +107,7 @@ session_backend = "database"
|
|||||||
# redis_password = "secret"
|
# 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
|
## Development
|
||||||
@@ -170,7 +176,7 @@ Initialize Git after extracting the release archive:
|
|||||||
```bash
|
```bash
|
||||||
git init
|
git init
|
||||||
git add .
|
git add .
|
||||||
git commit -m "RustBox 0.1.0 release"
|
git commit -m "RustBox 0.1.1 release"
|
||||||
./scripts/setup-upstream.sh
|
./scripts/setup-upstream.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+27
-86
@@ -1,89 +1,30 @@
|
|||||||
RustBox 0.1.0 source release
|
RustBox 0.1.1 release manifest
|
||||||
|
|
||||||
Runtime/source model:
|
Storage model:
|
||||||
- one root Cargo package
|
- database_backend = sqlite | mysql | pgsql selects the full application database
|
||||||
- src/ Rust application code
|
- session_backend = database stores sessions in the same database
|
||||||
- src/web/ RustBox-owned embedded browser UI
|
- session_backend = redis uses Redis or Redis Sentinel for sessions
|
||||||
- migrations/ dialect-separated SQLite/MySQL/PostgreSQL migrations
|
|
||||||
- no TypeScript/React/Next.js/Node.js/Bun build
|
Database baselines:
|
||||||
- ManageSieve TLS/STARTTLS/explicit insecure transport option
|
- migrations/sqlite/schema.sql: complete SQLite schema, baseline version 46
|
||||||
- native systemd packaging
|
- 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:
|
Oxi tracking:
|
||||||
- UPSTREAM_BASE stores consumed Oxi revision
|
- UPSTREAM_BASE stores the consumed Oxi revision
|
||||||
- scripts/setup-upstream.sh configures upstream-oxi
|
- backend source changes are mapped into the flat RustBox tree
|
||||||
- scripts/oxi-status.sh reports upstream changes
|
- upstream backend/migrations and backend/tests are review-only and never auto-imported
|
||||||
- scripts/sync-upstream.sh maps the cumulative Oxi backend delta into flat RustBox paths
|
- frontend changes are reviewed and manually ported to src/web/
|
||||||
- scripts/finish-upstream.sh advances UPSTREAM_BASE only after explicit upstream review/build validation
|
- scripts/finish-upstream.sh requires explicit acknowledgement for changed RustBox-owned upstream areas before advancing UPSTREAM_BASE
|
||||||
- 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
|
|
||||||
@@ -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
@@ -11,13 +11,14 @@ default_interface_skin = "rustbox"
|
|||||||
data_dir = ".dev/data"
|
data_dir = ".dev/data"
|
||||||
session_timeout_hours = 24
|
session_timeout_hours = 24
|
||||||
|
|
||||||
# Persistent sessions. With no extra configuration, sessions use SQLite at
|
# Application database. The selected engine stores all RustBox account data.
|
||||||
# data_dir/rustbox.sqlite and survive process restarts.
|
# With session_backend="database", it stores sessions as well.
|
||||||
database_backend = "sqlite" # sqlite | mysql | pgsql
|
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"
|
# database_url = "postgres://rustbox:secret@db.example.com/rustbox"
|
||||||
session_backend = "database" # database | redis
|
session_backend = "database" # database | redis
|
||||||
cluster_mode = false
|
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 = "..."
|
# session_encryption_key = "..."
|
||||||
# Direct Redis session backend:
|
# Direct Redis session backend:
|
||||||
# redis_url = "redis://redis.example.com:6379/"
|
# redis_url = "redis://redis.example.com:6379/"
|
||||||
|
|||||||
+5
-4
@@ -1,4 +1,4 @@
|
|||||||
# RustBox 0.1.0
|
# RustBox 0.1.1
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = 3001
|
port = 3001
|
||||||
environment = "production"
|
environment = "production"
|
||||||
@@ -12,13 +12,14 @@ data_dir = "/var/lib/rustbox"
|
|||||||
log_dir = "/var/log/rustbox"
|
log_dir = "/var/log/rustbox"
|
||||||
session_timeout_hours = 24
|
session_timeout_hours = 24
|
||||||
|
|
||||||
# Persistent sessions. With no extra configuration, sessions use SQLite at
|
# Application database. The selected engine stores all RustBox account data.
|
||||||
# data_dir/rustbox.sqlite and survive process restarts.
|
# With session_backend="database", it stores sessions as well.
|
||||||
database_backend = "sqlite" # sqlite | mysql | pgsql
|
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"
|
# database_url = "postgres://rustbox:secret@db.example.com/rustbox"
|
||||||
session_backend = "database" # database | redis
|
session_backend = "database" # database | redis
|
||||||
cluster_mode = false
|
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 = "..."
|
# session_encryption_key = "..."
|
||||||
# Direct Redis session backend:
|
# Direct Redis session backend:
|
||||||
# redis_url = "redis://redis.example.com:6379/"
|
# redis_url = "redis://redis.example.com:6379/"
|
||||||
|
|||||||
@@ -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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- Graceful SIGTERM/Ctrl+C shutdown stops accepting new requests and aborts dedicated IMAP IDLE tasks before process exit.
|
||||||
|
|
||||||
### Personalization
|
### Personalization
|
||||||
|
|
||||||
- Persistent per-user display preferences in SQLite.
|
- Persistent per-user display preferences in the configured application database.
|
||||||
- Light/dark/system theme.
|
- Light/dark/system theme.
|
||||||
- Presets: sage, blue, teal, cyan, indigo, slate, graphite, amber, orange, violet, rose, red and custom.
|
- Presets: sage, blue, teal, cyan, indigo, slate, graphite, amber, orange, violet, rose, red and custom.
|
||||||
- Custom accent, background, panel and text colors.
|
- Custom accent, background, panel and text colors.
|
||||||
|
|||||||
+22
-12
@@ -6,19 +6,24 @@ RustBox uses a flat Cargo layout, while Oxi uses a `backend/` subtree. The sync
|
|||||||
|
|
||||||
## Path mapping
|
## Path mapping
|
||||||
|
|
||||||
Examples:
|
Automatically mapped examples:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Oxi backend/Cargo.toml -> RustBox Cargo.toml
|
Oxi backend/Cargo.toml -> RustBox Cargo.toml
|
||||||
Oxi backend/Cargo.lock -> RustBox Cargo.lock
|
Oxi backend/Cargo.lock -> RustBox Cargo.lock
|
||||||
Oxi backend/rust-toolchain.toml -> RustBox rust-toolchain.toml
|
Oxi backend/rust-toolchain.toml -> RustBox rust-toolchain.toml
|
||||||
Oxi backend/src/... -> RustBox src/...
|
Oxi backend/src/... -> RustBox src/...
|
||||||
Oxi backend/migrations/... -> RustBox migrations/...
|
|
||||||
Oxi backend/tests/... -> RustBox tests/...
|
|
||||||
Oxi backend/examples/... -> RustBox examples/...
|
Oxi backend/examples/... -> RustBox examples/...
|
||||||
Oxi backend/docs/... -> RustBox docs/...
|
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.
|
RustBox-owned `src/web/` has no automatic mapping from the Oxi frontend.
|
||||||
|
|
||||||
## Configure the upstream
|
## Configure the upstream
|
||||||
@@ -28,7 +33,7 @@ A release archive does not contain `.git`. Initialize the RustBox repository onc
|
|||||||
```bash
|
```bash
|
||||||
git init
|
git init
|
||||||
git add .
|
git add .
|
||||||
git commit -m "RustBox 0.0.1 baseline"
|
git commit -m "RustBox 0.1.1 baseline"
|
||||||
./scripts/setup-upstream.sh
|
./scripts/setup-upstream.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -55,10 +60,11 @@ or inspect a specific Oxi ref:
|
|||||||
|
|
||||||
The report separates:
|
The report separates:
|
||||||
|
|
||||||
1. Oxi `backend/` changes that can be mapped into RustBox;
|
1. Oxi backend changes that can be mapped automatically;
|
||||||
2. Oxi `frontend/` changes that require a manual UI port;
|
2. Oxi backend migrations/tests that require manual review;
|
||||||
3. other Oxi root-level changes that require manual review;
|
3. Oxi frontend changes that require a manual UI port;
|
||||||
4. upstream commits since `UPSTREAM_BASE`.
|
4. other Oxi root-level changes that require manual review;
|
||||||
|
5. upstream commits since `UPSTREAM_BASE`.
|
||||||
|
|
||||||
## Advance the consumed Oxi backend state
|
## Advance the consumed Oxi backend state
|
||||||
|
|
||||||
@@ -72,10 +78,10 @@ The operation:
|
|||||||
2. fetches `upstream-oxi`;
|
2. fetches `upstream-oxi`;
|
||||||
3. verifies that the target descends from the recorded `UPSTREAM_BASE`;
|
3. verifies that the target descends from the recorded `UPSTREAM_BASE`;
|
||||||
4. creates a `sync/oxi-*` review branch;
|
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>`;
|
6. rewrites only patch metadata so `backend/<path>` becomes `<path>`;
|
||||||
7. applies the mapped delta with Git three-way merge support;
|
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;
|
9. records the target as a pending sync inside Git metadata;
|
||||||
10. deliberately leaves `UPSTREAM_BASE` unchanged until the maintainer finishes review.
|
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
|
./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
|
```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.
|
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.
|
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:
|
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/web/`;
|
||||||
- `src/ui.rs`;
|
- `src/ui.rs`;
|
||||||
- RustBox UI routing and same-origin behavior;
|
- RustBox UI routing and same-origin behavior;
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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;
|
|
||||||
@@ -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
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
ALTER TABLE display_preferences ADD COLUMN deep_index BOOLEAN NOT NULL DEFAULT 0;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
|
||||||
@@ -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
@@ -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.
|
# The active values below intentionally keep the existing package profile.
|
||||||
# Alternative storage/cluster examples are provided at the end of this file
|
# Alternative storage/cluster examples are provided at the end of this file
|
||||||
@@ -13,9 +13,9 @@ environment = "production"
|
|||||||
data_dir = "/var/lib/rustbox"
|
data_dir = "/var/lib/rustbox"
|
||||||
log_dir = "/var/log/rustbox"
|
log_dir = "/var/log/rustbox"
|
||||||
|
|
||||||
# Sessions and "remember me".
|
# Database, sessions and "remember me".
|
||||||
# With no database/session backend keys configured, RustBox defaults to
|
# With no database/session backend keys configured, the whole application uses
|
||||||
# persistent SQLite sessions under data_dir and survives process restarts.
|
# SQLite at data_dir/rustbox.sqlite; sessions are stored in that database.
|
||||||
session_timeout_hours = 72
|
session_timeout_hours = 72
|
||||||
remember_me_enabled = true
|
remember_me_enabled = true
|
||||||
remember_me_days = 30
|
remember_me_days = 30
|
||||||
@@ -74,21 +74,21 @@ pgp_allow_private_export = true
|
|||||||
# Copy/uncomment only the keys required by the selected profile.
|
# Copy/uncomment only the keys required by the selected profile.
|
||||||
#
|
#
|
||||||
# session_encryption_key must represent the same 32-byte key on every node that
|
# 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:
|
# No extra keys are required. The equivalent explicit configuration is:
|
||||||
# database_backend = "sqlite"
|
# database_backend = "sqlite"
|
||||||
# session_backend = "database"
|
# session_backend = "database"
|
||||||
# cluster_mode = false
|
# cluster_mode = false
|
||||||
#
|
#
|
||||||
# RustBox stores sessions in data_dir/rustbox.sqlite and creates/uses the local
|
# RustBox stores account data and sessions in data_dir/rustbox.sqlite and
|
||||||
# session encryption key for this single-node SQLite setup.
|
# 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_backend = "pgsql"
|
||||||
# database_url = "postgres://rustbox:change-me@127.0.0.1/rustbox"
|
# 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"
|
# 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_backend = "mysql"
|
||||||
# database_url = "mysql://rustbox:change-me@127.0.0.1/rustbox"
|
# 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"
|
# 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"
|
# session_backend = "redis"
|
||||||
# redis_url = "redis://127.0.0.1:6379/"
|
# 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
|
# 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"
|
# session_backend = "redis"
|
||||||
# cluster_mode = true
|
# cluster_mode = true
|
||||||
# redis_sentinel_nodes = ["sentinel-a:26379", "sentinel-b:26379", "sentinel-c:26379"]
|
# 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"
|
# 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_backend = "pgsql"
|
||||||
# database_url = "postgres://rustbox:change-me@db.internal/rustbox"
|
# 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"
|
# 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_backend = "mysql"
|
||||||
# database_url = "mysql://rustbox:change-me@db.internal/rustbox"
|
# database_url = "mysql://rustbox:change-me@db.internal/rustbox"
|
||||||
|
|||||||
@@ -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"}
|
|
||||||
@@ -6,13 +6,15 @@ cd "$ROOT"
|
|||||||
|
|
||||||
UI_REVIEWED=0
|
UI_REVIEWED=0
|
||||||
ROOT_REVIEWED=0
|
ROOT_REVIEWED=0
|
||||||
|
OWNED_REVIEWED=0
|
||||||
SKIP_BUILD=0
|
SKIP_BUILD=0
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--ui-reviewed) UI_REVIEWED=1 ;;
|
--ui-reviewed) UI_REVIEWED=1 ;;
|
||||||
--root-reviewed) ROOT_REVIEWED=1 ;;
|
--root-reviewed) ROOT_REVIEWED=1 ;;
|
||||||
|
--rustbox-owned-reviewed) OWNED_REVIEWED=1 ;;
|
||||||
--skip-build) SKIP_BUILD=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
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -49,6 +51,15 @@ if find . -path ./.git -prune -o -type f \( -name '*.ts' -o -name '*.tsx' \) -pr
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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 ! git diff --quiet "$BASE..$TARGET_SHA" -- frontend/; then
|
||||||
if [ "$UI_REVIEWED" -ne 1 ]; then
|
if [ "$UI_REVIEWED" -ne 1 ]; then
|
||||||
echo "error: Oxi frontend changed between $BASE and $TARGET_SHA." >&2
|
echo "error: Oxi frontend changed between $BASE and $TARGET_SHA." >&2
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ tmp_mapped="$(mktemp)"
|
|||||||
cleanup() { rm -f "$tmp_raw" "$tmp_mapped"; }
|
cleanup() { rm -f "$tmp_raw" "$tmp_mapped"; }
|
||||||
trap cleanup EXIT
|
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 \
|
sed \
|
||||||
-e 's#^diff --git a/backend/#diff --git a/#' \
|
-e 's#^diff --git a/backend/#diff --git a/#' \
|
||||||
-e 's# b/backend/# b/#' \
|
-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 'Ported backend portion of Oxi commit %s onto branch %s.\n' "$SHA" "$branch"
|
||||||
printf 'UPSTREAM_BASE was intentionally NOT changed.\n'
|
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"
|
printf 'Review the original commit, especially frontend changes: git show %s -- frontend/\n' "$SHA"
|
||||||
@@ -29,8 +29,11 @@ printf ' consumed: %s\n' "$BASE"
|
|||||||
printf ' target: %s\n' "$TARGET_SHA"
|
printf ' target: %s\n' "$TARGET_SHA"
|
||||||
printf ' commits: %s\n' "$(git rev-list --count "$BASE..$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'
|
printf '\nOxi backend changes that can be mapped automatically into RustBox root:\n'
|
||||||
git diff --name-status "$BASE..$TARGET_SHA" -- backend/ || true
|
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'
|
printf '\nOxi frontend changes requiring manual RustBox UI port:\n'
|
||||||
git diff --name-status "$BASE..$TARGET_SHA" -- frontend/ || true
|
git diff --name-status "$BASE..$TARGET_SHA" -- frontend/ || true
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|||||||
echo "Initialize it first:" >&2
|
echo "Initialize it first:" >&2
|
||||||
echo " git init" >&2
|
echo " git init" >&2
|
||||||
echo " git add ." >&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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ tmp_mapped="$(mktemp)"
|
|||||||
cleanup() { rm -f "$tmp_raw" "$tmp_mapped"; }
|
cleanup() { rm -f "$tmp_raw" "$tmp_mapped"; }
|
||||||
trap cleanup EXIT
|
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 \
|
sed \
|
||||||
-e 's#^diff --git a/backend/#diff --git a/#' \
|
-e 's#^diff --git a/backend/#diff --git a/#' \
|
||||||
-e 's# b/backend/# b/#' \
|
-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 '\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 '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 '\nFrontend changes were NOT imported. Review them with:\n'
|
||||||
printf ' git diff --name-status %s..%s -- frontend/\n' "$BASE" "$TARGET_SHA"
|
printf ' git diff --name-status %s..%s -- frontend/\n' "$BASE" "$TARGET_SHA"
|
||||||
printf ' git diff %s..%s -- frontend/\n' "$BASE" "$TARGET_SHA"
|
printf ' git diff %s..%s -- frontend/\n' "$BASE" "$TARGET_SHA"
|
||||||
|
|||||||
+9
-15
@@ -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
|
// Ensure this account has its isolated tables in the configured RustBox database.
|
||||||
// migrations from db::pool. Session migrations are handled separately by
|
// The same database backend stores sessions unless session_backend=redis.
|
||||||
// session_store for the configured session database backend.
|
|
||||||
let conn = crate::db::pool::open_user_db(data_dir, user_hash)
|
let conn = crate::db::pool::open_user_db(data_dir, user_hash)
|
||||||
.map_err(|e| format!("failed to initialize user database: {e}"))?;
|
.map_err(|e| format!("failed to initialize user database: {e}"))?;
|
||||||
drop(conn);
|
drop(conn);
|
||||||
@@ -75,8 +74,8 @@ mod tests {
|
|||||||
|
|
||||||
assert!(user_dir.exists(), "user directory should exist");
|
assert!(user_dir.exists(), "user directory should exist");
|
||||||
assert!(
|
assert!(
|
||||||
user_dir.join("db.sqlite").exists(),
|
tmp.path().join("rustbox.sqlite").exists(),
|
||||||
"db.sqlite should exist"
|
"global rustbox.sqlite should exist for the default SQLite backend"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
user_dir.join("tantivy").is_dir(),
|
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");
|
let dir2 = provision_user_data(data_dir, &user_hash).expect("second call failed");
|
||||||
|
|
||||||
assert_eq!(dir1, dir2);
|
assert_eq!(dir1, dir2);
|
||||||
assert!(dir2.join("db.sqlite").exists());
|
assert!(tmp.path().join("rustbox.sqlite").exists());
|
||||||
assert!(dir2.join("tantivy").is_dir());
|
assert!(dir2.join("tantivy").is_dir());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,17 +103,12 @@ mod tests {
|
|||||||
let data_dir = tmp.path().to_str().unwrap();
|
let data_dir = tmp.path().to_str().unwrap();
|
||||||
let user_hash = hash_email("meta@example.com");
|
let user_hash = hash_email("meta@example.com");
|
||||||
|
|
||||||
let user_dir = provision_user_data(data_dir, &user_hash).expect("provisioning failed");
|
provision_user_data(data_dir, &user_hash).expect("provisioning failed");
|
||||||
|
let conn = crate::db::pool::open_user_db(data_dir, &user_hash)
|
||||||
let conn =
|
.expect("failed to open account database");
|
||||||
rusqlite::Connection::open(user_dir.join("db.sqlite")).expect("failed to open db");
|
|
||||||
|
|
||||||
let value: String = conn
|
let value: String = conn
|
||||||
.query_row(
|
.query_row(crate::dbq!(auth_user_data::Q_001), [], |row| row.get(0))
|
||||||
crate::queries::sqlite::queries::auth_user_data::Q_001,
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.expect("failed to query user_meta");
|
.expect("failed to query user_meta");
|
||||||
|
|
||||||
assert_eq!(value, "1");
|
assert_eq!(value, "1");
|
||||||
|
|||||||
+19
-5
@@ -492,11 +492,11 @@ impl AppConfig {
|
|||||||
return Err("Redis sessions require REDIS_URL or REDIS_SENTINEL_NODES + REDIS_SENTINEL_MASTER".to_string());
|
return Err("Redis sessions require REDIS_URL or REDIS_SENTINEL_NODES + REDIS_SENTINEL_MASTER".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.cluster_mode
|
if self.cluster_mode && self.database_backend == "sqlite" {
|
||||||
&& self.session_backend == "database"
|
return Err(
|
||||||
&& self.database_backend == "sqlite"
|
"CLUSTER_MODE requires a shared mysql/pgsql application database; Redis only replaces session persistence"
|
||||||
{
|
.to_string(),
|
||||||
return Err("CLUSTER_MODE with database sessions requires mysql/pgsql, or configure SESSION_BACKEND=redis".to_string());
|
);
|
||||||
}
|
}
|
||||||
if (self.cluster_mode
|
if (self.cluster_mode
|
||||||
|| self.session_backend == "redis"
|
|| self.session_backend == "redis"
|
||||||
@@ -809,4 +809,18 @@ mod tests {
|
|||||||
vec![TrustedProxy::Ip("127.0.0.1".parse().unwrap())]
|
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
@@ -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 crate::queries::sqlite::queries::db_calendar::{EVENT_SELECT_COLS, SETTINGS_SELECT_COLS};
|
||||||
use rusqlite::{Connection, params};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ pub struct MeetingTemplate {
|
|||||||
pub created_at: String,
|
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)?;
|
let all_day_int: i32 = row.get(6)?;
|
||||||
Ok(CalendarEvent {
|
Ok(CalendarEvent {
|
||||||
id: row.get(0)?,
|
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)?;
|
let caldav_enabled_int: i32 = row.get(10)?;
|
||||||
Ok(CalendarSettings {
|
Ok(CalendarSettings {
|
||||||
default_view: row.get(0)?,
|
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)?;
|
let is_default_int: i32 = row.get(4)?;
|
||||||
Ok(MeetingTemplate {
|
Ok(MeetingTemplate {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
@@ -187,7 +191,8 @@ pub fn list_events(
|
|||||||
start: &str,
|
start: &str,
|
||||||
end: &str,
|
end: &str,
|
||||||
) -> Result<Vec<CalendarEvent>, String> {
|
) -> 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
|
let mut stmt = conn
|
||||||
.prepare(&sql)
|
.prepare(&sql)
|
||||||
.map_err(|e| format!("Failed to prepare list_events: {e}"))?;
|
.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> {
|
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
|
let mut stmt = conn
|
||||||
.prepare(&sql)
|
.prepare(&sql)
|
||||||
.map_err(|e| format!("Failed to prepare list_all_events: {e}"))?;
|
.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> {
|
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);
|
let result = conn.query_row(&sql, params![id], row_to_event);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(e) => Ok(Some(e)),
|
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}")),
|
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> {
|
pub fn create_event(conn: &Connection, data: &CreateEvent) -> Result<CalendarEvent, String> {
|
||||||
let id = Uuid::new_v4().to_string();
|
let id = Uuid::new_v4().to_string();
|
||||||
let now: String = conn
|
let now: String = conn
|
||||||
.query_row(
|
.query_row(crate::dbq!(db_calendar::Q_001), [], |row| row.get(0))
|
||||||
crate::queries::sqlite::queries::db_calendar::Q_001,
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to get current time: {e}"))?;
|
.map_err(|e| format!("Failed to get current time: {e}"))?;
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_calendar::Q_002,
|
crate::dbq!(db_calendar::Q_002),
|
||||||
params![
|
params![
|
||||||
id,
|
id,
|
||||||
data.title,
|
data.title,
|
||||||
@@ -275,14 +278,14 @@ pub fn update_event(
|
|||||||
data: &UpdateEvent,
|
data: &UpdateEvent,
|
||||||
) -> Result<Option<CalendarEvent>, String> {
|
) -> Result<Option<CalendarEvent>, String> {
|
||||||
let mut sets = Vec::new();
|
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;
|
let mut idx = 1u32;
|
||||||
|
|
||||||
macro_rules! maybe_set {
|
macro_rules! maybe_set {
|
||||||
($field:ident, $col:expr) => {
|
($field:ident, $col:expr) => {
|
||||||
if let Some(ref v) = data.$field {
|
if let Some(ref v) = data.$field {
|
||||||
sets.push(format!("{} = ?{idx}", $col));
|
sets.push(format!("{} = ?{idx}", $col));
|
||||||
values.push(Box::new(v.clone()));
|
values.push(v.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -302,13 +305,13 @@ pub fn update_event(
|
|||||||
|
|
||||||
if let Some(v) = data.all_day {
|
if let Some(v) = data.all_day {
|
||||||
sets.push(format!("all_day = ?{idx}"));
|
sets.push(format!("all_day = ?{idx}"));
|
||||||
values.push(Box::new(v as i32));
|
values.push((v as i32).to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(v) = data.reminder_minutes {
|
if let Some(v) = data.reminder_minutes {
|
||||||
sets.push(format!("reminder_minutes = ?{idx}"));
|
sets.push(format!("reminder_minutes = ?{idx}"));
|
||||||
values.push(Box::new(v));
|
values.push(v.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,12 +321,15 @@ pub fn update_event(
|
|||||||
|
|
||||||
sets.push("updated_at = datetime('now')".to_string());
|
sets.push("updated_at = datetime('now')".to_string());
|
||||||
|
|
||||||
let sql = crate::rustbox_query_db_calendar_004!(sets.join(", "), idx);
|
let sql = format!(
|
||||||
values.push(Box::new(id.to_string()));
|
"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
|
let updated = conn
|
||||||
.execute(&sql, params_refs.as_slice())
|
.execute(&sql, values)
|
||||||
.map_err(|e| format!("Failed to update event: {e}"))?;
|
.map_err(|e| format!("Failed to update event: {e}"))?;
|
||||||
|
|
||||||
if updated == 0 {
|
if updated == 0 {
|
||||||
@@ -335,10 +341,7 @@ pub fn update_event(
|
|||||||
|
|
||||||
pub fn delete_event(conn: &Connection, id: &str) -> Result<bool, String> {
|
pub fn delete_event(conn: &Connection, id: &str) -> Result<bool, String> {
|
||||||
let deleted = conn
|
let deleted = conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_calendar::Q_003), params![id])
|
||||||
crate::queries::sqlite::queries::db_calendar::Q_003,
|
|
||||||
params![id],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to delete event: {e}"))?;
|
.map_err(|e| format!("Failed to delete event: {e}"))?;
|
||||||
Ok(deleted > 0)
|
Ok(deleted > 0)
|
||||||
}
|
}
|
||||||
@@ -347,21 +350,23 @@ pub fn find_event_by_source_uid(
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
source_uid: &str,
|
source_uid: &str,
|
||||||
) -> Result<Option<CalendarEvent>, String> {
|
) -> 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);
|
let result = conn.query_row(&sql, params![source_uid], row_to_event);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(e) => Ok(Some(e)),
|
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}")),
|
Err(e) => Err(format!("Failed to find event by source_uid: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_calendar_settings(conn: &Connection) -> Result<CalendarSettings, String> {
|
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}"))?;
|
.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)
|
conn.query_row(&sql, [], row_to_settings)
|
||||||
.map_err(|e| format!("Failed to get calendar settings: {e}"))
|
.map_err(|e| format!("Failed to get calendar settings: {e}"))
|
||||||
}
|
}
|
||||||
@@ -370,18 +375,18 @@ pub fn update_calendar_settings(
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
data: &UpdateCalendarSettings,
|
data: &UpdateCalendarSettings,
|
||||||
) -> Result<CalendarSettings, String> {
|
) -> 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}"))?;
|
.map_err(|e| format!("Failed to ensure calendar_settings row: {e}"))?;
|
||||||
|
|
||||||
let mut sets = Vec::new();
|
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;
|
let mut idx = 1u32;
|
||||||
|
|
||||||
macro_rules! maybe_set {
|
macro_rules! maybe_set {
|
||||||
($field:ident, $col:expr) => {
|
($field:ident, $col:expr) => {
|
||||||
if let Some(ref v) = data.$field {
|
if let Some(ref v) = data.$field {
|
||||||
sets.push(format!("{} = ?{idx}", $col));
|
sets.push(format!("{} = ?{idx}", $col));
|
||||||
values.push(Box::new(v.clone()));
|
values.push(v.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -400,13 +405,13 @@ pub fn update_calendar_settings(
|
|||||||
|
|
||||||
if let Some(v) = data.week_starts_on {
|
if let Some(v) = data.week_starts_on {
|
||||||
sets.push(format!("week_starts_on = ?{idx}"));
|
sets.push(format!("week_starts_on = ?{idx}"));
|
||||||
values.push(Box::new(v));
|
values.push(v.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(v) = data.caldav_enabled {
|
if let Some(v) = data.caldav_enabled {
|
||||||
sets.push(format!("caldav_enabled = ?{idx}"));
|
sets.push(format!("caldav_enabled = ?{idx}"));
|
||||||
values.push(Box::new(v as i32));
|
values.push((v as i32).to_db_value());
|
||||||
#[allow(unused_assignments)]
|
#[allow(unused_assignments)]
|
||||||
{
|
{
|
||||||
idx += 1;
|
idx += 1;
|
||||||
@@ -415,10 +420,11 @@ pub fn update_calendar_settings(
|
|||||||
|
|
||||||
if !sets.is_empty() {
|
if !sets.is_empty() {
|
||||||
sets.push("updated_at = datetime('now')".to_string());
|
sets.push("updated_at = datetime('now')".to_string());
|
||||||
let sql = crate::rustbox_query_db_calendar_007!(sets.join(", "));
|
let sql = format!(
|
||||||
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
|
"UPDATE calendar_settings SET {} WHERE id = 1",
|
||||||
values.iter().map(|b| b.as_ref()).collect();
|
sets.join(", ")
|
||||||
conn.execute(&sql, params_refs.as_slice())
|
);
|
||||||
|
conn.execute(&sql, values)
|
||||||
.map_err(|e| format!("Failed to update calendar settings: {e}"))?;
|
.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> {
|
pub fn list_meeting_templates(conn: &Connection) -> Result<Vec<MeetingTemplate>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare list_meeting_templates: {e}"))?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map([], row_to_template)
|
.query_map([], row_to_template)
|
||||||
@@ -446,24 +452,21 @@ pub fn create_meeting_template(
|
|||||||
url_template: &str,
|
url_template: &str,
|
||||||
icon: &str,
|
icon: &str,
|
||||||
) -> Result<MeetingTemplate, String> {
|
) -> Result<MeetingTemplate, String> {
|
||||||
conn.execute(
|
let id = conn
|
||||||
crate::queries::sqlite::queries::db_calendar::Q_007,
|
.insert_id(
|
||||||
params![name, url_template, icon],
|
crate::dbq!(db_calendar::Q_007),
|
||||||
)
|
params![name, url_template, icon],
|
||||||
.map_err(|e| format!("Failed to create meeting template: {e}"))?;
|
)
|
||||||
|
.map_err(|e| format!("Failed to create meeting template: {e}"))?;
|
||||||
|
|
||||||
let id = conn.last_insert_rowid();
|
let sql = crate::dbq!(db_calendar::Q_008);
|
||||||
let sql = crate::queries::sqlite::queries::db_calendar::Q_008;
|
|
||||||
conn.query_row(sql, params![id], row_to_template)
|
conn.query_row(sql, params![id], row_to_template)
|
||||||
.map_err(|e| format!("Failed to read created template: {e}"))
|
.map_err(|e| format!("Failed to read created template: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_meeting_template(conn: &Connection, id: i64) -> Result<bool, String> {
|
pub fn delete_meeting_template(conn: &Connection, id: i64) -> Result<bool, String> {
|
||||||
let deleted = conn
|
let deleted = conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_calendar::Q_009), params![id])
|
||||||
crate::queries::sqlite::queries::db_calendar::Q_009,
|
|
||||||
params![id],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to delete meeting template: {e}"))?;
|
.map_err(|e| format!("Failed to delete meeting template: {e}"))?;
|
||||||
Ok(deleted > 0)
|
Ok(deleted > 0)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-19
@@ -1,4 +1,4 @@
|
|||||||
use rusqlite::{Connection, params};
|
use crate::db::pool::{Connection, params};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::db::contacts::Contact;
|
use crate::db::contacts::Contact;
|
||||||
@@ -13,17 +13,14 @@ pub struct ContactGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_group(conn: &Connection, id: &str, name: &str) -> Result<(), String> {
|
pub fn create_group(conn: &Connection, id: &str, name: &str) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_contact_groups::Q_001), params![id, name])
|
||||||
crate::queries::sqlite::queries::db_contact_groups::Q_001,
|
.map_err(|e| format!("Failed to create contact group: {e}"))?;
|
||||||
params![id, name],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to create contact group: {e}"))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_groups(conn: &Connection) -> Result<Vec<ContactGroup>, String> {
|
pub fn list_groups(conn: &Connection) -> Result<Vec<ContactGroup>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare list_groups: {e}"))?;
|
||||||
|
|
||||||
let rows = stmt
|
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> {
|
pub fn update_group(conn: &Connection, id: &str, name: &str) -> Result<bool, String> {
|
||||||
let updated = conn
|
let updated = conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_contact_groups::Q_003), params![name, id])
|
||||||
crate::queries::sqlite::queries::db_contact_groups::Q_003,
|
|
||||||
params![name, id],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to update contact group: {e}"))?;
|
.map_err(|e| format!("Failed to update contact group: {e}"))?;
|
||||||
Ok(updated > 0)
|
Ok(updated > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_group(conn: &Connection, id: &str) -> Result<bool, String> {
|
pub fn delete_group(conn: &Connection, id: &str) -> Result<bool, String> {
|
||||||
let deleted = conn
|
let deleted = conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_contact_groups::Q_004), params![id])
|
||||||
crate::queries::sqlite::queries::db_contact_groups::Q_004,
|
|
||||||
params![id],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to delete contact group: {e}"))?;
|
.map_err(|e| format!("Failed to delete contact group: {e}"))?;
|
||||||
Ok(deleted > 0)
|
Ok(deleted > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_member(conn: &Connection, group_id: &str, contact_id: &str) -> Result<(), String> {
|
pub fn add_member(conn: &Connection, group_id: &str, contact_id: &str) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_contact_groups::Q_005,
|
crate::dbq!(db_contact_groups::Q_005),
|
||||||
params![group_id, contact_id],
|
params![group_id, contact_id],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to add group member: {e}"))?;
|
.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> {
|
pub fn remove_member(conn: &Connection, group_id: &str, contact_id: &str) -> Result<bool, String> {
|
||||||
let deleted = conn
|
let deleted = conn
|
||||||
.execute(
|
.execute(
|
||||||
crate::queries::sqlite::queries::db_contact_groups::Q_006,
|
crate::dbq!(db_contact_groups::Q_006),
|
||||||
params![group_id, contact_id],
|
params![group_id, contact_id],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to remove group member: {e}"))?;
|
.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> {
|
pub fn list_group_members(conn: &Connection, group_id: &str) -> Result<Vec<Contact>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare list_group_members: {e}"))?;
|
||||||
|
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
@@ -131,7 +122,7 @@ pub fn list_contact_groups(
|
|||||||
contact_id: &str,
|
contact_id: &str,
|
||||||
) -> Result<Vec<ContactGroup>, String> {
|
) -> Result<Vec<ContactGroup>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare list_contact_groups: {e}"))?;
|
||||||
|
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
|
|||||||
+33
-43
@@ -1,5 +1,5 @@
|
|||||||
|
use crate::db::pool::{Connection, params};
|
||||||
use crate::queries::sqlite::queries::db_contacts::CONTACT_SELECT_COLS;
|
use crate::queries::sqlite::queries::db_contacts::CONTACT_SELECT_COLS;
|
||||||
use rusqlite::{Connection, params};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ pub struct Contact {
|
|||||||
pub has_photo: bool,
|
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 {
|
Ok(Contact {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
email: row.get(1)?,
|
email: row.get(1)?,
|
||||||
@@ -60,14 +60,14 @@ fn row_to_contact(row: &rusqlite::Row<'_>) -> rusqlite::Result<Contact> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn escape_like(term: &str) -> String {
|
fn escape_like(term: &str) -> String {
|
||||||
term.replace('\\', "\\\\")
|
term.replace('!', "!!")
|
||||||
.replace('%', "\\%")
|
.replace('%', "!%")
|
||||||
.replace('_', "\\_")
|
.replace('_', "!_")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upsert_contact(conn: &Connection, contact: &Contact) -> Result<(), String> {
|
pub fn upsert_contact(conn: &Connection, contact: &Contact) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_contacts::Q_001,
|
crate::dbq!(db_contacts::Q_001),
|
||||||
params![
|
params![
|
||||||
contact.id,
|
contact.id,
|
||||||
contact.email,
|
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> {
|
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);
|
let result = conn.query_row(&sql, params![id], row_to_contact);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(c) => Ok(Some(c)),
|
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}")),
|
Err(e) => Err(format!("Failed to get contact: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_contact_by_email(conn: &Connection, email: &str) -> Result<Option<Contact>, String> {
|
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);
|
let result = conn.query_row(&sql, params![email], row_to_contact);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(c) => Ok(Some(c)),
|
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}")),
|
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() => {
|
Some(q) if !q.is_empty() => {
|
||||||
let escaped = escape_like(q);
|
let escaped = escape_like(q);
|
||||||
let pattern = format!("%{escaped}%");
|
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);
|
drop(pattern);
|
||||||
(sql, Some(escaped))
|
(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)
|
(sql, None)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -170,10 +174,7 @@ pub fn list_contacts(
|
|||||||
|
|
||||||
pub fn delete_contact(conn: &Connection, id: &str) -> Result<bool, String> {
|
pub fn delete_contact(conn: &Connection, id: &str) -> Result<bool, String> {
|
||||||
let deleted = conn
|
let deleted = conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_contacts::Q_002), params![id])
|
||||||
crate::queries::sqlite::queries::db_contacts::Q_002,
|
|
||||||
params![id],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to delete contact: {e}"))?;
|
.map_err(|e| format!("Failed to delete contact: {e}"))?;
|
||||||
Ok(deleted > 0)
|
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 escaped = escape_like(query);
|
||||||
let pattern = format!("%{escaped}%");
|
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
|
let mut stmt = conn
|
||||||
.prepare(&sql)
|
.prepare(&sql)
|
||||||
@@ -206,7 +208,7 @@ pub fn search_known_addresses(
|
|||||||
let escaped = escape_like(query);
|
let escaped = escape_like(query);
|
||||||
let pattern = format!("%{escaped}%");
|
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
|
let mut stmt = conn
|
||||||
.prepare(sql)
|
.prepare(sql)
|
||||||
@@ -237,7 +239,7 @@ pub fn populate_known_addresses(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if !from_address.is_empty() {
|
if !from_address.is_empty() {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_contacts::Q_004,
|
crate::dbq!(db_contacts::Q_004),
|
||||||
params![from_address, from_name],
|
params![from_address, from_name],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to insert known address (from): {e}"))?;
|
.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;
|
continue;
|
||||||
}
|
}
|
||||||
let name = entry.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
let name = entry.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_contacts::Q_005), params![email, name])
|
||||||
crate::queries::sqlite::queries::db_contacts::Q_005,
|
.map_err(|e| format!("Failed to insert known address (json): {e}"))?;
|
||||||
params![email, name],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to insert known address (json): {e}"))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -274,22 +273,16 @@ fn insert_addresses_from_json(conn: &Connection, json: &str) -> Result<(), Strin
|
|||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn increment_contact_count(conn: &Connection, email: &str) -> Result<(), String> {
|
pub fn increment_contact_count(conn: &Connection, email: &str) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_contacts::Q_006), params![email])
|
||||||
crate::queries::sqlite::queries::db_contacts::Q_006,
|
.map_err(|e| format!("Failed to increment contact count: {e}"))?;
|
||||||
params![email],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to increment contact count: {e}"))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn auto_add_contact(conn: &Connection, email: &str, name: &str) -> Result<(), String> {
|
pub fn auto_add_contact(conn: &Connection, email: &str, name: &str) -> Result<(), String> {
|
||||||
let id = Uuid::new_v4().to_string();
|
let id = Uuid::new_v4().to_string();
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_contacts::Q_007), params![id, email, name])
|
||||||
crate::queries::sqlite::queries::db_contacts::Q_007,
|
.map_err(|e| format!("Failed to auto-add contact: {e}"))?;
|
||||||
params![id, email, name],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to auto-add contact: {e}"))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +293,7 @@ pub fn set_contact_photo(
|
|||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_contacts::Q_008,
|
crate::dbq!(db_contacts::Q_008),
|
||||||
params![contact_id, content_type, data],
|
params![contact_id, content_type, data],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to save contact photo: {e}"))?;
|
.map_err(|e| format!("Failed to save contact photo: {e}"))?;
|
||||||
@@ -312,23 +305,20 @@ pub fn get_contact_photo(
|
|||||||
contact_id: &str,
|
contact_id: &str,
|
||||||
) -> Result<Option<(String, Vec<u8>)>, String> {
|
) -> Result<Option<(String, Vec<u8>)>, String> {
|
||||||
match conn.query_row(
|
match conn.query_row(
|
||||||
crate::queries::sqlite::queries::db_contacts::Q_009,
|
crate::dbq!(db_contacts::Q_009),
|
||||||
params![contact_id],
|
params![contact_id],
|
||||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||||
) {
|
) {
|
||||||
Ok(value) => Ok(Some(value)),
|
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}")),
|
Err(e) => Err(format!("Failed to load contact photo: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_contact_photo(conn: &Connection, contact_id: &str) -> Result<bool, String> {
|
pub fn delete_contact_photo(conn: &Connection, contact_id: &str) -> Result<bool, String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_contacts::Q_010), params![contact_id])
|
||||||
crate::queries::sqlite::queries::db_contacts::Q_010,
|
.map(|n| n > 0)
|
||||||
params![contact_id],
|
.map_err(|e| format!("Failed to delete contact photo: {e}"))
|
||||||
)
|
|
||||||
.map(|n| n > 0)
|
|
||||||
.map_err(|e| format!("Failed to delete contact photo: {e}"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use rusqlite::Connection;
|
use crate::db::pool::{Connection, DbValue, ToDbValue};
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -184,70 +184,66 @@ fn defaults() -> DisplayPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_preferences(conn: &Connection) -> Result<DisplayPreferences, String> {
|
pub fn get_preferences(conn: &Connection) -> Result<DisplayPreferences, String> {
|
||||||
let result = conn.query_row(
|
let result = conn.query_row(crate::dbq!(db_display_preferences::Q_001), [], |row| {
|
||||||
crate::queries::sqlite::queries::db_display_preferences::Q_001,
|
Ok(DisplayPreferences {
|
||||||
[],
|
density: row.get(0)?,
|
||||||
|row| {
|
theme: row.get(1)?,
|
||||||
Ok(DisplayPreferences {
|
language: row.get(2)?,
|
||||||
density: row.get(0)?,
|
compose_format: row.get(3)?,
|
||||||
theme: row.get(1)?,
|
deep_index: row.get::<_, i32>(4)? != 0,
|
||||||
language: row.get(2)?,
|
animation_mode: row.get(5)?,
|
||||||
compose_format: row.get(3)?,
|
preview_pane: row.get(6)?,
|
||||||
deep_index: row.get::<_, i32>(4)? != 0,
|
ui_scale: row.get(7)?,
|
||||||
animation_mode: row.get(5)?,
|
color_preset: row.get(8)?,
|
||||||
preview_pane: row.get(6)?,
|
accent_color: row.get(9)?,
|
||||||
ui_scale: row.get(7)?,
|
background_color: row.get(10)?,
|
||||||
color_preset: row.get(8)?,
|
panel_color: row.get(11)?,
|
||||||
accent_color: row.get(9)?,
|
text_color: row.get(12)?,
|
||||||
background_color: row.get(10)?,
|
font_family: row.get(13)?,
|
||||||
panel_color: row.get(11)?,
|
font_size: row.get(14)?,
|
||||||
text_color: row.get(12)?,
|
row_height: row.get(15)?,
|
||||||
font_family: row.get(13)?,
|
folder_pane_width: row.get(16)?,
|
||||||
font_size: row.get(14)?,
|
message_pane_width: row.get(17)?,
|
||||||
row_height: row.get(15)?,
|
border_radius: row.get(18)?,
|
||||||
folder_pane_width: row.get(16)?,
|
contrast: row.get(19)?,
|
||||||
message_pane_width: row.get(17)?,
|
show_avatars: row.get::<_, i32>(20)? != 0,
|
||||||
border_radius: row.get(18)?,
|
show_attachment_icons: row.get::<_, i32>(21)? != 0,
|
||||||
contrast: row.get(19)?,
|
show_flags: row.get::<_, i32>(22)? != 0,
|
||||||
show_avatars: row.get::<_, i32>(20)? != 0,
|
reduce_motion: row.get::<_, i32>(23)? != 0,
|
||||||
show_attachment_icons: row.get::<_, i32>(21)? != 0,
|
date_format: row.get(24)?,
|
||||||
show_flags: row.get::<_, i32>(22)? != 0,
|
time_format: row.get(25)?,
|
||||||
reduce_motion: row.get::<_, i32>(23)? != 0,
|
compose_autosave_ms: row.get(26)?,
|
||||||
date_format: row.get(24)?,
|
default_priority: row.get(27)?,
|
||||||
time_format: row.get(25)?,
|
request_mdn_default: row.get::<_, i32>(28)? != 0,
|
||||||
compose_autosave_ms: row.get(26)?,
|
attachment_reminder: row.get::<_, i32>(29)? != 0,
|
||||||
default_priority: row.get(27)?,
|
icon_size: row.get(30)?,
|
||||||
request_mdn_default: row.get::<_, i32>(28)? != 0,
|
toolbar_density: row.get(31)?,
|
||||||
attachment_reminder: row.get::<_, i32>(29)? != 0,
|
sidebar_item_height: row.get(32)?,
|
||||||
icon_size: row.get(30)?,
|
reader_max_width: row.get(33)?,
|
||||||
toolbar_density: row.get(31)?,
|
composer_width: row.get(34)?,
|
||||||
sidebar_item_height: row.get(32)?,
|
selection_color: row.get(35)?,
|
||||||
reader_max_width: row.get(33)?,
|
unread_font_weight: row.get(36)?,
|
||||||
composer_width: row.get(34)?,
|
unread_badge_style: row.get(37)?,
|
||||||
selection_color: row.get(35)?,
|
compact_headers: row.get::<_, i32>(38)? != 0,
|
||||||
unread_font_weight: row.get(36)?,
|
show_borders: row.get::<_, i32>(39)? != 0,
|
||||||
unread_badge_style: row.get(37)?,
|
scrollbar_density: row.get(40)?,
|
||||||
compact_headers: row.get::<_, i32>(38)? != 0,
|
monochrome: row.get::<_, i32>(41)? != 0,
|
||||||
show_borders: row.get::<_, i32>(39)? != 0,
|
folder_background: row.get(42)?,
|
||||||
scrollbar_density: row.get(40)?,
|
message_background: row.get(43)?,
|
||||||
monochrome: row.get::<_, i32>(41)? != 0,
|
reader_background: row.get(44)?,
|
||||||
folder_background: row.get(42)?,
|
compose_font_family: row.get(45)?,
|
||||||
message_background: row.get(43)?,
|
reply_position: row.get(46)?,
|
||||||
reader_background: row.get(44)?,
|
quote_style: row.get(47)?,
|
||||||
compose_font_family: row.get(45)?,
|
forward_mode: row.get(48)?,
|
||||||
reply_position: row.get(46)?,
|
spellcheck_enabled: row.get::<_, i32>(49)? != 0,
|
||||||
quote_style: row.get(47)?,
|
interface_skin: row.get(50)?,
|
||||||
forward_mode: row.get(48)?,
|
preset_scale_unlocked: row.get::<_, i32>(51)? != 0,
|
||||||
spellcheck_enabled: row.get::<_, i32>(49)? != 0,
|
updated_at: row.get(52)?,
|
||||||
interface_skin: row.get(50)?,
|
})
|
||||||
preset_scale_unlocked: row.get::<_, i32>(51)? != 0,
|
});
|
||||||
updated_at: row.get(52)?,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
match result {
|
match result {
|
||||||
Ok(value) => Ok(value),
|
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}")),
|
Err(error) => Err(format!("Failed to get display preferences: {error}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,20 +270,17 @@ pub fn update_preferences(
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
data: &UpdateDisplayPreferences,
|
data: &UpdateDisplayPreferences,
|
||||||
) -> Result<DisplayPreferences, String> {
|
) -> Result<DisplayPreferences, String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_display_preferences::Q_002), [])
|
||||||
crate::queries::sqlite::queries::db_display_preferences::Q_002,
|
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
|
||||||
[],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
|
|
||||||
|
|
||||||
let mut sets = Vec::new();
|
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;
|
let mut idx = 1usize;
|
||||||
macro_rules! set_text {
|
macro_rules! set_text {
|
||||||
($field:ident, $column:literal) => {
|
($field:ident, $column:literal) => {
|
||||||
if let Some(ref value) = data.$field {
|
if let Some(ref value) = data.$field {
|
||||||
sets.push(format!(concat!($column, " = ?{}"), idx));
|
sets.push(format!(concat!($column, " = ?{}"), idx));
|
||||||
values.push(Box::new(value.clone()));
|
values.push(value.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -296,7 +289,7 @@ pub fn update_preferences(
|
|||||||
($field:ident, $column:literal) => {
|
($field:ident, $column:literal) => {
|
||||||
if let Some(value) = data.$field {
|
if let Some(value) = data.$field {
|
||||||
sets.push(format!(concat!($column, " = ?{}"), idx));
|
sets.push(format!(concat!($column, " = ?{}"), idx));
|
||||||
values.push(Box::new(value));
|
values.push(value.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -305,7 +298,7 @@ pub fn update_preferences(
|
|||||||
($field:ident, $column:literal) => {
|
($field:ident, $column:literal) => {
|
||||||
if let Some(value) = data.$field {
|
if let Some(value) = data.$field {
|
||||||
sets.push(format!(concat!($column, " = ?{}"), idx));
|
sets.push(format!(concat!($column, " = ?{}"), idx));
|
||||||
values.push(Box::new(value as i32));
|
values.push((value as i32).to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -499,7 +492,7 @@ pub fn update_preferences(
|
|||||||
set_bool!(deep_index, "deep_index");
|
set_bool!(deep_index, "deep_index");
|
||||||
if let Some(ref value) = data.animation_mode {
|
if let Some(ref value) = data.animation_mode {
|
||||||
sets.push(format!("animation_mode = ?{}", idx));
|
sets.push(format!("animation_mode = ?{}", idx));
|
||||||
values.push(Box::new(value.clone()));
|
values.push(value.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
set_text!(preview_pane, "preview_pane");
|
set_text!(preview_pane, "preview_pane");
|
||||||
@@ -549,10 +542,13 @@ pub fn update_preferences(
|
|||||||
return get_preferences(conn);
|
return get_preferences(conn);
|
||||||
}
|
}
|
||||||
sets.push("updated_at = datetime('now')".to_string());
|
sets.push("updated_at = datetime('now')".to_string());
|
||||||
let sql = crate::rustbox_query_db_display_preferences_001!(sets.join(", "), idx);
|
let sql = format!(
|
||||||
values.push(Box::new(1_i32));
|
"UPDATE display_preferences SET {} WHERE id = ?{}",
|
||||||
let refs: Vec<&dyn rusqlite::types::ToSql> = values.iter().map(|v| v.as_ref()).collect();
|
sets.join(", "),
|
||||||
conn.execute(&sql, refs.as_slice())
|
idx
|
||||||
|
);
|
||||||
|
values.push(1_i32.to_db_value());
|
||||||
|
conn.execute(&sql, values)
|
||||||
.map_err(|e| format!("Failed to update display preferences: {e}"))?;
|
.map_err(|e| format!("Failed to update display preferences: {e}"))?;
|
||||||
get_preferences(conn)
|
get_preferences(conn)
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-44
@@ -1,4 +1,4 @@
|
|||||||
use rusqlite::{Connection, params};
|
use crate::db::pool::{Connection, params};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -72,7 +72,7 @@ pub fn upsert_draft(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
validate_draft_id(id)?;
|
validate_draft_id(id)?;
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_drafts::Q_001,
|
crate::dbq!(db_drafts::Q_001),
|
||||||
params![
|
params![
|
||||||
id,
|
id,
|
||||||
to_addresses,
|
to_addresses,
|
||||||
@@ -97,46 +97,42 @@ pub fn upsert_draft(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_draft(conn: &Connection, id: &str) -> Result<Option<Draft>, String> {
|
pub fn get_draft(conn: &Connection, id: &str) -> Result<Option<Draft>, String> {
|
||||||
let result = conn.query_row(
|
let result = conn.query_row(crate::dbq!(db_drafts::Q_002), params![id], |row| {
|
||||||
crate::queries::sqlite::queries::db_drafts::Q_002,
|
Ok(Draft {
|
||||||
params![id],
|
id: row.get(0)?,
|
||||||
|row| {
|
to_addresses: row.get(1)?,
|
||||||
Ok(Draft {
|
cc_addresses: row.get(2)?,
|
||||||
id: row.get(0)?,
|
bcc_addresses: row.get(3)?,
|
||||||
to_addresses: row.get(1)?,
|
subject: row.get(4)?,
|
||||||
cc_addresses: row.get(2)?,
|
text_body: row.get(5)?,
|
||||||
bcc_addresses: row.get(3)?,
|
html_body: row.get(6)?,
|
||||||
subject: row.get(4)?,
|
in_reply_to: row.get(7)?,
|
||||||
text_body: row.get(5)?,
|
references_header: row.get(8)?,
|
||||||
html_body: row.get(6)?,
|
request_mdn: row.get::<_, i64>(9)? != 0,
|
||||||
in_reply_to: row.get(7)?,
|
priority: row.get(10)?,
|
||||||
references_header: row.get(8)?,
|
compose_format: row.get(11)?,
|
||||||
request_mdn: row.get::<_, i64>(9)? != 0,
|
pgp_mode: row.get(12)?,
|
||||||
priority: row.get(10)?,
|
pgp_inline: row.get::<_, i64>(13)? != 0,
|
||||||
compose_format: row.get(11)?,
|
pgp_attach_public_key: row.get::<_, i64>(14)? != 0,
|
||||||
pgp_mode: row.get(12)?,
|
pgp_attach_recipient_keys: row.get::<_, i64>(15)? != 0,
|
||||||
pgp_inline: row.get::<_, i64>(13)? != 0,
|
remote_folder: row.get(16)?,
|
||||||
pgp_attach_public_key: row.get::<_, i64>(14)? != 0,
|
remote_uid: row.get(17)?,
|
||||||
pgp_attach_recipient_keys: row.get::<_, i64>(15)? != 0,
|
remote_uid_validity: row.get(18)?,
|
||||||
remote_folder: row.get(16)?,
|
created_at: row.get(19)?,
|
||||||
remote_uid: row.get(17)?,
|
updated_at: row.get(20)?,
|
||||||
remote_uid_validity: row.get(18)?,
|
})
|
||||||
created_at: row.get(19)?,
|
});
|
||||||
updated_at: row.get(20)?,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(draft) => Ok(Some(draft)),
|
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}")),
|
Err(e) => Err(format!("Failed to get draft: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_drafts(conn: &Connection) -> Result<Vec<Draft>, String> {
|
pub fn list_drafts(conn: &Connection) -> Result<Vec<Draft>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare list_drafts: {e}"))?;
|
||||||
|
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
@@ -183,7 +179,7 @@ pub fn set_remote_location(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let updated = conn
|
let updated = conn
|
||||||
.execute(
|
.execute(
|
||||||
crate::queries::sqlite::queries::db_drafts::Q_004,
|
crate::dbq!(db_drafts::Q_004),
|
||||||
params![id, folder, uid, uid_validity],
|
params![id, folder, uid, uid_validity],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to update remote draft location: {e}"))?;
|
.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> {
|
pub fn delete_draft(conn: &Connection, id: &str) -> Result<bool, String> {
|
||||||
let deleted = conn
|
let deleted = conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_drafts::Q_005), params![id])
|
||||||
crate::queries::sqlite::queries::db_drafts::Q_005,
|
|
||||||
params![id],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to delete draft: {e}"))?;
|
.map_err(|e| format!("Failed to delete draft: {e}"))?;
|
||||||
Ok(deleted > 0)
|
Ok(deleted > 0)
|
||||||
}
|
}
|
||||||
@@ -213,7 +206,7 @@ pub fn add_draft_attachment(
|
|||||||
file_path: &str,
|
file_path: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
conn.execute(
|
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],
|
params![id, draft_id, filename, content_type, size, file_path],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to add draft attachment: {e}"))?;
|
.map_err(|e| format!("Failed to add draft attachment: {e}"))?;
|
||||||
@@ -225,7 +218,7 @@ pub fn get_draft_attachments(
|
|||||||
draft_id: &str,
|
draft_id: &str,
|
||||||
) -> Result<Vec<DraftAttachment>, String> {
|
) -> Result<Vec<DraftAttachment>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare get_draft_attachments: {e}"))?;
|
||||||
|
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
@@ -251,10 +244,7 @@ pub fn get_draft_attachments(
|
|||||||
|
|
||||||
pub fn delete_draft_attachment(conn: &Connection, id: &str) -> Result<bool, String> {
|
pub fn delete_draft_attachment(conn: &Connection, id: &str) -> Result<bool, String> {
|
||||||
let deleted = conn
|
let deleted = conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_drafts::Q_008), params![id])
|
||||||
crate::queries::sqlite::queries::db_drafts::Q_008,
|
|
||||||
params![id],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to delete draft attachment: {e}"))?;
|
.map_err(|e| format!("Failed to delete draft attachment: {e}"))?;
|
||||||
Ok(deleted > 0)
|
Ok(deleted > 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use rusqlite::{Connection, OptionalExtension, params};
|
use crate::db::pool::{Connection, OptionalExtension, params};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -13,11 +13,9 @@ pub struct UpdateFolderPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_override(conn: &Connection, folder: &str) -> Result<Option<bool>, String> {
|
fn get_override(conn: &Connection, folder: &str) -> Result<Option<bool>, String> {
|
||||||
conn.query_row(
|
conn.query_row(crate::dbq!(db_folder_preferences::Q_001), [folder], |row| {
|
||||||
crate::queries::sqlite::queries::db_folder_preferences::Q_001,
|
row.get::<_, Option<i32>>(0)
|
||||||
[folder],
|
})
|
||||||
|row| row.get::<_, Option<i32>>(0),
|
|
||||||
)
|
|
||||||
.optional()
|
.optional()
|
||||||
.map(|value| value.flatten().map(|value| value != 0))
|
.map(|value| value.flatten().map(|value| value != 0))
|
||||||
.map_err(|error| format!("Failed to read folder preferences: {error}"))
|
.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);
|
return Ok(value);
|
||||||
}
|
}
|
||||||
let global = conn
|
let global = conn
|
||||||
.query_row(
|
.query_row(crate::dbq!(db_folder_preferences::Q_002), [], |row| {
|
||||||
crate::queries::sqlite::queries::db_folder_preferences::Q_002,
|
row.get::<_, i32>(0)
|
||||||
[],
|
})
|
||||||
|row| row.get::<_, i32>(0),
|
|
||||||
)
|
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
Ok(global != 0)
|
Ok(global != 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_preferences(conn: &Connection, folder: &str) -> Result<FolderPreferences, String> {
|
pub fn get_preferences(conn: &Connection, folder: &str) -> Result<FolderPreferences, String> {
|
||||||
let exists: bool = conn
|
let exists: bool = conn
|
||||||
.query_row(
|
.query_row(crate::dbq!(db_folder_preferences::Q_003), [folder], |row| {
|
||||||
crate::queries::sqlite::queries::db_folder_preferences::Q_003,
|
row.get(0)
|
||||||
[folder],
|
})
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|error| format!("Failed to validate folder: {error}"))?;
|
.map_err(|error| format!("Failed to validate folder: {error}"))?;
|
||||||
if !exists {
|
if !exists {
|
||||||
return Err(format!("Folder does not exist: {folder}"));
|
return Err(format!("Folder does not exist: {folder}"));
|
||||||
}
|
}
|
||||||
let flagged_on_top = get_override(conn, folder)?;
|
let flagged_on_top = get_override(conn, folder)?;
|
||||||
let effective_flagged_on_top = flagged_on_top.unwrap_or(
|
let effective_flagged_on_top = flagged_on_top.unwrap_or(
|
||||||
conn.query_row(
|
conn.query_row(crate::dbq!(db_folder_preferences::Q_004), [], |row| {
|
||||||
crate::queries::sqlite::queries::db_folder_preferences::Q_004,
|
row.get::<_, i32>(0)
|
||||||
[],
|
})
|
||||||
|row| row.get::<_, i32>(0),
|
|
||||||
)
|
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
!= 0,
|
!= 0,
|
||||||
);
|
);
|
||||||
@@ -70,11 +62,9 @@ pub fn update_preferences(
|
|||||||
data: &UpdateFolderPreferences,
|
data: &UpdateFolderPreferences,
|
||||||
) -> Result<FolderPreferences, String> {
|
) -> Result<FolderPreferences, String> {
|
||||||
let exists: bool = conn
|
let exists: bool = conn
|
||||||
.query_row(
|
.query_row(crate::dbq!(db_folder_preferences::Q_005), [folder], |row| {
|
||||||
crate::queries::sqlite::queries::db_folder_preferences::Q_005,
|
row.get(0)
|
||||||
[folder],
|
})
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|error| format!("Failed to validate folder: {error}"))?;
|
.map_err(|error| format!("Failed to validate folder: {error}"))?;
|
||||||
if !exists {
|
if !exists {
|
||||||
return Err(format!("Folder does not exist: {folder}"));
|
return Err(format!("Folder does not exist: {folder}"));
|
||||||
@@ -83,17 +73,14 @@ pub fn update_preferences(
|
|||||||
match data.flagged_on_top {
|
match data.flagged_on_top {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_folder_preferences::Q_006,
|
crate::dbq!(db_folder_preferences::Q_006),
|
||||||
params![folder, value as i32],
|
params![folder, value as i32],
|
||||||
)
|
)
|
||||||
.map_err(|error| format!("Failed to update folder preferences: {error}"))?;
|
.map_err(|error| format!("Failed to update folder preferences: {error}"))?;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_folder_preferences::Q_007), [folder])
|
||||||
crate::queries::sqlite::queries::db_folder_preferences::Q_007,
|
.map_err(|error| format!("Failed to clear folder preferences: {error}"))?;
|
||||||
[folder],
|
|
||||||
)
|
|
||||||
.map_err(|error| format!("Failed to clear folder preferences: {error}"))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+63
-107
@@ -1,4 +1,4 @@
|
|||||||
use rusqlite::{Connection, OptionalExtension, params};
|
use crate::db::pool::{Connection, DbValue, OptionalExtension, ToDbValue, params};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -28,7 +28,7 @@ pub fn upsert_folder(
|
|||||||
highest_modseq: u64,
|
highest_modseq: u64,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_folders::Q_001,
|
crate::dbq!(db_folders::Q_001),
|
||||||
params![
|
params![
|
||||||
folder_name,
|
folder_name,
|
||||||
delimiter,
|
delimiter,
|
||||||
@@ -52,7 +52,7 @@ pub fn insert_folder_if_new(
|
|||||||
flags_csv: &str,
|
flags_csv: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_folders::Q_002,
|
crate::dbq!(db_folders::Q_002),
|
||||||
params![folder_name, delimiter, flags_csv],
|
params![folder_name, delimiter, flags_csv],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to insert folder: {e}"))?;
|
.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> {
|
pub fn get_all_folders(conn: &Connection) -> Result<Vec<CachedFolder>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare get_all_folders: {e}"))?;
|
||||||
|
|
||||||
let rows = stmt
|
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> {
|
pub fn remove_stale_folders(conn: &Connection, current_names: &[String]) -> Result<usize, String> {
|
||||||
if current_names.is_empty() {
|
if current_names.is_empty() {
|
||||||
let deleted = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to delete all folders: {e}"))?;
|
||||||
return Ok(deleted);
|
return Ok(deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
let placeholders: Vec<String> = (1..=current_names.len()).map(|i| format!("?{i}")).collect();
|
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
|
let params: Vec<DbValue> = current_names.iter().map(ToDbValue::to_db_value).collect();
|
||||||
.iter()
|
|
||||||
.map(|n| n as &dyn rusqlite::types::ToSql)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let deleted = conn
|
let deleted = conn
|
||||||
.execute(&sql, params.as_slice())
|
.execute(&sql, params)
|
||||||
.map_err(|e| format!("Failed to remove stale folders: {e}"))?;
|
.map_err(|e| format!("Failed to remove stale folders: {e}"))?;
|
||||||
|
|
||||||
Ok(deleted)
|
Ok(deleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn touch_all_folders(conn: &Connection) -> Result<(), String> {
|
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}"))?;
|
.map_err(|e| format!("Failed to touch folders: {e}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ pub fn update_folder_status(
|
|||||||
total_count: u32,
|
total_count: u32,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_folders::Q_006,
|
crate::dbq!(db_folders::Q_006),
|
||||||
params![uid_validity, total_count, name],
|
params![uid_validity, total_count, name],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to update folder status: {e}"))?;
|
.map_err(|e| format!("Failed to update folder status: {e}"))?;
|
||||||
@@ -140,7 +140,7 @@ pub fn update_folder_sync_status(
|
|||||||
highest_modseq: u64,
|
highest_modseq: u64,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
conn.execute(
|
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],
|
params![uid_validity, total_count, highest_modseq as i64, name],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to update folder sync status: {e}"))?;
|
.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> {
|
pub fn refresh_unread_count(conn: &Connection, folder_name: &str) -> Result<(), String> {
|
||||||
let unread: u32 = conn
|
let unread: u32 = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
crate::queries::sqlite::queries::db_folders::Q_008,
|
crate::dbq!(db_folders::Q_008),
|
||||||
params![folder_name],
|
params![folder_name],
|
||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to count unread messages: {e}"))?;
|
.map_err(|e| format!("Failed to count unread messages: {e}"))?;
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_folders::Q_009), params![unread, folder_name])
|
||||||
crate::queries::sqlite::queries::db_folders::Q_009,
|
.map_err(|e| format!("Failed to update unread_count: {e}"))?;
|
||||||
params![unread, folder_name],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to update unread_count: {e}"))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adjust_unread_count(conn: &Connection, folder_name: &str, delta: i32) -> Result<(), String> {
|
pub fn adjust_unread_count(conn: &Connection, folder_name: &str, delta: i32) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_folders::Q_010), params![delta, folder_name])
|
||||||
crate::queries::sqlite::queries::db_folders::Q_010,
|
.map_err(|e| format!("Failed to adjust unread_count: {e}"))?;
|
||||||
params![delta, folder_name],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to adjust unread_count: {e}"))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_folder_cache_fresh(conn: &Connection, max_age_secs: u32) -> Result<bool, String> {
|
pub fn is_folder_cache_fresh(conn: &Connection, max_age_secs: u32) -> Result<bool, String> {
|
||||||
let fresh: bool = conn
|
let fresh: bool = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
crate::queries::sqlite::queries::db_folders::Q_011,
|
crate::dbq!(db_folders::Q_011),
|
||||||
params![format!("-{max_age_secs} seconds")],
|
params![max_age_secs],
|
||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to check folder cache freshness: {e}"))?;
|
.map_err(|e| format!("Failed to check folder cache freshness: {e}"))?;
|
||||||
@@ -192,8 +186,8 @@ pub fn is_folder_fresh(
|
|||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
let fresh: bool = conn
|
let fresh: bool = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
crate::queries::sqlite::queries::db_folders::Q_012,
|
crate::dbq!(db_folders::Q_012),
|
||||||
params![folder_name, format!("-{max_age_secs} seconds")],
|
params![folder_name, max_age_secs],
|
||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to check folder freshness: {e}"))?;
|
.map_err(|e| format!("Failed to check folder freshness: {e}"))?;
|
||||||
@@ -206,7 +200,7 @@ pub fn is_folder_messages_invalidated(
|
|||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
let invalidated: bool = conn
|
let invalidated: bool = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
crate::queries::sqlite::queries::db_folders::Q_013,
|
crate::dbq!(db_folders::Q_013),
|
||||||
params![folder_name],
|
params![folder_name],
|
||||||
|row| row.get(0),
|
|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> {
|
pub fn invalidate_folder_freshness(conn: &Connection, folder_name: &str) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_folders::Q_014), params![folder_name])
|
||||||
crate::queries::sqlite::queries::db_folders::Q_014,
|
.map_err(|e| format!("Failed to invalidate folder freshness: {e}"))?;
|
||||||
params![folder_name],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to invalidate folder freshness: {e}"))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_folder(conn: &Connection, name: &str) -> Result<Option<CachedFolder>, String> {
|
pub fn get_folder(conn: &Connection, name: &str) -> Result<Option<CachedFolder>, String> {
|
||||||
let result = conn.query_row(
|
let result = conn.query_row(crate::dbq!(db_folders::Q_015), params![name], |row| {
|
||||||
crate::queries::sqlite::queries::db_folders::Q_015,
|
let is_subscribed_int: i32 = row.get(4)?;
|
||||||
params![name],
|
let highest_modseq_int: i64 = row.get(8)?;
|
||||||
|row| {
|
Ok(CachedFolder {
|
||||||
let is_subscribed_int: i32 = row.get(4)?;
|
name: row.get(0)?,
|
||||||
let highest_modseq_int: i64 = row.get(8)?;
|
delimiter: row.get(1)?,
|
||||||
Ok(CachedFolder {
|
parent: row.get(2)?,
|
||||||
name: row.get(0)?,
|
flags: row.get(3)?,
|
||||||
delimiter: row.get(1)?,
|
is_subscribed: is_subscribed_int != 0,
|
||||||
parent: row.get(2)?,
|
total_count: row.get(5)?,
|
||||||
flags: row.get(3)?,
|
unread_count: row.get(6)?,
|
||||||
is_subscribed: is_subscribed_int != 0,
|
uid_validity: row.get(7)?,
|
||||||
total_count: row.get(5)?,
|
highest_modseq: highest_modseq_int as u64,
|
||||||
unread_count: row.get(6)?,
|
})
|
||||||
uid_validity: row.get(7)?,
|
});
|
||||||
highest_modseq: highest_modseq_int as u64,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(folder) => Ok(Some(folder)),
|
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}")),
|
Err(e) => Err(format!("Failed to get folder: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_folder_and_messages(conn: &Connection, folder_name: &str) -> Result<(), String> {
|
pub fn delete_folder_and_messages(conn: &Connection, folder_name: &str) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_folders::Q_016), params![folder_name])
|
||||||
crate::queries::sqlite::queries::db_folders::Q_016,
|
.map_err(|e| format!("Failed to delete folder messages: {e}"))?;
|
||||||
params![folder_name],
|
conn.execute(crate::dbq!(db_folders::Q_017), params![folder_name])
|
||||||
)
|
.map_err(|e| format!("Failed to delete folder: {e}"))?;
|
||||||
.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}"))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,44 +254,26 @@ pub fn rename_folder_in_cache(
|
|||||||
let tx = conn
|
let tx = conn
|
||||||
.unchecked_transaction()
|
.unchecked_transaction()
|
||||||
.map_err(|e| format!("Failed to start folder rename transaction: {e}"))?;
|
.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}"))?;
|
.map_err(|e| format!("Failed to defer folder rename foreign keys: {e}"))?;
|
||||||
|
|
||||||
let inserted = tx
|
let inserted = tx
|
||||||
.execute(
|
.execute(crate::dbq!(db_folders::Q_019), params![new_name, old_name])
|
||||||
crate::queries::sqlite::queries::db_folders::Q_019,
|
|
||||||
params![new_name, old_name],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to create renamed folder cache entry: {e}"))?;
|
.map_err(|e| format!("Failed to create renamed folder cache entry: {e}"))?;
|
||||||
if inserted == 0 {
|
if inserted == 0 {
|
||||||
return Err(format!("Folder not found in cache: {old_name}"));
|
return Err(format!("Folder not found in cache: {old_name}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.execute(
|
tx.execute(crate::dbq!(db_folders::Q_020), params![new_name, old_name])
|
||||||
crate::queries::sqlite::queries::db_folders::Q_020,
|
.map_err(|e| format!("Failed to rename messages folder: {e}"))?;
|
||||||
params![new_name, old_name],
|
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}"))?;
|
||||||
.map_err(|e| format!("Failed to rename messages folder: {e}"))?;
|
tx.execute(crate::dbq!(db_folders::Q_022), params![new_name, old_name])
|
||||||
tx.execute(
|
.ok();
|
||||||
crate::queries::sqlite::queries::db_folders::Q_021,
|
tx.execute(crate::dbq!(db_folders::Q_023), params![new_name, old_name])
|
||||||
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 rename message tag folder references: {e}"))?;
|
.map_err(|e| format!("Failed to remove old folder cache entry: {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.commit()
|
tx.commit()
|
||||||
.map_err(|e| format!("Failed to commit folder rename: {e}"))?;
|
.map_err(|e| format!("Failed to commit folder rename: {e}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -320,11 +283,9 @@ pub fn find_folder_by_attribute(
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
attribute: &str,
|
attribute: &str,
|
||||||
) -> Result<Option<String>, String> {
|
) -> Result<Option<String>, String> {
|
||||||
conn.query_row(
|
conn.query_row(crate::dbq!(db_folders::Q_025), [attribute], |row| {
|
||||||
crate::queries::sqlite::queries::db_folders::Q_025,
|
row.get(0)
|
||||||
[attribute],
|
})
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.optional()
|
.optional()
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
@@ -442,17 +403,12 @@ mod tests {
|
|||||||
fn sync_metadata_preserves_message_sync_timestamp() {
|
fn sync_metadata_preserves_message_sync_timestamp() {
|
||||||
let conn = open_test_db();
|
let conn = open_test_db();
|
||||||
insert_folder_if_new(&conn, "Sent", None, "\\Sent").unwrap();
|
insert_folder_if_new(&conn, "Sent", None, "\\Sent").unwrap();
|
||||||
conn.execute(crate::queries::sqlite::queries::db_folders::Q_026, [])
|
conn.execute(crate::dbq!(db_folders::Q_026), []).unwrap();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
update_folder_sync_status(&conn, "Sent", 1, 5, 42).unwrap();
|
update_folder_sync_status(&conn, "Sent", 1, 5, 42).unwrap();
|
||||||
|
|
||||||
let timestamp: String = conn
|
let timestamp: String = conn
|
||||||
.query_row(
|
.query_row(crate::dbq!(db_folders::Q_027), [], |row| row.get(0))
|
||||||
crate::queries::sqlite::queries::db_folders::Q_027,
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(timestamp, "2000-01-01 00:00:00");
|
assert_eq!(timestamp, "2000-01-01 00:00:00");
|
||||||
assert!(!is_folder_fresh(&conn, "Sent", 30).unwrap());
|
assert!(!is_folder_fresh(&conn, "Sent", 30).unwrap());
|
||||||
|
|||||||
+9
-12
@@ -1,4 +1,4 @@
|
|||||||
use rusqlite::{Connection, OptionalExtension, Transaction, TransactionBehavior};
|
use crate::db::pool::{Connection, OptionalExtension, Transaction};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) struct HeaderSync {
|
pub(crate) struct HeaderSync {
|
||||||
@@ -8,11 +8,9 @@ pub(crate) struct HeaderSync {
|
|||||||
impl HeaderSync {
|
impl HeaderSync {
|
||||||
pub(crate) fn capture(conn: &Connection) -> Result<Self, String> {
|
pub(crate) fn capture(conn: &Connection) -> Result<Self, String> {
|
||||||
let generation = conn
|
let generation = conn
|
||||||
.query_row(
|
.query_row(crate::dbq!(db_header_sync::Q_001), [], |row| {
|
||||||
crate::queries::sqlite::queries::db_header_sync::Q_001,
|
row.get::<_, String>(0)
|
||||||
[],
|
})
|
||||||
|row| row.get::<_, String>(0),
|
|
||||||
)
|
|
||||||
.optional()
|
.optional()
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.map(|value| value.parse::<u64>().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 })
|
Ok(Self { generation })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn begin_write(self, conn: &Connection) -> Result<Transaction<'_>, String> {
|
pub(crate) fn begin_write(self, conn: &Connection) -> Result<Transaction, String> {
|
||||||
let transaction = Transaction::new_unchecked(conn, TransactionBehavior::Immediate)
|
let transaction = conn.unchecked_transaction().map_err(|e| e.to_string())?;
|
||||||
.map_err(|e| e.to_string())?;
|
if Self::capture(&transaction)?.generation != self.generation {
|
||||||
if Self::capture(conn)?.generation != self.generation {
|
|
||||||
return Err("Header sync superseded by cache removal; retry on next sync".into());
|
return Err("Header sync superseded by cache removal; retry on next sync".into());
|
||||||
}
|
}
|
||||||
Ok(transaction)
|
Ok(transaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn invalidate(conn: &Transaction<'_>) -> Result<(), String> {
|
pub(crate) fn invalidate(conn: &Transaction) -> Result<(), String> {
|
||||||
conn.execute(crate::queries::sqlite::queries::db_header_sync::Q_002, [])
|
conn.execute(crate::dbq!(db_header_sync::Q_002), [])
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
+47
-49
@@ -1,5 +1,5 @@
|
|||||||
|
use crate::db::pool::{Connection, DbValue, ToDbValue, params};
|
||||||
use crate::queries::sqlite::queries::db_identities::IDENTITY_SELECT_COLS;
|
use crate::queries::sqlite::queries::db_identities::IDENTITY_SELECT_COLS;
|
||||||
use rusqlite::{Connection, params};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, 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 {
|
Ok(Identity {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
display_name: row.get(1)?,
|
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> {
|
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
|
let mut stmt = conn
|
||||||
.prepare(&sql)
|
.prepare(&sql)
|
||||||
.map_err(|e| format!("Failed to prepare list_identities: {e}"))?;
|
.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> {
|
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) {
|
match conn.query_row(&sql, params![id], row_to_identity) {
|
||||||
Ok(identity) => Ok(Some(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}")),
|
Err(e) => Err(format!("Failed to get identity: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn get_default_identity(conn: &Connection) -> Result<Option<Identity>, String> {
|
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) {
|
match conn.query_row(&sql, [], row_to_identity) {
|
||||||
Ok(identity) => Ok(Some(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}")),
|
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 {
|
if data.is_default {
|
||||||
clear_default(conn)?;
|
clear_default(conn)?;
|
||||||
}
|
}
|
||||||
conn.execute(
|
let id = conn
|
||||||
crate::queries::sqlite::queries::db_identities::Q_001,
|
.insert_id(
|
||||||
params![
|
crate::dbq!(db_identities::Q_001),
|
||||||
data.display_name,
|
params![
|
||||||
data.email,
|
data.display_name,
|
||||||
data.signature_html,
|
data.email,
|
||||||
data.is_default as i32,
|
data.signature_html,
|
||||||
data.reply_to,
|
data.is_default as i32,
|
||||||
data.bcc,
|
data.reply_to,
|
||||||
data.organization,
|
data.bcc,
|
||||||
data.signature_text,
|
data.organization,
|
||||||
data.signature_new as i32,
|
data.signature_text,
|
||||||
data.signature_reply as i32,
|
data.signature_new as i32,
|
||||||
data.signature_forward as i32,
|
data.signature_reply as i32,
|
||||||
data.signature_position,
|
data.signature_forward as i32,
|
||||||
data.pgp_fingerprint,
|
data.signature_position,
|
||||||
data.pgp_sign_default as i32,
|
data.pgp_fingerprint,
|
||||||
data.pgp_encrypt_default as i32,
|
data.pgp_sign_default as i32,
|
||||||
data.pgp_inline_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();
|
.map_err(|e| format!("Failed to create identity: {e}"))?;
|
||||||
get_identity(conn, id)?.ok_or_else(|| "Failed to read back created identity".to_string())
|
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)?;
|
validate_signature_position(value)?;
|
||||||
}
|
}
|
||||||
let mut sets = Vec::new();
|
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;
|
let mut idx = 1usize;
|
||||||
macro_rules! text {
|
macro_rules! text {
|
||||||
($field:ident,$column:literal) => {
|
($field:ident,$column:literal) => {
|
||||||
if let Some(ref value) = data.$field {
|
if let Some(ref value) = data.$field {
|
||||||
sets.push(format!(concat!($column, " = ?{}"), idx));
|
sets.push(format!(concat!($column, " = ?{}"), idx));
|
||||||
values.push(Box::new(value.clone()));
|
values.push(value.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -211,7 +214,7 @@ pub fn update_identity(
|
|||||||
($field:ident,$column:literal) => {
|
($field:ident,$column:literal) => {
|
||||||
if let Some(value) = data.$field {
|
if let Some(value) = data.$field {
|
||||||
sets.push(format!(concat!($column, " = ?{}"), idx));
|
sets.push(format!(concat!($column, " = ?{}"), idx));
|
||||||
values.push(Box::new(value as i32));
|
values.push((value as i32).to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -230,7 +233,7 @@ pub fn update_identity(
|
|||||||
text!(signature_position, "signature_position");
|
text!(signature_position, "signature_position");
|
||||||
if let Some(ref value) = data.pgp_fingerprint {
|
if let Some(ref value) = data.pgp_fingerprint {
|
||||||
sets.push(format!("pgp_fingerprint = ?{}", idx));
|
sets.push(format!("pgp_fingerprint = ?{}", idx));
|
||||||
values.push(Box::new(value.clone()));
|
values.push(value.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
boolean!(pgp_sign_default, "pgp_sign_default");
|
boolean!(pgp_sign_default, "pgp_sign_default");
|
||||||
@@ -240,38 +243,33 @@ pub fn update_identity(
|
|||||||
return get_identity(conn, id);
|
return get_identity(conn, id);
|
||||||
}
|
}
|
||||||
sets.push("updated_at = datetime('now')".to_string());
|
sets.push("updated_at = datetime('now')".to_string());
|
||||||
let sql = crate::rustbox_query_db_identities_004!(sets.join(", "), idx);
|
let sql = format!(
|
||||||
values.push(Box::new(id));
|
"UPDATE identities SET {} WHERE id = ?{}",
|
||||||
let refs: Vec<&dyn rusqlite::types::ToSql> =
|
sets.join(", "),
|
||||||
values.iter().map(|value| value.as_ref()).collect();
|
idx
|
||||||
conn.execute(&sql, refs.as_slice())
|
);
|
||||||
|
values.push(id.to_db_value());
|
||||||
|
conn.execute(&sql, values)
|
||||||
.map_err(|e| format!("Failed to update identity: {e}"))?;
|
.map_err(|e| format!("Failed to update identity: {e}"))?;
|
||||||
get_identity(conn, id)
|
get_identity(conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_identity(conn: &Connection, id: i64) -> Result<bool, String> {
|
pub fn delete_identity(conn: &Connection, id: i64) -> Result<bool, String> {
|
||||||
Ok(conn
|
Ok(conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_identities::Q_002), params![id])
|
||||||
crate::queries::sqlite::queries::db_identities::Q_002,
|
|
||||||
params![id],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to delete identity: {e}"))?
|
.map_err(|e| format!("Failed to delete identity: {e}"))?
|
||||||
> 0)
|
> 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_identities(conn: &Connection) -> Result<bool, String> {
|
pub fn has_identities(conn: &Connection) -> Result<bool, String> {
|
||||||
let count: i64 = conn
|
let count: i64 = conn
|
||||||
.query_row(
|
.query_row(crate::dbq!(db_identities::Q_003), [], |row| row.get(0))
|
||||||
crate::queries::sqlite::queries::db_identities::Q_003,
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to count identities: {e}"))?;
|
.map_err(|e| format!("Failed to count identities: {e}"))?;
|
||||||
Ok(count > 0)
|
Ok(count > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_default(conn: &Connection) -> Result<(), String> {
|
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}"))?;
|
.map_err(|e| format!("Failed to clear default identity: {e}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-27
@@ -1,4 +1,4 @@
|
|||||||
use rusqlite::Connection;
|
use crate::db::pool::{Connection, params};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -31,25 +31,21 @@ pub struct UpdateMailPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_preferences(conn: &Connection) -> Result<MailPreferences, String> {
|
pub fn get_preferences(conn: &Connection) -> Result<MailPreferences, String> {
|
||||||
conn.query_row(
|
conn.query_row(crate::dbq!(db_mail_preferences::Q_001), [], |row| {
|
||||||
crate::queries::sqlite::queries::db_mail_preferences::Q_001,
|
Ok(MailPreferences {
|
||||||
[],
|
messages_per_page: row.get::<_, u32>(0)?,
|
||||||
|row| {
|
mark_read_delay_ms: row.get::<_, u64>(1)?,
|
||||||
Ok(MailPreferences {
|
remote_images: row.get(2)?,
|
||||||
messages_per_page: row.get::<_, u32>(0)?,
|
preferred_body: row.get(3)?,
|
||||||
mark_read_delay_ms: row.get::<_, u64>(1)?,
|
thread_mode: row.get(4)?,
|
||||||
remote_images: row.get(2)?,
|
collapse_quotes: row.get::<_, i32>(5)? != 0,
|
||||||
preferred_body: row.get(3)?,
|
show_snippets: row.get::<_, i32>(6)? != 0,
|
||||||
thread_mode: row.get(4)?,
|
default_sort: row.get(7)?,
|
||||||
collapse_quotes: row.get::<_, i32>(5)? != 0,
|
flagged_on_top: row.get::<_, i32>(8)? != 0,
|
||||||
show_snippets: row.get::<_, i32>(6)? != 0,
|
sync_imap_keywords: row.get::<_, i32>(9)? != 0,
|
||||||
default_sort: row.get(7)?,
|
updated_at: row.get(10)?,
|
||||||
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}"))
|
.map_err(|error| format!("Failed to read mail preferences: {error}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,11 +53,8 @@ pub fn update_preferences(
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
data: &UpdateMailPreferences,
|
data: &UpdateMailPreferences,
|
||||||
) -> Result<MailPreferences, String> {
|
) -> Result<MailPreferences, String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_mail_preferences::Q_002), [])
|
||||||
crate::queries::sqlite::queries::db_mail_preferences::Q_002,
|
.map_err(|error| format!("Failed to initialize mail preferences: {error}"))?;
|
||||||
[],
|
|
||||||
)
|
|
||||||
.map_err(|error| format!("Failed to initialize mail preferences: {error}"))?;
|
|
||||||
|
|
||||||
if let Some(value) = data.messages_per_page
|
if let Some(value) = data.messages_per_page
|
||||||
&& !(20..=200).contains(&value)
|
&& !(20..=200).contains(&value)
|
||||||
@@ -130,8 +123,8 @@ pub fn update_preferences(
|
|||||||
.unwrap_or(current.sync_imap_keywords);
|
.unwrap_or(current.sync_imap_keywords);
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_mail_preferences::Q_003,
|
crate::dbq!(db_mail_preferences::Q_003),
|
||||||
rusqlite::params![
|
params![
|
||||||
messages_per_page,
|
messages_per_page,
|
||||||
mark_read_delay_ms,
|
mark_read_delay_ms,
|
||||||
remote_images,
|
remote_images,
|
||||||
|
|||||||
+76
-74
@@ -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 crate::queries::sqlite::queries::db_messages::MSG_SELECT_COLS;
|
||||||
use rusqlite::{Connection, params};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -35,7 +37,9 @@ pub struct CachedMessage {
|
|||||||
pub reaction: Option<String>,
|
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)?;
|
let has_attachments_int: i32 = row.get(13)?;
|
||||||
Ok(CachedMessage {
|
Ok(CachedMessage {
|
||||||
uid: row.get(0)?,
|
uid: row.get(0)?,
|
||||||
@@ -79,7 +83,7 @@ pub fn upsert_message(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let date_epoch = parse_date_to_epoch(date);
|
let date_epoch = parse_date_to_epoch(date);
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_messages::Q_001,
|
crate::dbq!(db_messages::Q_001),
|
||||||
params![
|
params![
|
||||||
uid,
|
uid,
|
||||||
folder,
|
folder,
|
||||||
@@ -105,37 +109,30 @@ pub fn upsert_message(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_after_mutation(conn: &Connection, folder: &str, uid: u32) -> Result<(), String> {
|
pub fn remove_after_mutation(conn: &Connection, folder: &str, uid: u32) -> Result<(), String> {
|
||||||
let transaction =
|
let transaction = conn.unchecked_transaction().map_err(|e| e.to_string())?;
|
||||||
rusqlite::Transaction::new_unchecked(conn, rusqlite::TransactionBehavior::Immediate)
|
delete_message(&transaction, folder, uid)?;
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
delete_message(conn, folder, uid)?;
|
|
||||||
super::header_sync::invalidate(&transaction)?;
|
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())
|
transaction.commit().map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_folder_messages(conn: &Connection, folder: &str) -> Result<usize, String> {
|
pub fn delete_folder_messages(conn: &Connection, folder: &str) -> Result<usize, String> {
|
||||||
let deleted = conn
|
let deleted = conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_messages::Q_002), params![folder])
|
||||||
crate::queries::sqlite::queries::db_messages::Q_002,
|
|
||||||
params![folder],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to delete folder messages: {e}"))?;
|
.map_err(|e| format!("Failed to delete folder messages: {e}"))?;
|
||||||
Ok(deleted)
|
Ok(deleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn max_uid(conn: &Connection, folder: &str) -> Result<u32, String> {
|
pub fn max_uid(conn: &Connection, folder: &str) -> Result<u32, String> {
|
||||||
conn.query_row(
|
conn.query_row(crate::dbq!(db_messages::Q_003), params![folder], |row| {
|
||||||
crate::queries::sqlite::queries::db_messages::Q_003,
|
row.get(0)
|
||||||
params![folder],
|
})
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to get max uid: {e}"))
|
.map_err(|e| format!("Failed to get max uid: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_index_source_messages(conn: &Connection) -> Result<Vec<IndexSourceMessage>, String> {
|
pub fn list_index_source_messages(conn: &Connection) -> Result<Vec<IndexSourceMessage>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare index rebuild query: {e}"))?;
|
||||||
|
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
@@ -253,7 +250,7 @@ pub fn get_messages(
|
|||||||
per_page: u32,
|
per_page: u32,
|
||||||
) -> Result<Vec<CachedMessage>, String> {
|
) -> Result<Vec<CachedMessage>, String> {
|
||||||
let offset = page * per_page;
|
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
|
let mut stmt = conn
|
||||||
.prepare(&sql)
|
.prepare(&sql)
|
||||||
@@ -298,13 +295,20 @@ pub fn get_messages_sorted(
|
|||||||
_ => return Err(format!("Unsupported message sort: {sort}")),
|
_ => return Err(format!("Unsupported message sort: {sort}")),
|
||||||
};
|
};
|
||||||
let order_by = if flagged_on_top {
|
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 {
|
} else {
|
||||||
base_order.to_string()
|
base_order.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let offset = page * per_page;
|
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
|
let mut stmt = conn
|
||||||
.prepare(&sql)
|
.prepare(&sql)
|
||||||
.map_err(|e| format!("Failed to prepare sorted get_messages: {e}"))?;
|
.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> {
|
pub fn count_messages(conn: &Connection, folder: &str) -> Result<u32, String> {
|
||||||
conn.query_row(
|
conn.query_row(crate::dbq!(db_messages::Q_005), params![folder], |row| {
|
||||||
crate::queries::sqlite::queries::db_messages::Q_005,
|
row.get(0)
|
||||||
params![folder],
|
})
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to count messages: {e}"))
|
.map_err(|e| format!("Failed to count messages: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_all_messages(conn: &Connection) -> Result<usize, String> {
|
pub fn count_all_messages(conn: &Connection) -> Result<usize, String> {
|
||||||
let count: i64 = conn
|
let count: i64 = conn
|
||||||
.query_row(
|
.query_row(crate::dbq!(db_messages::Q_006), [], |row| row.get(0))
|
||||||
crate::queries::sqlite::queries::db_messages::Q_006,
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to count cached messages: {e}"))?;
|
.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())
|
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,
|
flags_csv: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_messages::Q_007,
|
crate::dbq!(db_messages::Q_007),
|
||||||
params![flags_csv, folder, uid],
|
params![flags_csv, folder, uid],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to update message flags: {e}"))?;
|
.map_err(|e| format!("Failed to update message flags: {e}"))?;
|
||||||
@@ -361,7 +359,7 @@ pub fn cache_message_body(
|
|||||||
email_theme: Option<i32>,
|
email_theme: Option<i32>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_messages::Q_008,
|
crate::dbq!(db_messages::Q_008),
|
||||||
params![
|
params![
|
||||||
html,
|
html,
|
||||||
text,
|
text,
|
||||||
@@ -383,7 +381,7 @@ pub fn update_email_theme(
|
|||||||
email_theme: i32,
|
email_theme: i32,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_messages::Q_009,
|
crate::dbq!(db_messages::Q_009),
|
||||||
params![email_theme, folder, uid],
|
params![email_theme, folder, uid],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to update email theme: {e}"))?;
|
.map_err(|e| format!("Failed to update email theme: {e}"))?;
|
||||||
@@ -404,7 +402,7 @@ pub fn get_cached_body(
|
|||||||
uid: u32,
|
uid: u32,
|
||||||
) -> Result<Option<CachedBody>, String> {
|
) -> Result<Option<CachedBody>, String> {
|
||||||
let result = conn.query_row(
|
let result = conn.query_row(
|
||||||
crate::queries::sqlite::queries::db_messages::Q_010,
|
crate::dbq!(db_messages::Q_010),
|
||||||
params![folder, uid],
|
params![folder, uid],
|
||||||
|row| {
|
|row| {
|
||||||
let cached: i32 = row.get(0)?;
|
let cached: i32 = row.get(0)?;
|
||||||
@@ -424,7 +422,7 @@ pub fn get_cached_body(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(body) => Ok(body),
|
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}")),
|
Err(e) => Err(format!("Failed to get cached body: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,29 +432,26 @@ pub fn get_single_message(
|
|||||||
folder: &str,
|
folder: &str,
|
||||||
uid: u32,
|
uid: u32,
|
||||||
) -> Result<Option<CachedMessage>, String> {
|
) -> 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);
|
let result = conn.query_row(&sql, params![folder, uid], row_to_cached_message);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(msg) => Ok(Some(msg)),
|
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}")),
|
Err(e) => Err(format!("Failed to get single message: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_message(conn: &Connection, folder: &str, uid: u32) -> Result<(), String> {
|
pub fn delete_message(conn: &Connection, folder: &str, uid: u32) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_messages::Q_011), params![folder, uid])
|
||||||
crate::queries::sqlite::queries::db_messages::Q_011,
|
.map_err(|e| format!("Failed to delete message: {e}"))?;
|
||||||
params![folder, uid],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to delete message: {e}"))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_unindexed_messages(conn: &Connection, limit: u32) -> Result<Vec<(String, u32)>, String> {
|
pub fn get_unindexed_messages(conn: &Connection, limit: u32) -> Result<Vec<(String, u32)>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare get_unindexed_messages: {e}"))?;
|
||||||
|
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
@@ -477,7 +472,7 @@ pub fn get_all_uids_and_flags(
|
|||||||
folder: &str,
|
folder: &str,
|
||||||
) -> Result<Vec<(u32, String)>, String> {
|
) -> Result<Vec<(u32, String)>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare get_all_uids_and_flags: {e}"))?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map(params![folder], |row| {
|
.query_map(params![folder], |row| {
|
||||||
@@ -496,9 +491,12 @@ pub fn get_thread_messages(
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
target_message_id: &str,
|
target_message_id: &str,
|
||||||
) -> Result<Vec<CachedMessage>, String> {
|
) -> 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 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
|
let mut stmt = conn
|
||||||
.prepare(&sql)
|
.prepare(&sql)
|
||||||
@@ -606,7 +604,7 @@ pub fn search_messages_sqlite(
|
|||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<CachedMessage>, String> {
|
) -> Result<Vec<CachedMessage>, String> {
|
||||||
let mut conditions = Vec::new();
|
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;
|
let mut idx = 1u32;
|
||||||
|
|
||||||
conditions.push("LOWER(folder) NOT IN ('trash', 'spam', 'junk')".to_string());
|
conditions.push("LOWER(folder) NOT IN ('trash', 'spam', 'junk')".to_string());
|
||||||
@@ -616,19 +614,19 @@ pub fn search_messages_sqlite(
|
|||||||
conditions.push(format!(
|
conditions.push(format!(
|
||||||
"(subject LIKE ?{idx} OR from_name LIKE ?{idx} OR from_address LIKE ?{idx} OR to_addresses LIKE ?{idx})"
|
"(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;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(subject) = subject.filter(|value| !value.trim().is_empty()) {
|
if let Some(subject) = subject.filter(|value| !value.trim().is_empty()) {
|
||||||
conditions.push(format!("subject LIKE ?{idx}"));
|
conditions.push(format!("subject LIKE ?{idx}"));
|
||||||
param_values.push(Box::new(format!("%{subject}%")));
|
param_values.push(format!("%{subject}%").to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(f) = folder {
|
if let Some(f) = folder {
|
||||||
conditions.push(format!("folder = ?{idx}"));
|
conditions.push(format!("folder = ?{idx}"));
|
||||||
param_values.push(Box::new(f.to_string()));
|
param_values.push(f.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,47 +641,57 @@ pub fn search_messages_sqlite(
|
|||||||
} else {
|
} else {
|
||||||
format!("%{f}%")
|
format!("%{f}%")
|
||||||
};
|
};
|
||||||
param_values.push(Box::new(val));
|
param_values.push(val.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(t) = to {
|
if let Some(t) = to {
|
||||||
conditions.push(format!("to_addresses LIKE ?{idx}"));
|
conditions.push(format!("to_addresses LIKE ?{idx}"));
|
||||||
param_values.push(Box::new(format!("%{t}%")));
|
param_values.push(format!("%{t}%").to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(df) = date_from {
|
if let Some(df) = date_from {
|
||||||
conditions.push(format!("date_epoch >= ?{idx}"));
|
conditions.push(format!("date_epoch >= ?{idx}"));
|
||||||
param_values.push(Box::new(df));
|
param_values.push(df.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(dt) = date_to {
|
if let Some(dt) = date_to {
|
||||||
conditions.push(format!("date_epoch <= ?{idx}"));
|
conditions.push(format!("date_epoch <= ?{idx}"));
|
||||||
param_values.push(Box::new(dt));
|
param_values.push(dt.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ha) = has_attachment {
|
if let Some(ha) = has_attachment {
|
||||||
conditions.push(format!("has_attachments = ?{idx}"));
|
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;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(want_unread) = unread {
|
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 {
|
conditions.push(if want_unread {
|
||||||
"flags NOT LIKE '%\\Seen%'".to_string()
|
format!("{seen} = 0")
|
||||||
} else {
|
} else {
|
||||||
"flags LIKE '%\\Seen%'".to_string()
|
format!("{seen} > 0")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(want_flagged) = flagged {
|
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 {
|
conditions.push(if want_flagged {
|
||||||
"flags LIKE '%\\Flagged%'".to_string()
|
format!("{flagged} > 0")
|
||||||
} else {
|
} else {
|
||||||
"flags NOT LIKE '%\\Flagged%'".to_string()
|
format!("{flagged} = 0")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,21 +703,19 @@ pub fn search_messages_sqlite(
|
|||||||
format!("WHERE {}", conditions.join(" AND "))
|
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));
|
param_values.push((limit as i64).to_db_value());
|
||||||
|
|
||||||
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
|
|
||||||
param_values.iter().map(|b| b.as_ref()).collect();
|
|
||||||
|
|
||||||
let mut stmt = conn
|
let mut stmt = conn
|
||||||
.prepare(&sql)
|
.prepare(&sql)
|
||||||
.map_err(|e| format!("Failed to prepare search query: {e}"))?;
|
.map_err(|e| format!("Failed to prepare search query: {e}"))?;
|
||||||
|
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map(rusqlite::params_from_iter(params_refs), |row| {
|
.query_map(param_values, row_to_cached_message)
|
||||||
row_to_cached_message(row)
|
|
||||||
})
|
|
||||||
.map_err(|e| format!("Failed to execute search query: {e}"))?;
|
.map_err(|e| format!("Failed to execute search query: {e}"))?;
|
||||||
|
|
||||||
let mut results = Vec::new();
|
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> {
|
pub fn cached_storage_bytes(conn: &Connection) -> Result<Option<u64>, String> {
|
||||||
conn.query_row(
|
conn.query_row(crate::dbq!(db_messages::Q_014), [], |row| row.get(0))
|
||||||
crate::queries::sqlite::queries::db_messages::Q_014,
|
.map_err(|e| e.to_string())
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use rusqlite::Connection;
|
use crate::db::pool::{Connection, DbValue, ToDbValue};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -23,28 +23,24 @@ pub struct UpdateNotificationPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_preferences(conn: &Connection) -> Result<NotificationPreferences, String> {
|
pub fn get_preferences(conn: &Connection) -> Result<NotificationPreferences, String> {
|
||||||
let result = conn.query_row(
|
let result = conn.query_row(crate::dbq!(db_notification_preferences::Q_001), [], |row| {
|
||||||
crate::queries::sqlite::queries::db_notification_preferences::Q_001,
|
let enabled_int: i32 = row.get(0)?;
|
||||||
[],
|
let sound_int: i32 = row.get(1)?;
|
||||||
|row| {
|
let folders_json: String = row.get(2)?;
|
||||||
let enabled_int: i32 = row.get(0)?;
|
let dnd_enabled_int: i32 = row.get(3)?;
|
||||||
let sound_int: i32 = row.get(1)?;
|
let dnd_start: String = row.get(4)?;
|
||||||
let folders_json: String = row.get(2)?;
|
let dnd_end: String = row.get(5)?;
|
||||||
let dnd_enabled_int: i32 = row.get(3)?;
|
let updated_at: String = row.get(6)?;
|
||||||
let dnd_start: String = row.get(4)?;
|
Ok((
|
||||||
let dnd_end: String = row.get(5)?;
|
enabled_int,
|
||||||
let updated_at: String = row.get(6)?;
|
sound_int,
|
||||||
Ok((
|
folders_json,
|
||||||
enabled_int,
|
dnd_enabled_int,
|
||||||
sound_int,
|
dnd_start,
|
||||||
folders_json,
|
dnd_end,
|
||||||
dnd_enabled_int,
|
updated_at,
|
||||||
dnd_start,
|
))
|
||||||
dnd_end,
|
});
|
||||||
updated_at,
|
|
||||||
))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok((
|
Ok((
|
||||||
@@ -68,7 +64,7 @@ pub fn get_preferences(conn: &Connection) -> Result<NotificationPreferences, Str
|
|||||||
updated_at,
|
updated_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(NotificationPreferences {
|
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(NotificationPreferences {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
sound: false,
|
sound: false,
|
||||||
folders: vec!["INBOX".to_string()],
|
folders: vec!["INBOX".to_string()],
|
||||||
@@ -85,36 +81,33 @@ pub fn update_preferences(
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
data: &UpdateNotificationPreferences,
|
data: &UpdateNotificationPreferences,
|
||||||
) -> Result<NotificationPreferences, String> {
|
) -> Result<NotificationPreferences, String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_notification_preferences::Q_002), [])
|
||||||
crate::queries::sqlite::queries::db_notification_preferences::Q_002,
|
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
|
||||||
[],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
|
|
||||||
|
|
||||||
let mut sets = Vec::new();
|
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;
|
let mut idx = 1;
|
||||||
|
|
||||||
if let Some(enabled) = data.enabled {
|
if let Some(enabled) = data.enabled {
|
||||||
sets.push(format!("enabled = ?{idx}"));
|
sets.push(format!("enabled = ?{idx}"));
|
||||||
values.push(Box::new(enabled as i32));
|
values.push((enabled as i32).to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
if let Some(sound) = data.sound {
|
if let Some(sound) = data.sound {
|
||||||
sets.push(format!("sound = ?{idx}"));
|
sets.push(format!("sound = ?{idx}"));
|
||||||
values.push(Box::new(sound as i32));
|
values.push((sound as i32).to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
if let Some(ref folders) = data.folders {
|
if let Some(ref folders) = data.folders {
|
||||||
let folders_json = serde_json::to_string(folders)
|
let folders_json = serde_json::to_string(folders)
|
||||||
.map_err(|e| format!("Failed to serialize folders: {e}"))?;
|
.map_err(|e| format!("Failed to serialize folders: {e}"))?;
|
||||||
sets.push(format!("folders = ?{idx}"));
|
sets.push(format!("folders = ?{idx}"));
|
||||||
values.push(Box::new(folders_json));
|
values.push(folders_json.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
if let Some(dnd_enabled) = data.dnd_enabled {
|
if let Some(dnd_enabled) = data.dnd_enabled {
|
||||||
sets.push(format!("dnd_enabled = ?{idx}"));
|
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;
|
idx += 1;
|
||||||
}
|
}
|
||||||
if let Some(ref dnd_start) = data.dnd_start {
|
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());
|
return Err("Invalid DND start time".to_string());
|
||||||
}
|
}
|
||||||
sets.push(format!("dnd_start = ?{idx}"));
|
sets.push(format!("dnd_start = ?{idx}"));
|
||||||
values.push(Box::new(dnd_start.clone()));
|
values.push(dnd_start.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
if let Some(ref dnd_end) = data.dnd_end {
|
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());
|
return Err("Invalid DND end time".to_string());
|
||||||
}
|
}
|
||||||
sets.push(format!("dnd_end = ?{idx}"));
|
sets.push(format!("dnd_end = ?{idx}"));
|
||||||
values.push(Box::new(dnd_end.clone()));
|
values.push(dnd_end.to_db_value());
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,12 +133,10 @@ pub fn update_preferences(
|
|||||||
|
|
||||||
sets.push("updated_at = datetime('now')".to_string());
|
sets.push("updated_at = datetime('now')".to_string());
|
||||||
let set_clause = sets.join(", ");
|
let set_clause = sets.join(", ");
|
||||||
let sql = crate::rustbox_query_db_notification_preferences_001!(&set_clause, idx);
|
let sql = format!("UPDATE notification_preferences SET {set_clause} WHERE id = ?{idx}");
|
||||||
values.push(Box::new(1_i32));
|
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, values)
|
||||||
|
|
||||||
conn.execute(&sql, params_refs.as_slice())
|
|
||||||
.map_err(|e| format!("Failed to update notification preferences: {e}"))?;
|
.map_err(|e| format!("Failed to update notification preferences: {e}"))?;
|
||||||
|
|
||||||
get_preferences(conn)
|
get_preferences(conn)
|
||||||
|
|||||||
+14
-14
@@ -1,5 +1,5 @@
|
|||||||
|
use crate::db::pool::{Connection, params};
|
||||||
use crate::queries::sqlite::queries::db_pgp::KEY_COLS;
|
use crate::queries::sqlite::queries::db_pgp::KEY_COLS;
|
||||||
use rusqlite::{Connection, params};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -31,7 +31,7 @@ pub struct PgpPreferences {
|
|||||||
pub require_trusted_encryption: bool,
|
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 {
|
Ok(PgpKeyInternal {
|
||||||
record: PgpKeyRecord {
|
record: PgpKeyRecord {
|
||||||
fingerprint: row.get(0)?,
|
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> {
|
pub fn list_keys(conn: &Connection) -> Result<Vec<PgpKeyRecord>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare PGP key list: {e}"))?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map([], row_to_key)
|
.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> {
|
pub fn get_key(conn: &Connection, fingerprint: &str) -> Result<Option<PgpKeyInternal>, String> {
|
||||||
let normalized = normalize_fingerprint(fingerprint);
|
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) {
|
match conn.query_row(&sql, params![normalized], row_to_key) {
|
||||||
Ok(v) => Ok(Some(v)),
|
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}")),
|
Err(e) => Err(format!("Failed to get PGP key: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_keys_by_email(conn: &Connection, email: &str) -> Result<Vec<PgpKeyRecord>, String> {
|
pub fn find_keys_by_email(conn: &Connection, email: &str) -> Result<Vec<PgpKeyRecord>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare PGP email lookup: {e}"))?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map(params![email.trim()], row_to_key)
|
.query_map(params![email.trim()], row_to_key)
|
||||||
@@ -93,7 +93,7 @@ pub fn upsert_key(
|
|||||||
secret_key_path: Option<&str>,
|
secret_key_path: Option<&str>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_pgp::Q_001,
|
crate::dbq!(db_pgp::Q_001),
|
||||||
params![
|
params![
|
||||||
record.fingerprint,
|
record.fingerprint,
|
||||||
record.key_id,
|
record.key_id,
|
||||||
@@ -113,7 +113,7 @@ pub fn upsert_key(
|
|||||||
pub fn delete_key(conn: &Connection, fingerprint: &str) -> Result<bool, String> {
|
pub fn delete_key(conn: &Connection, fingerprint: &str) -> Result<bool, String> {
|
||||||
Ok(conn
|
Ok(conn
|
||||||
.execute(
|
.execute(
|
||||||
crate::queries::sqlite::queries::db_pgp::Q_002,
|
crate::dbq!(db_pgp::Q_002),
|
||||||
params![normalize_fingerprint(fingerprint)],
|
params![normalize_fingerprint(fingerprint)],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to delete PGP key: {e}"))?
|
.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
|
Ok(conn
|
||||||
.execute(
|
.execute(
|
||||||
crate::queries::sqlite::queries::db_pgp::Q_003,
|
crate::dbq!(db_pgp::Q_003),
|
||||||
params![normalize_fingerprint(fingerprint), trust],
|
params![normalize_fingerprint(fingerprint), trust],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to update PGP trust: {e}"))?
|
.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> {
|
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 {
|
Ok(PgpPreferences {
|
||||||
wkd_enabled: row.get::<_, i32>(0)? != 0,
|
wkd_enabled: row.get::<_, i32>(0)? != 0,
|
||||||
hkp_enabled: row.get::<_, i32>(1)? != 0,
|
hkp_enabled: row.get::<_, i32>(1)? != 0,
|
||||||
@@ -149,7 +149,7 @@ pub fn update_preferences(
|
|||||||
prefs: &PgpPreferences,
|
prefs: &PgpPreferences,
|
||||||
) -> Result<PgpPreferences, String> {
|
) -> Result<PgpPreferences, String> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_pgp::Q_005,
|
crate::dbq!(db_pgp::Q_005),
|
||||||
params![
|
params![
|
||||||
prefs.wkd_enabled as i32,
|
prefs.wkd_enabled as i32,
|
||||||
prefs.hkp_enabled as i32,
|
prefs.hkp_enabled as i32,
|
||||||
@@ -172,17 +172,17 @@ pub fn find_key_internal_by_email(
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
email: &str,
|
email: &str,
|
||||||
) -> Result<Option<PgpKeyInternal>, String> {
|
) -> 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) {
|
match conn.query_row(&sql, params![email.trim()], row_to_key) {
|
||||||
Ok(value) => Ok(Some(value)),
|
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}")),
|
Err(e) => Err(format!("Failed to query PGP key by email: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_secret_keys(conn: &Connection) -> Result<Vec<PgpKeyInternal>, String> {
|
pub fn list_secret_keys(conn: &Connection) -> Result<Vec<PgpKeyInternal>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare PGP secret key list: {e}"))?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map([], row_to_key)
|
.query_map([], row_to_key)
|
||||||
|
|||||||
+2361
-290
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,4 @@
|
|||||||
use rusqlite::{Connection, params};
|
use crate::db::pool::{Connection, params};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -11,7 +11,7 @@ pub struct ResponseTemplate {
|
|||||||
pub updated_at: String,
|
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 {
|
Ok(ResponseTemplate {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
name: row.get(1)?,
|
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> {
|
pub fn list(conn: &Connection) -> Result<Vec<ResponseTemplate>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare response templates: {e}"))?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map([], from_row)
|
.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> {
|
pub fn get(conn: &Connection, id: i64) -> Result<Option<ResponseTemplate>, String> {
|
||||||
match conn.query_row(
|
match conn.query_row(
|
||||||
crate::queries::sqlite::queries::db_response_templates::Q_002,
|
crate::dbq!(db_response_templates::Q_002),
|
||||||
params![id],
|
params![id],
|
||||||
from_row,
|
from_row,
|
||||||
) {
|
) {
|
||||||
Ok(value) => Ok(Some(value)),
|
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}")),
|
Err(e) => Err(format!("Failed to get response template: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,19 +66,19 @@ pub fn create(
|
|||||||
"text" | "" => "text",
|
"text" | "" => "text",
|
||||||
_ => return Err("Invalid response template format".to_string()),
|
_ => return Err("Invalid response template format".to_string()),
|
||||||
};
|
};
|
||||||
conn.execute(
|
let id = conn
|
||||||
crate::queries::sqlite::queries::db_response_templates::Q_003,
|
.insert_id(
|
||||||
params![name, body, format],
|
crate::dbq!(db_response_templates::Q_003),
|
||||||
)
|
params![name, body, format],
|
||||||
.map_err(|e| {
|
)
|
||||||
if e.to_string().contains("UNIQUE constraint failed") {
|
.map_err(|e| {
|
||||||
"A response template with this name already exists".to_string()
|
if e.to_string().contains("UNIQUE constraint failed") {
|
||||||
} else {
|
"A response template with this name already exists".to_string()
|
||||||
format!("Failed to create response template: {e}")
|
} else {
|
||||||
}
|
format!("Failed to create response template: {e}")
|
||||||
})?;
|
}
|
||||||
get(conn, conn.last_insert_rowid())?
|
})?;
|
||||||
.ok_or_else(|| "Failed to read response template".to_string())
|
get(conn, id)?.ok_or_else(|| "Failed to read response template".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(
|
pub fn update(
|
||||||
@@ -105,7 +105,7 @@ pub fn update(
|
|||||||
};
|
};
|
||||||
let changed = conn
|
let changed = conn
|
||||||
.execute(
|
.execute(
|
||||||
crate::queries::sqlite::queries::db_response_templates::Q_004,
|
crate::dbq!(db_response_templates::Q_004),
|
||||||
params![name, body, format, id],
|
params![name, body, format, id],
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -123,10 +123,7 @@ pub fn update(
|
|||||||
|
|
||||||
pub fn delete(conn: &Connection, id: i64) -> Result<bool, String> {
|
pub fn delete(conn: &Connection, id: i64) -> Result<bool, String> {
|
||||||
Ok(conn
|
Ok(conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_response_templates::Q_005), params![id])
|
||||||
crate::queries::sqlite::queries::db_response_templates::Q_005,
|
|
||||||
params![id],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to delete response template: {e}"))?
|
.map_err(|e| format!("Failed to delete response template: {e}"))?
|
||||||
> 0)
|
> 0)
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-16
@@ -1,4 +1,4 @@
|
|||||||
use rusqlite::{Connection, params};
|
use crate::db::pool::{Connection, params};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -10,7 +10,7 @@ pub struct SavedSearch {
|
|||||||
pub updated_at: String,
|
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 {
|
Ok(SavedSearch {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
name: row.get(1)?,
|
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> {
|
pub fn list(conn: &Connection) -> Result<Vec<SavedSearch>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare saved searches: {e}"))?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map([], row_to_saved)
|
.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() {
|
if name.is_empty() || query_string.is_empty() {
|
||||||
return Err("Saved search name and query are required".to_string());
|
return Err("Saved search name and query are required".to_string());
|
||||||
}
|
}
|
||||||
conn.execute(
|
let id = conn
|
||||||
crate::queries::sqlite::queries::db_saved_searches::Q_002,
|
.insert_id(
|
||||||
params![name, query_string],
|
crate::dbq!(db_saved_searches::Q_002),
|
||||||
)
|
params![name, query_string],
|
||||||
.map_err(|e| format!("Failed to save search: {e}"))?;
|
)
|
||||||
let id = conn.last_insert_rowid();
|
.map_err(|e| format!("Failed to save search: {e}"))?;
|
||||||
get(conn, id)?.ok_or_else(|| "Failed to read saved search".to_string())
|
get(conn, id)?.ok_or_else(|| "Failed to read saved search".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ pub fn update(
|
|||||||
}
|
}
|
||||||
let changed = conn
|
let changed = conn
|
||||||
.execute(
|
.execute(
|
||||||
crate::queries::sqlite::queries::db_saved_searches::Q_003,
|
crate::dbq!(db_saved_searches::Q_003),
|
||||||
params![name, query_string, id],
|
params![name, query_string, id],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to update saved search: {e}"))?;
|
.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> {
|
pub fn get(conn: &Connection, id: i64) -> Result<Option<SavedSearch>, String> {
|
||||||
match conn.query_row(
|
match conn.query_row(
|
||||||
crate::queries::sqlite::queries::db_saved_searches::Q_004,
|
crate::dbq!(db_saved_searches::Q_004),
|
||||||
params![id],
|
params![id],
|
||||||
row_to_saved,
|
row_to_saved,
|
||||||
) {
|
) {
|
||||||
Ok(value) => Ok(Some(value)),
|
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}")),
|
Err(e) => Err(format!("Failed to get saved search: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(conn: &Connection, id: i64) -> Result<bool, String> {
|
pub fn delete(conn: &Connection, id: i64) -> Result<bool, String> {
|
||||||
Ok(conn
|
Ok(conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_saved_searches::Q_005), params![id])
|
||||||
crate::queries::sqlite::queries::db_saved_searches::Q_005,
|
|
||||||
params![id],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to delete saved search: {e}"))?
|
.map_err(|e| format!("Failed to delete saved search: {e}"))?
|
||||||
> 0)
|
> 0)
|
||||||
}
|
}
|
||||||
+13
-23
@@ -1,4 +1,4 @@
|
|||||||
use rusqlite::{Connection, OptionalExtension, params};
|
use crate::db::pool::{Connection, OptionalExtension, params};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
const ROLES: &[&str] = &["sent", "drafts", "trash", "junk", "archive"];
|
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) {
|
if !valid_role(role) {
|
||||||
return Err(format!("Invalid special folder role: {role}"));
|
return Err(format!("Invalid special folder role: {role}"));
|
||||||
}
|
}
|
||||||
conn.query_row(
|
conn.query_row(crate::dbq!(db_special_folders::Q_001), [role], |row| {
|
||||||
crate::queries::sqlite::queries::db_special_folders::Q_001,
|
row.get(0)
|
||||||
[role],
|
})
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.optional()
|
.optional()
|
||||||
.map_err(|e| format!("Failed to read special folder {role}: {e}"))
|
.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()) {
|
match folder_name.map(str::trim).filter(|value| !value.is_empty()) {
|
||||||
Some(folder) => {
|
Some(folder) => {
|
||||||
let exists: bool = conn
|
let exists: bool = conn
|
||||||
.query_row(
|
.query_row(crate::dbq!(db_special_folders::Q_002), [folder], |row| {
|
||||||
crate::queries::sqlite::queries::db_special_folders::Q_002,
|
row.get(0)
|
||||||
[folder],
|
})
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to validate folder {folder}: {e}"))?;
|
.map_err(|e| format!("Failed to validate folder {folder}: {e}"))?;
|
||||||
if !exists {
|
if !exists {
|
||||||
return Err(format!("Folder does not exist: {folder}"));
|
return Err(format!("Folder does not exist: {folder}"));
|
||||||
}
|
}
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_special_folders::Q_003,
|
crate::dbq!(db_special_folders::Q_003),
|
||||||
params![role, folder],
|
params![role, folder],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to set special folder {role}: {e}"))?;
|
.map_err(|e| format!("Failed to set special folder {role}: {e}"))?;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_special_folders::Q_004), [role])
|
||||||
crate::queries::sqlite::queries::db_special_folders::Q_004,
|
.map_err(|e| format!("Failed to clear special folder {role}: {e}"))?;
|
||||||
[role],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to clear special folder {role}: {e}"))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -87,7 +80,7 @@ pub fn rename_folder_reference(
|
|||||||
new_name: &str,
|
new_name: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_special_folders::Q_005,
|
crate::dbq!(db_special_folders::Q_005),
|
||||||
params![new_name, old_name],
|
params![new_name, old_name],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to update special folder reference: {e}"))?;
|
.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> {
|
pub fn clear_folder_reference(conn: &Connection, folder_name: &str) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_special_folders::Q_006), [folder_name])
|
||||||
crate::queries::sqlite::queries::db_special_folders::Q_006,
|
.map_err(|e| format!("Failed to clear special folder reference: {e}"))?;
|
||||||
[folder_name],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to clear special folder reference: {e}"))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-43
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use rusqlite::{Connection, params};
|
use crate::db::pool::{Connection, DbValue, ToDbValue, params};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::db::messages::CachedMessage;
|
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> {
|
pub fn create_tag(conn: &Connection, id: &str, name: &str, color: &str) -> Result<(), String> {
|
||||||
let imap_keyword = imap_keyword_for_tag_id(id);
|
let imap_keyword = imap_keyword_for_tag_id(id);
|
||||||
conn.execute(
|
conn.execute(
|
||||||
crate::queries::sqlite::queries::db_tags::Q_001,
|
crate::dbq!(db_tags::Q_001),
|
||||||
params![id, name, color, imap_keyword],
|
params![id, name, color, imap_keyword],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to create tag: {e}"))?;
|
.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> {
|
pub fn list_tags(conn: &Connection) -> Result<Vec<Tag>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare list_tags: {e}"))?;
|
||||||
|
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
@@ -76,7 +76,7 @@ pub fn get_message_refs_by_tag(
|
|||||||
tag_id: &str,
|
tag_id: &str,
|
||||||
) -> Result<Vec<(u32, String)>, String> {
|
) -> Result<Vec<(u32, String)>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare tag message refs: {e}"))?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map(params![tag_id], |row| Ok((row.get(0)?, row.get(1)?)))
|
.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> {
|
pub fn update_tag(conn: &Connection, id: &str, name: &str, color: &str) -> Result<bool, String> {
|
||||||
let updated = conn
|
let updated = conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_tags::Q_004), params![name, color, id])
|
||||||
crate::queries::sqlite::queries::db_tags::Q_004,
|
|
||||||
params![name, color, id],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to update tag: {e}"))?;
|
.map_err(|e| format!("Failed to update tag: {e}"))?;
|
||||||
Ok(updated > 0)
|
Ok(updated > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_tag(conn: &Connection, id: &str) -> Result<bool, String> {
|
pub fn delete_tag(conn: &Connection, id: &str) -> Result<bool, String> {
|
||||||
let deleted = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to delete tag: {e}"))?;
|
||||||
Ok(deleted > 0)
|
Ok(deleted > 0)
|
||||||
}
|
}
|
||||||
@@ -167,11 +164,8 @@ pub fn add_tag_to_message(
|
|||||||
uid: u32,
|
uid: u32,
|
||||||
folder: &str,
|
folder: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_tags::Q_006), params![tag_id, uid, folder])
|
||||||
crate::queries::sqlite::queries::db_tags::Q_006,
|
.map_err(|e| format!("Failed to add tag to message: {e}"))?;
|
||||||
params![tag_id, uid, folder],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to add tag to message: {e}"))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,10 +176,7 @@ pub fn remove_tag_from_message(
|
|||||||
folder: &str,
|
folder: &str,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
let deleted = conn
|
let deleted = conn
|
||||||
.execute(
|
.execute(crate::dbq!(db_tags::Q_007), params![tag_id, uid, folder])
|
||||||
crate::queries::sqlite::queries::db_tags::Q_007,
|
|
||||||
params![tag_id, uid, folder],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to remove tag from message: {e}"))?;
|
.map_err(|e| format!("Failed to remove tag from message: {e}"))?;
|
||||||
Ok(deleted > 0)
|
Ok(deleted > 0)
|
||||||
}
|
}
|
||||||
@@ -196,7 +187,7 @@ pub fn get_message_tags(
|
|||||||
folder: &str,
|
folder: &str,
|
||||||
) -> Result<Vec<MessageTag>, String> {
|
) -> Result<Vec<MessageTag>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare get_message_tags: {e}"))?;
|
||||||
|
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
@@ -236,22 +227,22 @@ pub fn get_tags_for_messages(
|
|||||||
})
|
})
|
||||||
.collect();
|
.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
|
let mut stmt = conn
|
||||||
.prepare(&sql)
|
.prepare(&sql)
|
||||||
.map_err(|e| format!("Failed to prepare get_tags_for_messages: {e}"))?;
|
.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 {
|
for &(uid, folder) in messages {
|
||||||
param_values.push(Box::new(uid));
|
param_values.push(uid.to_db_value());
|
||||||
param_values.push(Box::new(folder.to_string()));
|
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
|
let rows = stmt
|
||||||
.query_map(params_ref.as_slice(), |row| {
|
.query_map(param_values, |row| {
|
||||||
let uid: u32 = row.get(0)?;
|
let uid: u32 = row.get(0)?;
|
||||||
let folder: String = row.get(1)?;
|
let folder: String = row.get(1)?;
|
||||||
let tag = MessageTag {
|
let tag = MessageTag {
|
||||||
@@ -279,7 +270,7 @@ pub fn get_messages_by_tag(
|
|||||||
) -> Result<Vec<CachedMessage>, String> {
|
) -> Result<Vec<CachedMessage>, String> {
|
||||||
let offset = page * per_page;
|
let offset = page * per_page;
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare get_messages_by_tag: {e}"))?;
|
||||||
|
|
||||||
let rows = stmt
|
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> {
|
pub fn count_messages_by_tag(conn: &Connection, tag_id: &str) -> Result<u32, String> {
|
||||||
conn.query_row(
|
conn.query_row(crate::dbq!(db_tags::Q_010), params![tag_id], |row| {
|
||||||
crate::queries::sqlite::queries::db_tags::Q_010,
|
row.get(0)
|
||||||
params![tag_id],
|
})
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to count messages by tag: {e}"))
|
.map_err(|e| format!("Failed to count messages by tag: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,16 +317,10 @@ mod tests {
|
|||||||
use crate::db::pool::open_test_db;
|
use crate::db::pool::open_test_db;
|
||||||
|
|
||||||
fn insert_test_message(conn: &Connection, folder: &str, uid: u32) {
|
fn insert_test_message(conn: &Connection, folder: &str, uid: u32) {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_tags::Q_011), params![folder])
|
||||||
crate::queries::sqlite::queries::db_tags::Q_011,
|
.unwrap();
|
||||||
params![folder],
|
conn.execute(crate::dbq!(db_tags::Q_012), params![uid, folder])
|
||||||
)
|
.unwrap();
|
||||||
.unwrap();
|
|
||||||
conn.execute(
|
|
||||||
crate::queries::sqlite::queries::db_tags::Q_012,
|
|
||||||
params![uid, folder],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use rusqlite::Connection;
|
use crate::db::pool::Connection;
|
||||||
|
|
||||||
pub fn list(conn: &Connection) -> Result<Vec<String>, String> {
|
pub fn list(conn: &Connection) -> Result<Vec<String>, String> {
|
||||||
let mut stmt = conn
|
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}"))?;
|
.map_err(|e| format!("Failed to prepare trusted sender list: {e}"))?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map([], |row| row.get::<_, String>(0))
|
.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> {
|
pub fn add(conn: &Connection, email: &str) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_trusted_senders::Q_002), [email])
|
||||||
crate::queries::sqlite::queries::db_trusted_senders::Q_002,
|
.map_err(|e| format!("Failed to trust sender: {e}"))?;
|
||||||
[email],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to trust sender: {e}"))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(conn: &Connection, email: &str) -> Result<(), String> {
|
pub fn remove(conn: &Connection, email: &str) -> Result<(), String> {
|
||||||
conn.execute(
|
conn.execute(crate::dbq!(db_trusted_senders::Q_003), [email])
|
||||||
crate::queries::sqlite::queries::db_trusted_senders::Q_003,
|
.map_err(|e| format!("Failed to remove trusted sender: {e}"))?;
|
||||||
[email],
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to remove trusted sender: {e}"))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
format!("invalid RustBox configuration: {message}"),
|
format!("invalid RustBox configuration: {message}"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
db::pool::configure(&loaded_config)
|
||||||
|
.await
|
||||||
|
.map_err(std::io::Error::other)?;
|
||||||
let config = Arc::new(loaded_config);
|
let config = Arc::new(loaded_config);
|
||||||
|
|
||||||
let persistence = session_store::from_config(&config)
|
let persistence = session_store::from_config(&config)
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ async fn https_get_address(
|
|||||||
format!("{host}:{port}")
|
format!("{host}:{port}")
|
||||||
};
|
};
|
||||||
let request = format!(
|
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()))
|
timeout(duration, stream.write_all(request.as_bytes()))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub mod db_calendar {
|
|||||||
working_hours_end, time_format, zoom_link, teams_link, meet_link, caldav_url, \
|
working_hours_end, time_format, zoom_link, teams_link, meet_link, caldav_url, \
|
||||||
caldav_username, caldav_enabled, updated_at";
|
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
|
pub const Q_002: &str = "INSERT INTO calendar_events
|
||||||
(id, title, description, location, start_time, end_time, all_day,
|
(id, title, description, location, start_time, end_time, all_day,
|
||||||
recurrence_rule, attendees, organizer, status, source, source_uid,
|
recurrence_rule, attendees, organizer, status, source, source_uid,
|
||||||
@@ -45,7 +45,7 @@ pub mod db_calendar {
|
|||||||
pub mod db_contact_groups {
|
pub mod db_contact_groups {
|
||||||
pub const Q_001: &str = "INSERT INTO contact_groups (id, name) VALUES (?, ?)";
|
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,
|
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
|
FROM contact_groups g
|
||||||
ORDER BY g.name ASC";
|
ORDER BY g.name ASC";
|
||||||
pub const Q_003: &str =
|
pub const Q_003: &str =
|
||||||
@@ -62,7 +62,7 @@ pub mod db_contact_groups {
|
|||||||
WHERE m.group_id = ?
|
WHERE m.group_id = ?
|
||||||
ORDER BY c.name ASC";
|
ORDER BY c.name ASC";
|
||||||
pub const Q_008: &str = "SELECT g.id, g.name, g.created_at, g.updated_at,
|
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
|
FROM contact_groups g
|
||||||
JOIN contact_group_members m ON m.group_id = g.id
|
JOIN contact_group_members m ON m.group_id = g.id
|
||||||
WHERE m.contact_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,
|
(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)
|
contact_count, source, created_at, updated_at, emails_json, phones_json, addresses_json)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON DUPLICATE KEY UPDATE
|
||||||
email=excluded.email, name=excluded.name, company=excluded.company, notes=excluded.notes,
|
email=VALUES(email), name=VALUES(name), company=VALUES(company), notes=VALUES(notes),
|
||||||
phone=excluded.phone, job_title=excluded.job_title, address=excluded.address, website=excluded.website, birthday=excluded.birthday,
|
phone=VALUES(phone), job_title=VALUES(job_title), address=VALUES(address), website=VALUES(website), birthday=VALUES(birthday),
|
||||||
is_favorite=excluded.is_favorite, last_contacted=excluded.last_contacted, contact_count=excluded.contact_count,
|
is_favorite=VALUES(is_favorite), last_contacted=VALUES(last_contacted), contact_count=VALUES(contact_count),
|
||||||
source=excluded.source, updated_at=excluded.updated_at, emails_json=excluded.emails_json,
|
source=VALUES(source), updated_at=VALUES(updated_at), emails_json=VALUES(emails_json),
|
||||||
phones_json=excluded.phones_json, addresses_json=excluded.addresses_json";
|
phones_json=VALUES(phones_json), addresses_json=VALUES(addresses_json)";
|
||||||
pub const Q_002: &str = "DELETE FROM contacts WHERE id = ?";
|
pub const Q_002: &str = "DELETE FROM contacts WHERE id = ?";
|
||||||
pub const Q_003: &str = r#"
|
pub const Q_003: &str = r#"
|
||||||
SELECT email, name
|
SELECT email, name
|
||||||
FROM known_addresses
|
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 email NOT IN (SELECT email FROM contacts)
|
||||||
AND NOT EXISTS (
|
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)
|
WHERE lower(alt.value) = lower(known_addresses.email)
|
||||||
)
|
)
|
||||||
ORDER BY email ASC
|
ORDER BY email ASC
|
||||||
LIMIT ?
|
LIMIT ?2
|
||||||
"#;
|
"#;
|
||||||
pub const Q_004: &str = "INSERT INTO known_addresses (email, name) VALUES (?, ?)
|
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 (?, ?)
|
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
|
pub const Q_006: &str = "UPDATE contacts
|
||||||
SET contact_count = contact_count + 1,
|
SET contact_count = contact_count + 1,
|
||||||
last_contacted = CURRENT_TIMESTAMP,
|
last_contacted = CURRENT_TIMESTAMP,
|
||||||
@@ -105,24 +106,24 @@ pub mod db_contacts {
|
|||||||
WHERE email = ?";
|
WHERE email = ?";
|
||||||
pub const Q_007: &str = "INSERT IGNORE INTO contacts (id, email, name, source)
|
pub const Q_007: &str = "INSERT IGNORE INTO contacts (id, email, name, source)
|
||||||
VALUES (?, ?, ?, 'auto')";
|
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_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 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_001: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts WHERE id = ?";
|
||||||
pub const FORMAT_002: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts \
|
pub const FORMAT_002: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts \
|
||||||
WHERE lower(email) = lower(?) \
|
WHERE lower(email) = lower(?1) \
|
||||||
OR EXISTS (SELECT 1 FROM json_each(contacts.emails_json) WHERE lower(value) = lower(?))";
|
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
|
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
|
ORDER BY name ASC
|
||||||
LIMIT ? OFFSET ?";
|
LIMIT ?2 OFFSET ?3";
|
||||||
pub const FORMAT_004: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
|
pub const FORMAT_004: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
|
||||||
ORDER BY name ASC
|
ORDER BY name ASC
|
||||||
LIMIT ? OFFSET ?";
|
LIMIT ? OFFSET ?";
|
||||||
pub const FORMAT_005: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
|
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
|
ORDER BY name ASC
|
||||||
LIMIT ?";
|
LIMIT ?2";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_display_preferences {
|
pub mod db_display_preferences {
|
||||||
@@ -134,29 +135,28 @@ pub mod db_display_preferences {
|
|||||||
pub mod db_drafts {
|
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)
|
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)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON DUPLICATE KEY UPDATE
|
||||||
to_addresses = excluded.to_addresses,
|
to_addresses = VALUES(to_addresses),
|
||||||
cc_addresses = excluded.cc_addresses,
|
cc_addresses = VALUES(cc_addresses),
|
||||||
bcc_addresses = excluded.bcc_addresses,
|
bcc_addresses = VALUES(bcc_addresses),
|
||||||
subject = excluded.subject,
|
subject = VALUES(subject),
|
||||||
text_body = excluded.text_body,
|
text_body = VALUES(text_body),
|
||||||
html_body = excluded.html_body,
|
html_body = VALUES(html_body),
|
||||||
in_reply_to = excluded.in_reply_to,
|
in_reply_to = VALUES(in_reply_to),
|
||||||
references_header = excluded.references_header,
|
references_header = VALUES(references_header),
|
||||||
request_mdn = excluded.request_mdn,
|
request_mdn = VALUES(request_mdn),
|
||||||
priority = excluded.priority,
|
priority = VALUES(priority),
|
||||||
compose_format = excluded.compose_format,
|
compose_format = VALUES(compose_format),
|
||||||
pgp_mode = excluded.pgp_mode,
|
pgp_mode = VALUES(pgp_mode),
|
||||||
pgp_inline = excluded.pgp_inline,
|
pgp_inline = VALUES(pgp_inline),
|
||||||
pgp_attach_public_key = excluded.pgp_attach_public_key,
|
pgp_attach_public_key = VALUES(pgp_attach_public_key),
|
||||||
pgp_attach_recipient_keys = excluded.pgp_attach_recipient_keys,
|
pgp_attach_recipient_keys = VALUES(pgp_attach_recipient_keys),
|
||||||
updated_at = CURRENT_TIMESTAMP";
|
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
|
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 = ?";
|
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
|
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";
|
FROM drafts ORDER BY updated_at DESC";
|
||||||
pub const Q_004: &str =
|
pub const Q_004: &str = "UPDATE drafts SET remote_folder = ?2, remote_uid = ?3, remote_uid_validity = ?4 WHERE id = ?1";
|
||||||
"UPDATE drafts SET remote_folder = ?, remote_uid = ?, remote_uid_validity = ? WHERE id = ?";
|
|
||||||
pub const Q_005: &str = "DELETE FROM drafts WHERE id = ?";
|
pub const Q_005: &str = "DELETE FROM drafts WHERE id = ?";
|
||||||
pub const Q_006: &str =
|
pub const Q_006: &str =
|
||||||
"INSERT INTO draft_attachments (id, draft_id, filename, content_type, size, file_path)
|
"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_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_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_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 = ?";
|
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 =
|
pub const Q_007: &str =
|
||||||
"UPDATE folders SET uid_validity = ?, total_count = ?, highest_modseq = ?
|
"UPDATE folders SET uid_validity = ?, total_count = ?, highest_modseq = ?
|
||||||
WHERE name = ?";
|
WHERE name = ?";
|
||||||
pub const Q_008: &str =
|
pub const Q_008: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages WHERE folder = ? AND LOCATE(CONCAT(CHAR(92), 'Seen'), flags) = 0";
|
||||||
"SELECT COUNT(*) FROM messages WHERE folder = ? AND flags NOT LIKE '%\\Seen%'";
|
|
||||||
pub const Q_009: &str = "UPDATE folders SET unread_count = ? WHERE name = ?";
|
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_010: &str = "UPDATE folders SET unread_count = GREATEST(0, CAST(unread_count AS SIGNED) + ?) WHERE name = ?";
|
||||||
pub const Q_011: &str = "SELECT EXISTS(
|
pub const Q_011: &str = "SELECT EXISTS(
|
||||||
SELECT 1 FROM folders
|
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(
|
pub const Q_012: &str = "SELECT EXISTS(
|
||||||
SELECT 1 FROM folders
|
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_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 = ?";
|
pub const Q_014: &str = "UPDATE folders SET messages_updated_at = NULL WHERE name = ?";
|
||||||
@@ -216,7 +215,7 @@ pub mod db_folders {
|
|||||||
WHERE name = ?";
|
WHERE name = ?";
|
||||||
pub const Q_016: &str = "DELETE FROM messages WHERE folder = ?";
|
pub const Q_016: &str = "DELETE FROM messages WHERE folder = ?";
|
||||||
pub const Q_017: &str = "DELETE FROM folders WHERE name = ?";
|
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)
|
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
|
SELECT ?, delimiter, parent, flags, is_subscribed, total_count, unread_count, uid_validity, highest_modseq, CURRENT_TIMESTAMP, messages_updated_at
|
||||||
FROM folders WHERE name = ?";
|
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_001: &str = "SELECT value FROM user_meta WHERE key = 'header_sync_generation'";
|
||||||
pub const Q_002: &str =
|
pub const Q_002: &str =
|
||||||
"INSERT INTO user_meta (key, value) VALUES ('header_sync_generation', '1')
|
"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 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 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_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_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 =
|
pub const Q_004: &str =
|
||||||
"UPDATE identities SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE is_default = 1";
|
"UPDATE identities SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE is_default = 1";
|
||||||
pub const FORMAT_001: &str =
|
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, \
|
pub const Q_004: &str = "SELECT uid, folder, subject, from_address, from_name, to_addresses, \
|
||||||
COALESCE(NULLIF(body_text, ''), snippet, ''), date_epoch, has_attachments \
|
COALESCE(NULLIF(body_text, ''), snippet, ''), date_epoch, has_attachments \
|
||||||
FROM messages ORDER BY folder, uid";
|
FROM messages ORDER BY folder, uid";
|
||||||
pub const Q_005: &str = "SELECT COUNT(*) FROM messages WHERE folder = ?";
|
pub const Q_005: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages WHERE folder = ?";
|
||||||
pub const Q_006: &str = "SELECT COUNT(*) FROM messages";
|
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_007: &str = "UPDATE messages SET flags = ? WHERE folder = ? AND uid = ?";
|
||||||
pub const Q_008: &str = "UPDATE messages
|
pub const Q_008: &str = "UPDATE messages
|
||||||
SET body_html = ?, body_text = ?, body_cached = 1,
|
SET body_html = ?, body_text = ?, body_cached = 1,
|
||||||
@@ -291,7 +290,7 @@ pub mod db_messages {
|
|||||||
ORDER BY date_epoch DESC
|
ORDER BY date_epoch DESC
|
||||||
LIMIT ?";
|
LIMIT ?";
|
||||||
pub const Q_013: &str = "SELECT uid, flags FROM messages WHERE folder = ?";
|
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}
|
pub const FORMAT_001: &str = "SELECT {MSG_SELECT_COLS}
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE folder = ?
|
WHERE folder = ?
|
||||||
@@ -307,9 +306,9 @@ pub mod db_messages {
|
|||||||
WHERE folder = ? AND uid = ?";
|
WHERE folder = ? AND uid = ?";
|
||||||
pub const FORMAT_004: &str = "SELECT {MSG_SELECT_COLS}
|
pub const FORMAT_004: &str = "SELECT {MSG_SELECT_COLS}
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE message_id = ?
|
WHERE message_id = ?1
|
||||||
OR in_reply_to = ?
|
OR in_reply_to = ?1
|
||||||
OR references_header LIKE ? ESCAPE '\\'
|
OR references_header LIKE ?2 ESCAPE '!'
|
||||||
ORDER BY date_epoch ASC";
|
ORDER BY date_epoch ASC";
|
||||||
pub const FORMAT_005: &str = "SELECT {MSG_SELECT_COLS}
|
pub const FORMAT_005: &str = "SELECT {MSG_SELECT_COLS}
|
||||||
FROM messages
|
FROM messages
|
||||||
@@ -326,9 +325,9 @@ pub mod db_notification_preferences {
|
|||||||
|
|
||||||
pub mod db_pgp {
|
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 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_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 =
|
pub const Q_004: &str =
|
||||||
"SELECT wkd_enabled,hkp_enabled,require_trusted_encryption FROM pgp_preferences WHERE id=1";
|
"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";
|
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 mod db_pool {
|
||||||
pub const Q_001: &str = "SELECT 0";
|
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_003: &str = "SELECT 1";
|
||||||
pub const Q_004: &str = "SELECT 1";
|
pub const Q_004: &str = "SELECT 1";
|
||||||
pub const Q_005: &str = "SHOW TABLES";
|
pub const Q_005: &str = "SHOW TABLES";
|
||||||
pub const Q_006: &str = "SELECT 1";
|
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 {
|
pub mod db_response_templates {
|
||||||
@@ -374,7 +374,7 @@ pub mod db_saved_searches {
|
|||||||
pub mod db_special_folders {
|
pub mod db_special_folders {
|
||||||
pub const Q_001: &str = "SELECT folder_name FROM special_folders WHERE role = ?";
|
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_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_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_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 = ?";
|
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_001: &str = "INSERT INTO tags (id, name, color, imap_keyword) VALUES (?, ?, ?, ?)";
|
||||||
pub const Q_002: &str =
|
pub const Q_002: &str =
|
||||||
"SELECT t.id, t.name, t.color, t.imap_keyword, t.created_at, t.updated_at,
|
"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
|
FROM tags t
|
||||||
ORDER BY t.name ASC";
|
ORDER BY t.name ASC";
|
||||||
pub const Q_003: &str = "SELECT message_uid, message_folder FROM message_tags WHERE tag_id = ?";
|
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 = ?
|
WHERE mt.tag_id = ?
|
||||||
ORDER BY m.date_epoch DESC
|
ORDER BY m.date_epoch DESC
|
||||||
LIMIT ? OFFSET ?";
|
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_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)
|
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, '')";
|
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 mod routes_contacts {
|
||||||
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP";
|
pub const Q_001: &str = "SELECT CAST(CURRENT_TIMESTAMP AS CHAR)";
|
||||||
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 CURRENT_TIMESTAMP";
|
pub const Q_003: &str = "SELECT CAST(CURRENT_TIMESTAMP AS CHAR)";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod routes_diagnostics {
|
pub mod routes_diagnostics {
|
||||||
pub const Q_001: &str = "SELECT COUNT(*),
|
pub const Q_001: &str = "SELECT CAST(COUNT(*) AS SIGNED),
|
||||||
COALESCE(SUM(CASE WHEN messages_updated_at IS NULL THEN 1 ELSE 0 END), 0),
|
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 replace(MAX(updated_at), ' ', 'T') || 'Z' END,
|
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 replace(MAX(messages_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";
|
FROM folders";
|
||||||
pub const Q_002: &str = "SELECT COUNT(*) FROM messages";
|
pub const Q_002: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages";
|
||||||
pub const Q_003: &str = "SELECT COUNT(*) FROM messages WHERE body_cached = 1";
|
pub const Q_003: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages WHERE body_cached = 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod routes_folder_mgmt {
|
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_002: &str = "UPDATE folders SET unread_count = 0 WHERE name = ?";
|
||||||
pub const Q_003: &str = "SELECT uid FROM messages WHERE folder = ?";
|
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 = ?";
|
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";
|
"SELECT * FROM sessions WHERE browser_id = ? ORDER BY created_at_unix";
|
||||||
pub const BY_USER: &str =
|
pub const BY_USER: &str =
|
||||||
"SELECT * FROM sessions WHERE user_hash = ? ORDER BY last_accessed_unix DESC";
|
"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_ACCOUNT: &str = "DELETE FROM sessions WHERE account_id = ?";
|
||||||
pub const DELETE_BROWSER: &str = "DELETE FROM sessions WHERE browser_id = ?";
|
pub const DELETE_BROWSER: &str = "DELETE FROM sessions WHERE browser_id = ?";
|
||||||
pub const DELETE_EXPIRED: &str =
|
pub const DELETE_EXPIRED: &str =
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ pub mod db_calendar {
|
|||||||
pub const SETTINGS_SELECT_COLS: &str = "default_view, week_starts_on, working_hours_start, \
|
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, \
|
working_hours_end, time_format, zoom_link, teams_link, meet_link, caldav_url, \
|
||||||
caldav_username, caldav_enabled, updated_at";
|
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
|
pub const Q_002: &str = "INSERT INTO calendar_events
|
||||||
(id, title, description, location, start_time, end_time, all_day,
|
(id, title, description, location, start_time, end_time, all_day,
|
||||||
recurrence_rule, attendees, organizer, status, source, source_uid,
|
recurrence_rule, attendees, organizer, status, source, source_uid,
|
||||||
@@ -18,8 +18,10 @@ pub mod db_calendar {
|
|||||||
reminder_minutes, created_at, updated_at)
|
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)";
|
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_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_004: &str =
|
||||||
pub const Q_005: &str = "INSERT OR IGNORE INTO calendar_settings (id) VALUES (1)";
|
"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
|
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";
|
FROM meeting_templates ORDER BY is_default DESC, name ASC";
|
||||||
pub const Q_007: &str =
|
pub const Q_007: &str =
|
||||||
@@ -48,10 +50,9 @@ pub mod db_contact_groups {
|
|||||||
FROM contact_groups g
|
FROM contact_groups g
|
||||||
ORDER BY g.name ASC";
|
ORDER BY g.name ASC";
|
||||||
pub const Q_003: &str =
|
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_004: &str = "DELETE FROM contact_groups WHERE id = $1";
|
||||||
pub const Q_005: &str =
|
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";
|
||||||
"INSERT OR IGNORE INTO contact_group_members (group_id, contact_id) VALUES ($1, $2)";
|
|
||||||
pub const Q_006: &str =
|
pub const Q_006: &str =
|
||||||
"DELETE FROM contact_group_members WHERE group_id = $1 AND contact_id = $2";
|
"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,
|
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#"
|
pub const Q_003: &str = r#"
|
||||||
SELECT email, name
|
SELECT email, name
|
||||||
FROM known_addresses
|
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 email NOT IN (SELECT email FROM contacts)
|
||||||
AND NOT EXISTS (
|
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)
|
WHERE lower(alt.value) = lower(known_addresses.email)
|
||||||
)
|
)
|
||||||
ORDER BY email ASC
|
ORDER BY email ASC
|
||||||
@@ -99,40 +101,41 @@ pub mod db_contacts {
|
|||||||
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != ''";
|
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != ''";
|
||||||
pub const Q_006: &str = "UPDATE contacts
|
pub const Q_006: &str = "UPDATE contacts
|
||||||
SET contact_count = contact_count + 1,
|
SET contact_count = contact_count + 1,
|
||||||
last_contacted = CURRENT_TIMESTAMP,
|
last_contacted = CURRENT_TIMESTAMP::text,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP::text
|
||||||
WHERE email = $1";
|
WHERE email = $1";
|
||||||
pub const Q_007: &str = "INSERT OR IGNORE INTO contacts (id, email, name, source)
|
pub const Q_007: &str = "INSERT INTO contacts (id, email, name, source)
|
||||||
VALUES ($1, $2, $3, 'auto')";
|
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) 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 ($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_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 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_001: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts WHERE id = $1";
|
||||||
pub const FORMAT_002: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts \
|
pub const FORMAT_002: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts \
|
||||||
WHERE lower(email) = lower($1) \
|
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
|
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
|
ORDER BY name ASC
|
||||||
LIMIT $2 OFFSET $3";
|
LIMIT $2 OFFSET $3";
|
||||||
pub const FORMAT_004: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
|
pub const FORMAT_004: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
|
||||||
ORDER BY name ASC
|
ORDER BY name ASC
|
||||||
LIMIT $1 OFFSET $2";
|
LIMIT $1 OFFSET $2";
|
||||||
pub const FORMAT_005: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
|
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
|
ORDER BY name ASC
|
||||||
LIMIT $2";
|
LIMIT $2";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_display_preferences {
|
pub mod db_display_preferences {
|
||||||
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, updated_at FROM display_preferences WHERE id = 1";
|
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, 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 const FORMAT_001: &str = "UPDATE display_preferences SET {} WHERE id = ${}";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_drafts {
|
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)
|
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
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
to_addresses = excluded.to_addresses,
|
to_addresses = excluded.to_addresses,
|
||||||
cc_addresses = excluded.cc_addresses,
|
cc_addresses = excluded.cc_addresses,
|
||||||
@@ -149,7 +152,7 @@ pub mod db_drafts {
|
|||||||
pgp_inline = excluded.pgp_inline,
|
pgp_inline = excluded.pgp_inline,
|
||||||
pgp_attach_public_key = excluded.pgp_attach_public_key,
|
pgp_attach_public_key = excluded.pgp_attach_public_key,
|
||||||
pgp_attach_recipient_keys = excluded.pgp_attach_recipient_keys,
|
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
|
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";
|
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
|
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_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_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_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 const Q_007: &str = "DELETE FROM folder_preferences WHERE folder_name = $1";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_folders {
|
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,
|
(name, delimiter, parent, flags, is_subscribed,
|
||||||
total_count, unread_count, uid_validity, highest_modseq, updated_at)
|
total_count, unread_count, uid_validity, highest_modseq, updated_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP)";
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP::text)
|
||||||
pub const Q_002: &str = "INSERT OR IGNORE INTO folders
|
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)
|
(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,
|
pub const Q_003: &str = "SELECT name, delimiter, parent, flags, is_subscribed,
|
||||||
total_count, unread_count, uid_validity, highest_modseq
|
total_count, unread_count, uid_validity, highest_modseq
|
||||||
FROM folders
|
FROM folders
|
||||||
ORDER BY name";
|
ORDER BY name";
|
||||||
pub const Q_004: &str = "DELETE FROM folders";
|
pub const Q_004: &str = "DELETE FROM folders";
|
||||||
pub const Q_005: &str = "UPDATE folders SET 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
|
pub const Q_006: &str = "UPDATE folders SET uid_validity = $1, total_count = $2, messages_updated_at = CURRENT_TIMESTAMP::text
|
||||||
WHERE name = $3";
|
WHERE name = $3";
|
||||||
pub const Q_007: &str =
|
pub const Q_007: &str =
|
||||||
"UPDATE folders SET uid_validity = $1, total_count = $2, highest_modseq = $3
|
"UPDATE folders SET uid_validity = $1, total_count = $2, highest_modseq = $3
|
||||||
WHERE name = $4";
|
WHERE name = $4";
|
||||||
pub const Q_008: &str =
|
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_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_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(
|
pub const Q_011: &str = "SELECT EXISTS(
|
||||||
SELECT 1 FROM folders
|
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(
|
pub const Q_012: &str = "SELECT EXISTS(
|
||||||
SELECT 1 FROM folders
|
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_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";
|
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";
|
WHERE name = $1";
|
||||||
pub const Q_016: &str = "DELETE FROM messages WHERE folder = $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_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)
|
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";
|
FROM folders WHERE name = $2";
|
||||||
pub const Q_020: &str = "UPDATE messages SET folder = $1 WHERE folder = $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";
|
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_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_002: &str = "DELETE FROM identities WHERE id = $1";
|
||||||
pub const Q_003: &str = "SELECT COUNT(*) FROM identities";
|
pub const Q_003: &str = "SELECT COUNT(*) FROM identities";
|
||||||
pub const Q_004: &str =
|
pub const Q_004: &str = "UPDATE identities SET is_default = 0, updated_at = CURRENT_TIMESTAMP::text WHERE is_default = 1";
|
||||||
"UPDATE identities SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE is_default = 1";
|
|
||||||
pub const FORMAT_001: &str =
|
pub const FORMAT_001: &str =
|
||||||
"SELECT {IDENTITY_SELECT_COLS} FROM identities ORDER BY is_default DESC, email ASC";
|
"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";
|
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 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_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_002: &str =
|
||||||
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";
|
"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 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 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,
|
(uid, folder, message_id, in_reply_to, references_header,
|
||||||
subject, from_address, from_name, to_addresses, cc_addresses,
|
subject, from_address, from_name, to_addresses, cc_addresses,
|
||||||
date, flags, size, has_attachments, snippet, date_epoch, reaction)
|
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_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_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, \
|
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
|
ORDER BY date_epoch DESC
|
||||||
LIMIT $1";
|
LIMIT $1";
|
||||||
pub const Q_013: &str = "SELECT uid, flags FROM messages WHERE folder = $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}
|
pub const FORMAT_001: &str = "SELECT {MSG_SELECT_COLS}
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE folder = $1
|
WHERE folder = $1
|
||||||
@@ -307,7 +318,7 @@ pub mod db_messages {
|
|||||||
FROM messages
|
FROM messages
|
||||||
WHERE message_id = $1
|
WHERE message_id = $1
|
||||||
OR in_reply_to = $1
|
OR in_reply_to = $1
|
||||||
OR references_header LIKE $2 ESCAPE '\\'
|
OR references_header LIKE $2 ESCAPE '!'
|
||||||
ORDER BY date_epoch ASC";
|
ORDER BY date_epoch ASC";
|
||||||
pub const FORMAT_005: &str = "SELECT {MSG_SELECT_COLS}
|
pub const FORMAT_005: &str = "SELECT {MSG_SELECT_COLS}
|
||||||
FROM messages
|
FROM messages
|
||||||
@@ -318,19 +329,20 @@ pub mod db_messages {
|
|||||||
|
|
||||||
pub mod db_notification_preferences {
|
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_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 =
|
pub const FORMAT_001: &str =
|
||||||
"UPDATE notification_preferences SET {set_clause} WHERE id = ${idx}";
|
"UPDATE notification_preferences SET {set_clause} WHERE id = ${idx}";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_pgp {
|
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 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_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 =
|
pub const Q_004: &str =
|
||||||
"SELECT wkd_enabled,hkp_enabled,require_trusted_encryption FROM pgp_preferences WHERE id=1";
|
"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 =
|
pub const FORMAT_001: &str =
|
||||||
"SELECT {KEY_COLS} FROM pgp_keys ORDER BY has_secret DESC, lower(email), fingerprint";
|
"SELECT {KEY_COLS} FROM pgp_keys ORDER BY has_secret DESC, lower(email), fingerprint";
|
||||||
pub const FORMAT_002: &str =
|
pub const FORMAT_002: &str =
|
||||||
@@ -342,11 +354,13 @@ pub mod db_pgp {
|
|||||||
|
|
||||||
pub mod db_pool {
|
pub mod db_pool {
|
||||||
pub const Q_001: &str = "SELECT 0";
|
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_003: &str = "SELECT 1";
|
||||||
pub const Q_004: &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_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_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 {
|
pub mod db_response_templates {
|
||||||
@@ -355,7 +369,7 @@ pub mod db_response_templates {
|
|||||||
pub const Q_002: &str =
|
pub const Q_002: &str =
|
||||||
"SELECT id,name,body,format,created_at,updated_at FROM response_templates WHERE id=$1";
|
"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_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";
|
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 =
|
pub const Q_001: &str =
|
||||||
"SELECT id,name,query_string,created_at,updated_at FROM saved_searches ORDER BY name ";
|
"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_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 =
|
pub const Q_004: &str =
|
||||||
"SELECT id,name,query_string,created_at,updated_at FROM saved_searches WHERE id=$1";
|
"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";
|
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 mod db_special_folders {
|
||||||
pub const Q_001: &str = "SELECT folder_name FROM special_folders WHERE role = $1";
|
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_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_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";
|
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 =
|
pub const Q_003: &str =
|
||||||
"SELECT message_uid, message_folder FROM message_tags WHERE tag_id = $1";
|
"SELECT message_uid, message_folder FROM message_tags WHERE tag_id = $1";
|
||||||
pub const Q_004: &str =
|
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_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 =
|
pub const Q_007: &str =
|
||||||
"DELETE FROM message_tags WHERE tag_id = $1 AND message_uid = $2 AND message_folder = $3";
|
"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
|
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
|
ORDER BY m.date_epoch DESC
|
||||||
LIMIT $2 OFFSET $3";
|
LIMIT $2 OFFSET $3";
|
||||||
pub const Q_010: &str = "SELECT COUNT(*) FROM message_tags WHERE tag_id = $1";
|
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)
|
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, '')";
|
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
|
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 mod db_trusted_senders {
|
||||||
pub const Q_001: &str = "SELECT email FROM trusted_senders ORDER BY lower(email)";
|
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 const Q_003: &str = "DELETE FROM trusted_senders WHERE email = $1 ";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod routes_contacts {
|
pub mod routes_contacts {
|
||||||
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP";
|
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP::text";
|
||||||
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP";
|
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP::text";
|
||||||
pub const Q_003: &str = "SELECT CURRENT_TIMESTAMP";
|
pub const Q_003: &str = "SELECT CURRENT_TIMESTAMP::text";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod routes_diagnostics {
|
pub mod routes_diagnostics {
|
||||||
@@ -442,10 +457,10 @@ pub mod routes_diagnostics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod routes_folder_mgmt {
|
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_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_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 {
|
pub mod routes_messages_mod {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ pub mod db_contacts {
|
|||||||
pub const Q_003: &str = r#"
|
pub const Q_003: &str = r#"
|
||||||
SELECT email, name
|
SELECT email, name
|
||||||
FROM known_addresses
|
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 email NOT IN (SELECT email FROM contacts)
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM contacts c, json_each(c.emails_json) alt
|
SELECT 1 FROM contacts c, json_each(c.emails_json) alt
|
||||||
@@ -112,14 +112,14 @@ pub mod db_contacts {
|
|||||||
WHERE lower(email) = lower(?1) \
|
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 json_each(contacts.emails_json) WHERE lower(value) = lower(?1))";
|
||||||
pub const FORMAT_003: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
|
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
|
ORDER BY name ASC
|
||||||
LIMIT ?2 OFFSET ?3";
|
LIMIT ?2 OFFSET ?3";
|
||||||
pub const FORMAT_004: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
|
pub const FORMAT_004: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
|
||||||
ORDER BY name ASC
|
ORDER BY name ASC
|
||||||
LIMIT ?1 OFFSET ?2";
|
LIMIT ?1 OFFSET ?2";
|
||||||
pub const FORMAT_005: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
|
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
|
ORDER BY name ASC
|
||||||
LIMIT ?2";
|
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_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(
|
pub const Q_011: &str = "SELECT EXISTS(
|
||||||
SELECT 1 FROM folders
|
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(
|
pub const Q_012: &str = "SELECT EXISTS(
|
||||||
SELECT 1 FROM folders
|
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_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";
|
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
|
FROM messages
|
||||||
WHERE message_id = ?1
|
WHERE message_id = ?1
|
||||||
OR in_reply_to = ?1
|
OR in_reply_to = ?1
|
||||||
OR references_header LIKE ?2 ESCAPE '\\'
|
OR references_header LIKE ?2 ESCAPE '!'
|
||||||
ORDER BY date_epoch ASC";
|
ORDER BY date_epoch ASC";
|
||||||
pub const FORMAT_005: &str = "SELECT {MSG_SELECT_COLS}
|
pub const FORMAT_005: &str = "SELECT {MSG_SELECT_COLS}
|
||||||
FROM messages
|
FROM messages
|
||||||
@@ -347,6 +347,8 @@ pub mod db_pool {
|
|||||||
pub const Q_004: &str = "PRAGMA foreign_keys=ON;";
|
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_005: &str = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
|
||||||
pub const Q_006: &str = "PRAGMA foreign_keys";
|
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 {
|
pub fn set_user_version(version: u32) -> String {
|
||||||
format!("PRAGMA user_version = {version};")
|
format!("PRAGMA user_version = {version};")
|
||||||
@@ -532,11 +534,11 @@ macro_rules! rustbox_query_db_contacts_002 {
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! rustbox_query_db_contacts_003 {
|
macro_rules! rustbox_query_db_contacts_003 {
|
||||||
() => { format!("SELECT {CONTACT_SELECT_COLS} FROM contacts
|
() => { 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
|
ORDER BY name ASC
|
||||||
LIMIT ?2 OFFSET ?3") };
|
LIMIT ?2 OFFSET ?3") };
|
||||||
($($args:tt)+) => { format!("SELECT {CONTACT_SELECT_COLS} FROM contacts
|
($($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
|
ORDER BY name ASC
|
||||||
LIMIT ?2 OFFSET ?3", $($args)+) };
|
LIMIT ?2 OFFSET ?3", $($args)+) };
|
||||||
}
|
}
|
||||||
@@ -554,11 +556,11 @@ macro_rules! rustbox_query_db_contacts_004 {
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! rustbox_query_db_contacts_005 {
|
macro_rules! rustbox_query_db_contacts_005 {
|
||||||
() => { format!("SELECT {CONTACT_SELECT_COLS} FROM contacts
|
() => { 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
|
ORDER BY name ASC
|
||||||
LIMIT ?2") };
|
LIMIT ?2") };
|
||||||
($($args:tt)+) => { format!("SELECT {CONTACT_SELECT_COLS} FROM contacts
|
($($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
|
ORDER BY name ASC
|
||||||
LIMIT ?2", $($args)+) };
|
LIMIT ?2", $($args)+) };
|
||||||
}
|
}
|
||||||
@@ -643,13 +645,13 @@ macro_rules! rustbox_query_db_messages_004 {
|
|||||||
FROM messages
|
FROM messages
|
||||||
WHERE message_id = ?1
|
WHERE message_id = ?1
|
||||||
OR in_reply_to = ?1
|
OR in_reply_to = ?1
|
||||||
OR references_header LIKE ?2 ESCAPE '\\'
|
OR references_header LIKE ?2 ESCAPE '!'
|
||||||
ORDER BY date_epoch ASC") };
|
ORDER BY date_epoch ASC") };
|
||||||
($($args:tt)+) => { format!("SELECT {MSG_SELECT_COLS}
|
($($args:tt)+) => { format!("SELECT {MSG_SELECT_COLS}
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE message_id = ?1
|
WHERE message_id = ?1
|
||||||
OR in_reply_to = ?1
|
OR in_reply_to = ?1
|
||||||
OR references_header LIKE ?2 ESCAPE '\\'
|
OR references_header LIKE ?2 ESCAPE '!'
|
||||||
ORDER BY date_epoch ASC", $($args)+) };
|
ORDER BY date_epoch ASC", $($args)+) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-11
@@ -318,20 +318,20 @@ async fn sync_condstore(
|
|||||||
let mut folder_changed = !changed.is_empty();
|
let mut folder_changed = !changed.is_empty();
|
||||||
for (uid, mut flags) in changed {
|
for (uid, mut flags) in changed {
|
||||||
flags.sort();
|
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 {
|
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 {
|
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) {
|
if !imap_uids.contains(&uid) {
|
||||||
db::messages::delete_message(conn, &name, uid)?;
|
db::messages::delete_message(&transaction, &name, uid)?;
|
||||||
folder_changed = true;
|
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 new_messages = status.uid_next > max_uid.saturating_add(1);
|
||||||
let modseq = if new_modseq > 0 {
|
let modseq = if new_modseq > 0 {
|
||||||
new_modseq
|
new_modseq
|
||||||
@@ -339,14 +339,14 @@ async fn sync_condstore(
|
|||||||
status.highest_modseq
|
status.highest_modseq
|
||||||
};
|
};
|
||||||
db::folders::update_folder_sync_status(
|
db::folders::update_folder_sync_status(
|
||||||
conn,
|
&transaction,
|
||||||
&name,
|
&name,
|
||||||
status.uid_validity,
|
status.uid_validity,
|
||||||
status.exists,
|
status.exists,
|
||||||
modseq,
|
modseq,
|
||||||
)?;
|
)?;
|
||||||
if new_messages {
|
if new_messages {
|
||||||
db::folders::invalidate_folder_freshness(conn, &name)?;
|
db::folders::invalidate_folder_freshness(&transaction, &name)?;
|
||||||
folder_changed = true;
|
folder_changed = true;
|
||||||
}
|
}
|
||||||
transaction.commit().map_err(|e| e.to_string())?;
|
transaction.commit().map_err(|e| e.to_string())?;
|
||||||
@@ -448,7 +448,7 @@ async fn sync_full(
|
|||||||
for (uid, cached_flags_csv) in &cached {
|
for (uid, cached_flags_csv) in &cached {
|
||||||
match imap_map.get(uid) {
|
match imap_map.get(uid) {
|
||||||
None => {
|
None => {
|
||||||
db::messages::delete_message(conn, folder_name, *uid)?;
|
db::messages::delete_message(&transaction, folder_name, *uid)?;
|
||||||
folder_changed = true;
|
folder_changed = true;
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
folder = %folder_name,
|
folder = %folder_name,
|
||||||
@@ -467,7 +467,7 @@ async fn sync_full(
|
|||||||
|
|
||||||
if cached_normalized != imap_flags_csv {
|
if cached_normalized != imap_flags_csv {
|
||||||
db::messages::update_message_flags(
|
db::messages::update_message_flags(
|
||||||
conn,
|
&transaction,
|
||||||
folder_name,
|
folder_name,
|
||||||
*uid,
|
*uid,
|
||||||
&imap_flags_csv,
|
&imap_flags_csv,
|
||||||
@@ -475,14 +475,19 @@ async fn sync_full(
|
|||||||
folder_changed = true;
|
folder_changed = true;
|
||||||
}
|
}
|
||||||
if sync_keywords {
|
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(
|
db::folders::update_folder_sync_status(
|
||||||
conn,
|
&transaction,
|
||||||
folder_name,
|
folder_name,
|
||||||
status.uid_validity,
|
status.uid_validity,
|
||||||
status.exists,
|
status.exists,
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ pub async fn export_ics(
|
|||||||
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
||||||
|
|
||||||
let mut body = String::from(
|
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 {
|
for event in &events {
|
||||||
append_ics_event(&mut body, event);
|
append_ics_event(&mut body, event);
|
||||||
|
|||||||
+3
-15
@@ -184,11 +184,7 @@ pub async fn create_contact_handler(
|
|||||||
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
||||||
|
|
||||||
let now: String = conn
|
let now: String = conn
|
||||||
.query_row(
|
.query_row(crate::dbq!(routes_contacts::Q_001), [], |row| row.get(0))
|
||||||
crate::queries::sqlite::queries::routes_contacts::Q_001,
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
||||||
let id = body.id.unwrap_or_else(|| Uuid::new_v4().to_string());
|
let id = body.id.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||||
let email = body.email.trim().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.last_contacted = source.last_contacted.clone();
|
||||||
}
|
}
|
||||||
target.updated_at = tx
|
target.updated_at = tx
|
||||||
.query_row(
|
.query_row(crate::dbq!(routes_contacts::Q_002), [], |row| row.get(0))
|
||||||
crate::queries::sqlite::queries::routes_contacts::Q_002,
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
||||||
|
|
||||||
for group in db::contact_groups::list_contact_groups(&tx, &source.id)
|
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}")))?;
|
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
||||||
|
|
||||||
let now: String = conn
|
let now: String = conn
|
||||||
.query_row(
|
.query_row(crate::dbq!(routes_contacts::Q_003), [], |row| row.get(0))
|
||||||
crate::queries::sqlite::queries::routes_contacts::Q_003,
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
||||||
|
|
||||||
let parsed = parse_vcards(&vcf_content);
|
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
Reference in new issue
Block a user