SharedInbox — Improvement Plan (30 tasks) #19

Closed
opened 2026-05-13 19:54:25 +00:00 by guettlibot · 2 comments
guettlibot commented 2026-05-13 19:54:25 +00:00 (Migrated from codeberg.org)

30 tasks across 7 perspectives. Priority markers: 🔴 high · 🟡 medium · 🟢 nice-to-have.


Group 1: Performance

P1 🔴 Replace LIKE-based search with FTS5 virtual table

The current observeEmails and search queries use LIKE '%query%' which becomes a full-table scan at scale.
Create an email_fts FTS5 virtual table (subject, preview, fromJson) populated via trigger or sync-time insert.
Wire SearchScreen to query the FTS table instead.
Files: lib/data/db/database.dart, lib/data/repositories/email_repository_impl.dart.

P2 🔴 Lazy-load email bodies on scroll (pagination)

observeThreads and observeEmails return the full list with no limit. As the mailbox grows this streams thousands of rows into memory.
Add a page-size parameter (e.g. 50) with "load more" support in EmailListScreen.
The EmailBodies table is already separate — never fetch bodies in the list query.
Files: lib/data/repositories/email_repository_impl.dart, lib/ui/screens/email_list_screen.dart.

P3 🟡 Defer HTML parsing off the UI thread using an Isolate

flutter_html parsing blocks the raster thread for large HTML bodies, causing jank when opening email detail.
Move the HTML→Widget tree conversion (or at minimum the html_utils.dart HTML-to-plain step) into a compute() call.
Files: lib/ui/screens/email_detail_screen.dart, lib/core/utils/html_utils.dart.

P4 🟡 Index DB columns used in WHERE/ORDER clauses

emails.receivedAt, emails.accountId, emails.mailboxPath, emails.threadId, and emails.snoozedUntil are queried without indexes.
Add explicit @Index annotations (or raw CREATE INDEX) in the Drift schema.
Files: lib/data/db/database.dart.

P5 🟢 Cache the formatted date strings in EmailListScreen

DateFormat('MMM d').format(...) is called for every email on every rebuild. Compute and cache these in the model layer or inside the list item widget's build method using a static cache map.
Files: lib/ui/screens/email_list_screen.dart, lib/core/utils/format_utils.dart.


Group 2: Reliability & Resilience

R1 — Done: https://codeberg.org/guettli/sharedinbox/pulls/20

R2 — Done: https://codeberg.org/guettli/sharedinbox/pulls/22

R3 🔴 Wrap HTML renderer in an ErrorWidget boundary

A malformed HTML body can throw inside flutter_html and crash the entire EmailDetailScreen.
Wrap the Html(...) widget in a Builder + try/catch or use ErrorWidget.builder locally so only the body section shows the error, not the whole screen.
Files: lib/ui/screens/email_detail_screen.dart.

R4 — Done: https://codeberg.org/guettli/sharedinbox/pulls/23

R5 🟡 Handle TLS certificate changes gracefully

tls_error.dart detects TLS errors but they bubble up as generic errors in the sync loop.
Detect TlsError specifically in _AccountSync and show a user-facing dialog offering to re-add the account or trust the new certificate.
Files: lib/data/imap/tls_error.dart, lib/core/sync/account_sync_manager.dart.

R6 — Done: https://codeberg.org/guettli/sharedinbox/pulls/24


Group 3: Security

S1 🔴 Optional SQLCipher encryption for the Drift database

Emails cached locally are plaintext. Users on shared or rooted devices are exposed.
Add an opt-in "Encrypt local storage" setting using drift's encrypted backend (sqflite_cipher / sqlcipher_flutter_libs).
Store the database key in flutter_secure_storage (already present).
Files: lib/data/db/database.dart, pubspec.yaml, a new settings toggle.

S2 — Done: https://codeberg.org/guettli/sharedinbox/pulls/25

S3 🟡 Enforce certificate pinning for known providers (opt-in)

Auto-discovered accounts for major providers (Gmail, Fastmail, Proton) could be pinned to their known CA hierarchy.
Implement as an opt-in per-account setting; only applies when the account is auto-discovered via AccountDiscoveryService.
Files: lib/core/services/account_discovery_service.dart, lib/data/imap/imap_client_factory.dart.

S4 🟢 Audit and restrict external link handling in HTML emails

flutter_html passes <a href> clicks to url_launcher without a prompt.
Before launching, show a confirmation dialog with the destination URL so phishing links are visible.
Files: lib/ui/screens/email_detail_screen.dart.


Group 4: User Experience

U1 — Done: https://codeberg.org/guettli/sharedinbox/pulls/26

U2 🔴 Draft synchronisation with server Drafts folder

DraftRepositoryImpl is local-only. Drafts are lost if the app is uninstalled or the user switches devices.
On sendDraft, upload to the IMAP Drafts folder via APPEND. On startup, fetch any server drafts not present locally.
Files: lib/data/repositories/draft_repository_impl.dart, lib/core/repositories/draft_repository.dart.

U3 🟡 Add "Recent searches" history to SearchScreen

The search bar clears on navigation. Store the last 10 search terms in a local DB table and show them as chips below the search field when the field is focused but empty.
Files: lib/ui/screens/search_screen.dart, lib/data/db/database.dart.

U4 🟡 UnifiedPush / platform notification integration

IMAP IDLE runs in the foreground sync loop. When the app is backgrounded, new emails arrive silently.
Implement a WorkManager/foreground-service approach on Android (or BGAppRefreshTask on iOS) that wakes the sync loop and posts a local notification on new mail.
Files: lib/main.dart, pubspec.yaml, android/ manifest.

U5 🟡 Accessible swipe actions on email list items

Delete and Move are hidden behind long-press or detail-screen menus. Add leading/trailing swipe actions on the EmailListScreen tile (archive / delete) matching Material 3 patterns.
Files: lib/ui/screens/email_list_screen.dart.

U6 🟡 Show connection status indicator in app bar

Users have no way to know if background sync is running, stalled, or paused.
Add a small icon in the EmailListScreen app bar (animated for syncing, static for idle, red for error) driven by a SyncStatusProvider backed by AccountSyncManager.
Files: lib/ui/screens/email_list_screen.dart, lib/di.dart, lib/core/sync/account_sync_manager.dart.

U7 🟢 Onboarding walkthrough for first-time users

The app opens directly to an empty account list with only a + button. First-time users have no guidance.
Add a one-time welcome card or bottom-sheet with the three-step flow: Add account → wait for sync → open inbox.
Files: lib/ui/screens/account_list_screen.dart.

U8 🟢 "Mark all as read" action in mailbox

Power users managing high-volume mailboxes need bulk read marking. Add a "Mark all as read" option in the mailbox overflow menu.
Files: lib/ui/screens/email_list_screen.dart, lib/core/repositories/email_repository.dart, lib/data/repositories/email_repository_impl.dart.


Group 5: Testing

T1 🔴 Raise coverage on EmailRepositoryImpl to ≥90 %

scripts/check_coverage.dart excludes several impl files. email_repository_impl.dart is the most critical path (sync, send, move, delete) and has the highest risk of regressions.
Add unit tests for edge cases: concurrent moves, IMAP UID mismatch, SMTP auth failure.
Files: test/unit/email_repository_impl_test.dart.

T2 🔴 Widget tests for ThreadDetailScreen and SearchScreen

These screens exist but have no entries in test/widget/. A regression in thread rendering or search result display would go undetected.
Add test/widget/thread_detail_screen_test.dart and test/widget/search_screen_test.dart following the pattern in test/widget/helpers.dart.
Files: test/widget/ (new files).

T3 🟡 Contract tests for all Repository interfaces

The interfaces in core/repositories/ have no shared contract test suite. Concrete impls can silently diverge.
Add a shared EmailRepositoryContract abstract test class; run it against both EmailRepositoryImpl and any future mock/fake. Mirror this for MailboxRepository and AccountRepository.
Files: test/unit/ (new contract test files).

T4 🟡 DB migration test for every schema version

test/unit/migration_test.dart exists. Verify it covers every schema version bump (currently v1→v22) by asserting the step count matches the DB schemaVersion.
If gaps exist, add migration steps for missing versions.
Files: test/unit/migration_test.dart, lib/data/db/database.dart.

T5 🟢 Snapshot / golden tests for key email list states

The email list has multiple states: loading, empty, normal, selection mode, search active, error banner.
Add golden tests using matchesGoldenFile for each state so visual regressions surface in CI.
Files: test/widget/email_list_screen_test.dart.


Group 6: Architecture & Code Quality

A1 🔴 Eliminate direct DB access from UI screens

email_detail_screen.dart calls ref.read(emailRepositoryProvider) and awaits futures in initState — tightly coupling the screen to the repository API.
Extract a EmailDetailNotifier (StateNotifier or AsyncNotifier) that owns the load logic and exposes a single AsyncValue<(Email, EmailBody)> to the screen.
Files: lib/ui/screens/email_detail_screen.dart, lib/di.dart.

A2 🟡 Extract reusable EmailTile widget

The email list item rendering is inlined in email_list_screen.dart and duplicated in thread_detail_screen.dart.
Extract to lib/ui/widgets/email_tile.dart with a clear interface; both screens import it.
Files: lib/ui/screens/email_list_screen.dart, lib/ui/screens/thread_detail_screen.dart, lib/ui/widgets/email_tile.dart (new).

A3 🟡 Make AccountSyncManager testable without real IMAP connections

AccountSyncManager accepts ImapConnectFn as a dependency but _JmapAccountSync constructs its HTTP client internally.
Pass an injectable http.Client to _JmapAccountSync (already done in EmailRepositoryImpl; mirror the pattern here).
Files: lib/core/sync/account_sync_manager.dart, test/unit/account_sync_manager_test.dart.

A4 🟡 Replace raw JSON strings in DB with structured encoding

fromJson, toAddresses, ccJson, references are stored as raw JSON strings parsed on every model conversion.
Create typed value classes with fromJson/toJson in core/models/email.dart and add a TypeConverter in the Drift schema so the DB layer owns the serialisation.
Files: lib/data/db/database.dart, lib/core/models/email.dart, lib/data/repositories/email_repository_impl.dart.

A5 🟢 Enforce layer boundaries via lint custom rules or barrel imports

The ui/ layer directly imports data/ concrete classes in several screens (e.g. drift types leak through).
Add a custom analysis_options.yaml rule or a CI lint step that flags any ui/ import of data/ (only core/ interfaces are allowed from UI).
Files: analysis_options.yaml, CI config.


Group 7: Developer Experience

D1 🔴 CI matrix for macOS and Windows builds

The CI currently tests Linux and Android. The macOS and Windows targets are "scaffolded" and may have accumulated silent breakage.
Add flutter build macos --debug and flutter build windows --debug jobs to the CI workflow with the same failure threshold as Linux.
Files: .github/workflows/ci.yml (or Codeberg equivalent).

D2 🟡 Add task check-coverage command that fails on regression

scripts/check_coverage.dart runs coverage but there is no Taskfile target that fails the build if coverage drops below the 85 % gate.
Wire it to a task check step so a PR that drops coverage is blocked before merge.
Files: Taskfile.yml, scripts/check_coverage.dart.

D3 🟢 Document the sync protocol in a SYNC.md architecture doc

DB-SYNC.md exists but focuses on the DB schema. The IMAP IDLE loop, exponential backoff, pending-change queue, and undo cancel logic are spread across four files with no single reference.
Write SYNC.md that describes the full lifecycle of an email action from UI tap to server confirmation.
Files: SYNC.md (new).

30 tasks across 7 perspectives. Priority markers: 🔴 high · 🟡 medium · 🟢 nice-to-have. --- ## Group 1: Performance ### P1 🔴 Replace LIKE-based search with FTS5 virtual table The current `observeEmails` and search queries use `LIKE '%query%'` which becomes a full-table scan at scale. Create an `email_fts` FTS5 virtual table (subject, preview, fromJson) populated via trigger or sync-time insert. Wire `SearchScreen` to query the FTS table instead. Files: `lib/data/db/database.dart`, `lib/data/repositories/email_repository_impl.dart`. ### P2 🔴 Lazy-load email bodies on scroll (pagination) `observeThreads` and `observeEmails` return the full list with no limit. As the mailbox grows this streams thousands of rows into memory. Add a page-size parameter (e.g. 50) with "load more" support in `EmailListScreen`. The `EmailBodies` table is already separate — never fetch bodies in the list query. Files: `lib/data/repositories/email_repository_impl.dart`, `lib/ui/screens/email_list_screen.dart`. ### P3 🟡 Defer HTML parsing off the UI thread using an Isolate `flutter_html` parsing blocks the raster thread for large HTML bodies, causing jank when opening email detail. Move the HTML→Widget tree conversion (or at minimum the `html_utils.dart` HTML-to-plain step) into a `compute()` call. Files: `lib/ui/screens/email_detail_screen.dart`, `lib/core/utils/html_utils.dart`. ### P4 🟡 Index DB columns used in WHERE/ORDER clauses `emails.receivedAt`, `emails.accountId`, `emails.mailboxPath`, `emails.threadId`, and `emails.snoozedUntil` are queried without indexes. Add explicit `@Index` annotations (or raw `CREATE INDEX`) in the Drift schema. Files: `lib/data/db/database.dart`. ### P5 🟢 Cache the formatted date strings in EmailListScreen `DateFormat('MMM d').format(...)` is called for every email on every rebuild. Compute and cache these in the model layer or inside the list item widget's `build` method using a static cache map. Files: `lib/ui/screens/email_list_screen.dart`, `lib/core/utils/format_utils.dart`. --- ## Group 2: Reliability & Resilience ### R1 — Done: https://codeberg.org/guettli/sharedinbox/pulls/20 ### R2 — Done: https://codeberg.org/guettli/sharedinbox/pulls/22 ### R3 🔴 Wrap HTML renderer in an ErrorWidget boundary A malformed HTML body can throw inside `flutter_html` and crash the entire `EmailDetailScreen`. Wrap the `Html(...)` widget in a `Builder` + `try/catch` or use `ErrorWidget.builder` locally so only the body section shows the error, not the whole screen. Files: `lib/ui/screens/email_detail_screen.dart`. ### R4 — Done: https://codeberg.org/guettli/sharedinbox/pulls/23 ### R5 🟡 Handle TLS certificate changes gracefully `tls_error.dart` detects TLS errors but they bubble up as generic errors in the sync loop. Detect `TlsError` specifically in `_AccountSync` and show a user-facing dialog offering to re-add the account or trust the new certificate. Files: `lib/data/imap/tls_error.dart`, `lib/core/sync/account_sync_manager.dart`. ### R6 — Done: https://codeberg.org/guettli/sharedinbox/pulls/24 --- ## Group 3: Security ### S1 🔴 Optional SQLCipher encryption for the Drift database Emails cached locally are plaintext. Users on shared or rooted devices are exposed. Add an opt-in "Encrypt local storage" setting using `drift`'s `encrypted` backend (`sqflite_cipher` / `sqlcipher_flutter_libs`). Store the database key in `flutter_secure_storage` (already present). Files: `lib/data/db/database.dart`, `pubspec.yaml`, a new settings toggle. ### S2 — Done: https://codeberg.org/guettli/sharedinbox/pulls/25 ### S3 🟡 Enforce certificate pinning for known providers (opt-in) Auto-discovered accounts for major providers (Gmail, Fastmail, Proton) could be pinned to their known CA hierarchy. Implement as an opt-in per-account setting; only applies when the account is auto-discovered via `AccountDiscoveryService`. Files: `lib/core/services/account_discovery_service.dart`, `lib/data/imap/imap_client_factory.dart`. ### S4 🟢 Audit and restrict external link handling in HTML emails `flutter_html` passes `<a href>` clicks to `url_launcher` without a prompt. Before launching, show a confirmation dialog with the destination URL so phishing links are visible. Files: `lib/ui/screens/email_detail_screen.dart`. --- ## Group 4: User Experience ### U1 — Done: https://codeberg.org/guettli/sharedinbox/pulls/26 ### U2 🔴 Draft synchronisation with server Drafts folder `DraftRepositoryImpl` is local-only. Drafts are lost if the app is uninstalled or the user switches devices. On `sendDraft`, upload to the IMAP `Drafts` folder via APPEND. On startup, fetch any server drafts not present locally. Files: `lib/data/repositories/draft_repository_impl.dart`, `lib/core/repositories/draft_repository.dart`. ### U3 🟡 Add "Recent searches" history to SearchScreen The search bar clears on navigation. Store the last 10 search terms in a local DB table and show them as chips below the search field when the field is focused but empty. Files: `lib/ui/screens/search_screen.dart`, `lib/data/db/database.dart`. ### U4 🟡 UnifiedPush / platform notification integration IMAP IDLE runs in the foreground sync loop. When the app is backgrounded, new emails arrive silently. Implement a `WorkManager`/foreground-service approach on Android (or `BGAppRefreshTask` on iOS) that wakes the sync loop and posts a local notification on new mail. Files: `lib/main.dart`, `pubspec.yaml`, `android/` manifest. ### U5 🟡 Accessible swipe actions on email list items Delete and Move are hidden behind long-press or detail-screen menus. Add leading/trailing swipe actions on the `EmailListScreen` tile (archive / delete) matching Material 3 patterns. Files: `lib/ui/screens/email_list_screen.dart`. ### U6 🟡 Show connection status indicator in app bar Users have no way to know if background sync is running, stalled, or paused. Add a small icon in the `EmailListScreen` app bar (animated for syncing, static for idle, red for error) driven by a `SyncStatusProvider` backed by `AccountSyncManager`. Files: `lib/ui/screens/email_list_screen.dart`, `lib/di.dart`, `lib/core/sync/account_sync_manager.dart`. ### U7 🟢 Onboarding walkthrough for first-time users The app opens directly to an empty account list with only a `+` button. First-time users have no guidance. Add a one-time welcome card or bottom-sheet with the three-step flow: Add account → wait for sync → open inbox. Files: `lib/ui/screens/account_list_screen.dart`. ### U8 🟢 "Mark all as read" action in mailbox Power users managing high-volume mailboxes need bulk read marking. Add a "Mark all as read" option in the mailbox overflow menu. Files: `lib/ui/screens/email_list_screen.dart`, `lib/core/repositories/email_repository.dart`, `lib/data/repositories/email_repository_impl.dart`. --- ## Group 5: Testing ### T1 🔴 Raise coverage on EmailRepositoryImpl to ≥90 % `scripts/check_coverage.dart` excludes several impl files. `email_repository_impl.dart` is the most critical path (sync, send, move, delete) and has the highest risk of regressions. Add unit tests for edge cases: concurrent moves, IMAP UID mismatch, SMTP auth failure. Files: `test/unit/email_repository_impl_test.dart`. ### T2 🔴 Widget tests for ThreadDetailScreen and SearchScreen These screens exist but have no entries in `test/widget/`. A regression in thread rendering or search result display would go undetected. Add `test/widget/thread_detail_screen_test.dart` and `test/widget/search_screen_test.dart` following the pattern in `test/widget/helpers.dart`. Files: `test/widget/` (new files). ### T3 🟡 Contract tests for all Repository interfaces The interfaces in `core/repositories/` have no shared contract test suite. Concrete impls can silently diverge. Add a shared `EmailRepositoryContract` abstract test class; run it against both `EmailRepositoryImpl` and any future mock/fake. Mirror this for `MailboxRepository` and `AccountRepository`. Files: `test/unit/` (new contract test files). ### T4 🟡 DB migration test for every schema version `test/unit/migration_test.dart` exists. Verify it covers every schema version bump (currently v1→v22) by asserting the step count matches the DB `schemaVersion`. If gaps exist, add migration steps for missing versions. Files: `test/unit/migration_test.dart`, `lib/data/db/database.dart`. ### T5 🟢 Snapshot / golden tests for key email list states The email list has multiple states: loading, empty, normal, selection mode, search active, error banner. Add golden tests using `matchesGoldenFile` for each state so visual regressions surface in CI. Files: `test/widget/email_list_screen_test.dart`. --- ## Group 6: Architecture & Code Quality ### A1 🔴 Eliminate direct DB access from UI screens `email_detail_screen.dart` calls `ref.read(emailRepositoryProvider)` and awaits futures in `initState` — tightly coupling the screen to the repository API. Extract a `EmailDetailNotifier` (StateNotifier or AsyncNotifier) that owns the load logic and exposes a single `AsyncValue<(Email, EmailBody)>` to the screen. Files: `lib/ui/screens/email_detail_screen.dart`, `lib/di.dart`. ### A2 🟡 Extract reusable EmailTile widget The email list item rendering is inlined in `email_list_screen.dart` and duplicated in `thread_detail_screen.dart`. Extract to `lib/ui/widgets/email_tile.dart` with a clear interface; both screens import it. Files: `lib/ui/screens/email_list_screen.dart`, `lib/ui/screens/thread_detail_screen.dart`, `lib/ui/widgets/email_tile.dart` (new). ### A3 🟡 Make AccountSyncManager testable without real IMAP connections `AccountSyncManager` accepts `ImapConnectFn` as a dependency but `_JmapAccountSync` constructs its HTTP client internally. Pass an injectable `http.Client` to `_JmapAccountSync` (already done in `EmailRepositoryImpl`; mirror the pattern here). Files: `lib/core/sync/account_sync_manager.dart`, `test/unit/account_sync_manager_test.dart`. ### A4 🟡 Replace raw JSON strings in DB with structured encoding `fromJson`, `toAddresses`, `ccJson`, `references` are stored as raw JSON strings parsed on every model conversion. Create typed value classes with `fromJson`/`toJson` in `core/models/email.dart` and add a `TypeConverter` in the Drift schema so the DB layer owns the serialisation. Files: `lib/data/db/database.dart`, `lib/core/models/email.dart`, `lib/data/repositories/email_repository_impl.dart`. ### A5 🟢 Enforce layer boundaries via lint custom rules or barrel imports The `ui/` layer directly imports `data/` concrete classes in several screens (e.g. `drift` types leak through). Add a custom `analysis_options.yaml` rule or a CI lint step that flags any `ui/` import of `data/` (only `core/` interfaces are allowed from UI). Files: `analysis_options.yaml`, CI config. --- ## Group 7: Developer Experience ### D1 🔴 CI matrix for macOS and Windows builds The CI currently tests Linux and Android. The macOS and Windows targets are "scaffolded" and may have accumulated silent breakage. Add `flutter build macos --debug` and `flutter build windows --debug` jobs to the CI workflow with the same failure threshold as Linux. Files: `.github/workflows/ci.yml` (or Codeberg equivalent). ### D2 🟡 Add `task check-coverage` command that fails on regression `scripts/check_coverage.dart` runs coverage but there is no Taskfile target that fails the build if coverage drops below the 85 % gate. Wire it to a `task check` step so a PR that drops coverage is blocked before merge. Files: `Taskfile.yml`, `scripts/check_coverage.dart`. ### D3 🟢 Document the sync protocol in a SYNC.md architecture doc `DB-SYNC.md` exists but focuses on the DB schema. The IMAP IDLE loop, exponential backoff, pending-change queue, and undo cancel logic are spread across four files with no single reference. Write `SYNC.md` that describes the full lifecycle of an email action from UI tap to server confirmation. Files: `SYNC.md` (new).
guettlibot commented 2026-05-13 22:24:31 +00:00 (Migrated from codeberg.org)

U2 (Draft sync with IMAP Drafts folder): PR #27 opened — https://codeberg.org/guettli/sharedinbox/pulls/27

U2 (Draft sync with IMAP Drafts folder): PR #27 opened — https://codeberg.org/guettli/sharedinbox/pulls/27
guettlibot commented 2026-05-13 22:51:25 +00:00 (Migrated from codeberg.org)

U4 (Background sync + local notifications): PR #28 opened — https://codeberg.org/guettli/sharedinbox/pulls/28

U4 (Background sync + local notifications): PR #28 opened — https://codeberg.org/guettli/sharedinbox/pulls/28
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: guettli/sharedinbox#19