688 lines
36 KiB
Markdown
688 lines
36 KiB
Markdown
# Done
|
||
|
||
This file contains tasks which got implemented.
|
||
|
||
Tasks get moved from next.md to done.md
|
||
|
||
## Tasks (2026-05-29)
|
||
|
||
- **Merge PR #307 — user preferences and configurable navigation (Issue #315)**: Confirmed that
|
||
all features from PR #307 (issue #299) were already merged into main via separate PRs:
|
||
- Configurable menu bar position (bottom/top) for mailbox view — merged via #298/#303
|
||
- Configurable back button position for single mail view — merged via #299/#307 features in #300
|
||
- Configurable "after mail action" (next message / return to mailbox) — merged via #300/#308
|
||
- Archive button with `resolveMailboxByRole` helper — merged via #287/#291, #286/#290
|
||
- User preferences DB schema (v34–v36: `user_preferences` table) — in main
|
||
- PR #307 and issue #299 closed.
|
||
- Issue #315 closed.
|
||
|
||
## Tasks (2026-05-26)
|
||
|
||
- **Renovate Bot (Issue #257)**: Renovate Bot runs daily via Forgejo Actions to keep
|
||
dependencies up to date. All required components are in main:
|
||
- `renovate.json` — Renovate configuration covering pub, Dockerfile, and Forgejo Actions
|
||
- `ci/main.go` — `Renovate()` Dagger function using Forgejo platform and Codeberg endpoint
|
||
- `.forgejo/workflows/renovate.yml` — daily cron (06:00 UTC) workflow
|
||
- `Taskfile.yml` — `renovate` task
|
||
- Issue #257 closed.
|
||
|
||
## Tasks (2026-05-11)
|
||
|
||
- **Stabilize Email List UI during Selection (Issue #14)**: Prevented layout shifts when entering
|
||
selection mode in the email list.
|
||
- Consolidated `AppBar` logic to maintain a constant height by preserving the `SearchBar` space.
|
||
- Refactored `ListTile` to keep `trailing` widgets (date, flag) visible during selection.
|
||
- Wrapped `leading` widgets in a `SizedBox` to ensure consistent horizontal alignment.
|
||
- Retained `Dismissible` wrappers during selection (disabling swipe via `direction`) to avoid widget tree churn.
|
||
- Verified with widget and E2E integration tests.
|
||
- Deployed to Android.
|
||
|
||
## Tasks (2026-05-10)
|
||
|
||
- **Improved Undo Log and Android Deployment (Issue #7)**: Enhanced the Undo Log to support
|
||
undoing any action from history, not just the latest. This provides more flexibility
|
||
when managing multiple destructive actions.
|
||
- Refactored `UndoService.undo()` to accept an optional `actionId`, allowing targeted rollbacks.
|
||
- Updated `UndoLogScreen` to remove the "latest only" restriction and provide immediate feedback.
|
||
- Successfully built and deployed the release APK to the distribution server via `task deploy-android`.
|
||
- Verified the new undo logic with updated unit tests and ensured full system integrity via the CI check suite.
|
||
- **Global Undo Log and History**: Implemented a persistent history of undoable
|
||
actions, allowing users to view and undo recent destructive operations.
|
||
- Added `UndoLogScreen` to display a chronologically reversed list of actions.
|
||
- Refactored `UndoService` to maintain a history of the last 10 actions.
|
||
- Added timestamp and description metadata to `UndoAction`.
|
||
- Added a "History" icon to the `AccountListScreen` app bar.
|
||
- **Improved .gitignore**: Added more patterns to keep the repository clean
|
||
(FVM, Android studio files, Flutter/Dart tool directories).
|
||
|
||
## Tasks (2026-05-09)
|
||
|
||
- **Fix Crash Page (Issue 3)**: Added a "Report this issue on Codeberg" button to the
|
||
global `CrashScreen`, facilitating easier bug reporting for users.
|
||
- **Fix Show Mail Headers (Issue 1)**: Extended the database schema and repository
|
||
to fetch and store raw email headers. Added a new "Headers" tab/view in the
|
||
email detail screen to display them in a zebra-colored table.
|
||
- **Fix Exception on Undo of Delete (Issue 2)**: Added `toJson()` and `fromJson()`
|
||
to the `EmailAddress` model to support correct serialization during undo
|
||
operations, resolving a crash when restoring deleted emails.
|
||
- **Dev Environment Hardening**: Added an automated check and fix for Nix
|
||
experimental features (`nix-command`, `flakes`) to the `Taskfile.yml`.
|
||
|
||
- **Optimistic UI**: Both IMAP and JMAP `moveEmail` operations are now optimistic,
|
||
updating the local database immediately instead of waiting for sync. This
|
||
provides instant feedback and ensures rows are available for Undo actions.
|
||
- **Global Undo Support**: Introduced `UndoShell` and `ShellRoute` to provide a
|
||
consistent "Undo" experience across all screens, automatically surfacing the
|
||
Undo SnackBar whenever a destructive action is performed.
|
||
- **Improved Thread Support**: Fixed a bug where deleting emails from the
|
||
`ThreadDetailScreen` lacked Undo logic.
|
||
|
||
## Undo Feature Fix (IMAP)
|
||
|
||
Fixed a bug where undoing an email deletion or move would fail for IMAP accounts
|
||
because the local row was hard-deleted before the Undo action was performed.
|
||
|
||
- **Data Preservation**: The app now fetches and preserves the full email data in
|
||
the `UndoAction` before performing a move or delete.
|
||
- **Restoration Support**: Added `restoreEmails` to the repository to allow
|
||
re-inserting hard-deleted rows into the local database during an Undo.
|
||
- **Robust Cancellation**: Improved `UndoService` to attempt cancellation of both
|
||
`move` and `delete` pending changes, ensuring consistency even if a delete
|
||
was implemented as a move-to-trash.
|
||
- **IMAP Optimization**: Made `moveEmail` a no-op locally if the destination
|
||
mailbox matches the current one, preventing accidental re-deletion during Undo.
|
||
|
||
## Network Resilience: Exponential Backoff and Smart Retries
|
||
|
||
Improved the sync engine's reliability on intermittent connections.
|
||
|
||
- **Exponential Backoff**: Replaced fixed retry intervals with a strategy that
|
||
scales from 5s up to 15m depending on consecutive failures.
|
||
- **Permanent Error Handling**: Sync loops now detect authentication failures
|
||
and stop automatically to prevent account lockout or useless battery drain.
|
||
- **Manual Override**: "Pull to refresh" now triggers an immediate full account
|
||
sync, bypassing any active backoff timers.
|
||
|
||
## Sync Reliability and Reliability Runner
|
||
|
||
Implemented a robust verification system to ensure the local database accurately
|
||
reflects the server state across multiple accounts and protocols.
|
||
|
||
- **Reliability Check**: Added `verifySyncReliability` to `EmailRepository` to
|
||
compare local UIDs/IDs and flags against the server's "ground truth".
|
||
- **Reliability Runner**: A background service (`lib/core/sync/reliability_runner.dart`)
|
||
that periodically identifies discrepancies.
|
||
- **Database Support**: Added `SyncHealth` table (Schema v19) to store verification
|
||
results.
|
||
- **UI Integration**: Added "Sync health" indicators to the account list tiles and
|
||
a manual "Verify sync health" menu action.
|
||
- **Comprehensive Testing**: Verified with a new integration test suite
|
||
(`test/integration/sync_reliability_test.dart`) covering both IMAP and JMAP
|
||
paths.
|
||
|
||
## Coverage Gate Cleanup and Verification Test
|
||
|
||
- **Reduced Exclusions**: Removed well-tested widgets (`try_connection_button.dart`, `add_account_screen.dart`, `edit_account_screen.dart`) from the unit-test `_excluded` list in `scripts/check_coverage.dart`.
|
||
- **Standalone Ghost Path Test**: Added a dedicated `test/unit/coverage_exclusion_test.dart` that parses the check_coverage script to ensure no "ghost paths" ever make it into the codebase, guaranteeing the exclusion list stays clean and valid during the standard test phase.
|
||
- **Coverage Status**: verified combined unit and integration coverage meets the 80%+ threshold (currently 84%).
|
||
|
||
## Undo for Delete and Move actions
|
||
|
||
Implemented a robust Undo mechanism for destructive actions like deleting
|
||
emails or moving them to different folders.
|
||
|
||
- **UndoService Infrastructure**: Added a new service (`lib/core/services/undo_service.dart`)
|
||
that maintains a history of the last 10 actions. It uses a `StateNotifier`
|
||
to expose the most recent undoable action to the UI.
|
||
- **UI Integration**: Added a global Snackbar listener in `EmailListScreen`.
|
||
Whenever a move or delete occurs (including bulk actions and swipes), a
|
||
Snackbar appears with an "Undo" button. Redundant snackbar triggers were
|
||
removed for a cleaner experience.
|
||
- **Optimized Repository Interaction**: Added `cancelPendingChange` to the
|
||
`EmailRepository` interface and implementation. This allows the Undo
|
||
operation to attempt to remove unsynced changes from the local queue,
|
||
preventing unnecessary server round-trips and potential conflicts.
|
||
- **Improved Model Coverage**: Added comprehensive unit tests for `Mailbox`
|
||
and `Email` models, achieving 100% coverage for these critical data
|
||
structures.
|
||
- **Sorting Logic Fix**: Identified and fixed a bug in `compareMailboxes`
|
||
where different unknown roles would cause the sort to return equality
|
||
incorrectly. The logic now correctly falls through to path-based sorting
|
||
for all same-priority roles.
|
||
- **Status**: Verified with unit, widget, and integration tests.
|
||
Total unit coverage: **83%**.
|
||
|
||
## Multi-account search improvement
|
||
|
||
Extended the search functionality to allow searching across all accounts
|
||
simultaneously, including folders, addresses, and messages.
|
||
|
||
- **Global Search UI**: Updated `SearchScreen` (`lib/ui/screens/search_screen.dart`)
|
||
to support searching without a specific `accountId`.
|
||
- **Account Context**: Added account display names and icons to search results
|
||
when performing a global search.
|
||
- **Repository Support**: Modified `EmailRepository` and `MailboxRepository`
|
||
to handle optional `accountId` parameters, enabling cross-account queries.
|
||
- **Global Entry Point**: Added a search icon to the `AccountListScreen`
|
||
app bar for quick access to global search.
|
||
- **Model Enhancements**: Added `compareMailboxes` to the `Mailbox` model
|
||
and `copyWith` to the `Account` model for better code reuse and testability.
|
||
|
||
## Thread View UI and Repository Support
|
||
|
||
Implemented a dedicated screen to view all emails within a thread, providing
|
||
a cohesive conversation view.
|
||
|
||
- **ThreadDetailScreen**: A new screen (`lib/ui/screens/thread_detail_screen.dart`)
|
||
that displays a list of emails in a thread. Each email is rendered as an
|
||
expandable card, with the latest message expanded by default.
|
||
- **HTML Support**: Integrated HTML rendering with remote image blocking
|
||
(reusing logic from `EmailDetailScreen`) into the thread view.
|
||
- **Message Actions**: Added reply and delete actions for individual messages
|
||
within the thread.
|
||
- **Repository Support**: Added `observeEmailsInThread` to `EmailRepository`
|
||
to fetch and watch all messages belonging to a specific thread ID.
|
||
- **Navigation**: Updated `EmailListScreen` to navigate to the new thread view
|
||
when a thread with multiple messages is tapped.
|
||
- **Mock Support**: Updated `FakeEmailRepository` in unit, widget, and
|
||
integration tests to support the new `observeEmailsInThread` method.
|
||
|
||
## Database-Backed Threading and Performance Optimizations
|
||
|
||
Refactored the threading logic from in-memory grouping to a persistent
|
||
database-backed approach for improved performance and scalability.
|
||
|
||
- **Threads Table**: Added a new `Threads` table to the SQLite database
|
||
(Schema v17/v18) to store aggregated thread metadata (subject, unread
|
||
status, participants, etc.).
|
||
- **Automatic Sync**: Implemented `_updateThread` logic in `EmailRepositoryImpl`
|
||
to keep the `Threads` table synchronized during IMAP/JMAP syncs and
|
||
user actions (flag changes, moves, deletions).
|
||
- **Migration**: Added migration logic to automatically populate the `Threads`
|
||
table from existing email data upon schema upgrade.
|
||
- **Indexes**: Added performance indexes on `emails.receivedAt`,
|
||
`emails.threadId`, and `pending_changes.accountId` to speed up common
|
||
query patterns for large mailboxes.
|
||
- **Repository Refactor**: Updated `observeThreads` to query the `Threads`
|
||
table directly, significantly reducing CPU and memory usage when
|
||
displaying the inbox.
|
||
|
||
## Global Crash Screen and Error Handling
|
||
|
||
Implemented a robust error handling system to capture and display unhandled
|
||
exceptions to users, facilitating easier bug reporting.
|
||
|
||
- **CrashScreen**: A new full-screen widget (`lib/ui/screens/crash_screen.dart`)
|
||
that displays the exception message, stack trace, and a "Copy to Clipboard"
|
||
button for easy sharing of error details.
|
||
- **Global Handlers**: Wrapped `main()` in `runZonedGuarded` to catch unhandled
|
||
async errors.
|
||
- **Framework Integration**: Installed `FlutterError.onError` and
|
||
`ErrorWidget.builder` to catch framework-level and widget build errors,
|
||
ensuring that all types of crashes result in a graceful error display.
|
||
|
||
## Optimized Android Deployment and Fixed E2E Flakiness
|
||
|
||
Improved the speed and reliability of the Android deployment pipeline.
|
||
|
||
- **Taskfile Optimization**: Updated `Taskfile.yml` to use `sources` and
|
||
`generates` for long-running tasks. Implemented marker files (`.done` files)
|
||
to skip `integration-android` and `deploy-android` when inputs haven't changed.
|
||
- **E2E Reliability**: Fixed a race condition in `app_e2e_test.dart` by adding
|
||
`pumpAndSettle()` and a 2-second safety delay before the "Save" button tap,
|
||
resolving the intermittent "missed tap" failure on slow emulators.
|
||
- **Deployment Confirmation**: The `deploy-android` task now verifies the build
|
||
with a full Android integration test before uploading the APK.
|
||
|
||
## Coverage Gate Maintenance
|
||
|
||
- **Ghost Path Check**: Updated `scripts/check_coverage.dart` to verify that all
|
||
excluded files still exist on disk, preventing "ghost paths" from cluttering
|
||
the configuration.
|
||
- **Increased Coverage**: Included `account_sync_manager.dart` and
|
||
`email_repository_impl.dart` in the coverage gate.
|
||
- **Current Status**: Total unit coverage increased to **82%**.
|
||
|
||
## IMAP attachments: accurate sizes and reliable downloads
|
||
|
||
Attachments in IMAP accounts previously showed as "0 B" in the UI because
|
||
the size was retrieved from the `Content-Disposition` header's `size`
|
||
parameter, which is frequently missing from real-world emails. Since the
|
||
full message is already fetched into memory when viewing an email,
|
||
`EmailRepositoryImpl.getEmailBody` now falls back to the length of the
|
||
actual (decoded) part content when the header is missing.
|
||
|
||
IMAP attachment downloads also frequently failed (throwing a `StateError`)
|
||
because `downloadAttachment` would fetch a single part from the server
|
||
and then try to call `msg.getPart(fetchId)` on the result. When fetching
|
||
only a single part, the IMAP library returns an `ImapMessage` where the
|
||
requested part *is* the root, so `getPart` (which looks for children)
|
||
would return `null`. The repository now falls back to the message itself
|
||
if the specific part ID cannot be found in the result of a partial fetch.
|
||
|
||
## Immediate server-side sync for local deletions and flag changes
|
||
|
||
Deletions and flag changes (seen/flagged) made in SharedInbox previously
|
||
did not appear in other clients (like Thunderbird) until the next sync
|
||
cycle or app restart, because the local mutations were enqueued but the
|
||
background sync loop was not notified to flush them immediately.
|
||
|
||
Added `onChangesQueued` stream to `EmailRepository` interface and
|
||
implementation. `EmailRepositoryImpl._enqueueChange` now emits the
|
||
`accountId` on this stream whenever a new local mutation is queued.
|
||
`AccountSyncManager` listens to this stream; when it sees an account ID,
|
||
it "kicks" the corresponding active sync loop, waking it from its IDLE
|
||
or wait phase to immediately run a sync cycle and flush the pending
|
||
changes to the server.
|
||
|
||
## Plain-text connections only via localhost; SSL toggle hidden for non-localhost hosts
|
||
|
||
## ManageSieve uses STARTTLS; clearer TLS-mismatch errors; broader connection check
|
||
|
||
The "Email filters" screen failed for IMAP accounts with
|
||
`HandshakeException: WRONG_VERSION_NUMBER(tls_record.cc:127)` because the
|
||
ManageSieve client was opening an implicit-TLS socket to port 4190, while
|
||
the server (Stalwart and other RFC 5804 implementations) listens plaintext
|
||
on 4190 and expects a `STARTTLS` upgrade. The plaintext capability greeting
|
||
landed in the TLS parser, which (correctly) rejected it.
|
||
|
||
`ManageSieveClient.connect` (`lib/data/imap/managesieve_client.dart`) now
|
||
follows RFC 5804 §1.7: it opens a plaintext socket, reads the capability
|
||
greeting, and — when `useTls` is true — sends `STARTTLS`, waits for `OK`,
|
||
detaches the plaintext listener, hands the raw socket to
|
||
`SecureSocket.secure()`, and re-reads capabilities on the secured stream.
|
||
The previous "implicit TLS, no STARTTLS" mode is gone; if the server does
|
||
not advertise `STARTTLS`, the client throws a clear error pointing the
|
||
user at the SSL toggle.
|
||
|
||
`WRONG_VERSION_NUMBER` is also produced for SMTP (and IMAP) when the SSL
|
||
toggle and the chosen port disagree — e.g. SSL=on with port 587 (which is
|
||
STARTTLS-only). New helper `lib/data/imap/tls_error.dart` translates that
|
||
specific BoringSSL error into a `TlsModeMismatchException` with the host,
|
||
port, and a hint about which port matches which TLS mode (465/587, 993/143,
|
||
4190). `connectImap`, `connectSmtp`, and the ManageSieve TLS upgrade now
|
||
all funnel through `rethrowAsTlsHint` so the same readable message reaches
|
||
the UI regardless of which protocol failed.
|
||
|
||
`ConnectionTestService` (`lib/core/services/connection_test_service.dart`)
|
||
previously only verified IMAP/JMAP, so SMTP and ManageSieve misconfig
|
||
silently passed the "Try connection" button on the edit-account screen
|
||
and only surfaced when the user later tried to send mail or open Email
|
||
filters. After IMAP succeeds, the service now also verifies SMTP (always
|
||
for IMAP accounts — sending mail requires it) and ManageSieve (only when
|
||
`manageSieveHost` is explicitly set, since the section is collapsed and
|
||
opt-in by default). Failures are prefixed with `SMTP:` or `ManageSieve:`
|
||
so the user can tell which leg of the connection is broken. New tests in
|
||
`test/unit/connection_test_service_test.dart` cover SMTP-failure
|
||
surfacing, the opt-in skip path, and ManageSieve-failure surfacing.
|
||
|
||
## Sieve filter editing for IMAP accounts (ManageSieve)
|
||
|
||
The "Email filters" entry was previously hidden for IMAP accounts because
|
||
`SieveRepository` only spoke JMAP. Added a minimal ManageSieve client
|
||
(RFC 5804) so IMAP accounts can now list / fetch / upload / activate /
|
||
delete server-side Sieve scripts.
|
||
|
||
New: `lib/data/imap/managesieve_client.dart` — implements CONNECT
|
||
(implicit TLS or plaintext), AUTHENTICATE PLAIN (SASL), LISTSCRIPTS,
|
||
GETSCRIPT, PUTSCRIPT, SETACTIVE, DELETESCRIPT, LOGOUT. Handles
|
||
RFC 5804 quoted-strings and `{N+}` non-synchronizing literals (used for
|
||
both reading script bodies and uploading them in PUTSCRIPT). The base64
|
||
SASL PLAIN payload is redacted in the verbose protocol log.
|
||
|
||
`SieveRepository` (`lib/data/jmap/sieve_repository.dart`) is now a
|
||
dispatcher: `account.type == imap` routes through `ManageSieveClient`
|
||
(connecting per-call and `LOGOUT`-ing in `finally`); `account.type ==
|
||
jmap` keeps the existing JMAP path unchanged. Public API is unchanged
|
||
so the existing Sieve script list / edit screens work for both
|
||
account types. For ManageSieve, where scripts are identified by name,
|
||
`SieveScript.id` and `SieveScript.blobId` are both set to the script
|
||
name. Renames are implemented as PUTSCRIPT(new) followed by
|
||
DELETESCRIPT(old).
|
||
|
||
Account model + DB: added `manageSieveHost`, `manageSievePort` (default
|
||
4190), `manageSieveSsl` (default true) to `Account` and `Accounts`
|
||
table. Schema bumped to v15 with a forward-only migration that
|
||
`addColumn`s the three fields. Empty `manageSieveHost` falls back to
|
||
`imapHost` so the typical setup (Stalwart / Dovecot on the same host)
|
||
needs no extra configuration.
|
||
|
||
UI: removed the `account.type == AccountType.jmap` guard from the
|
||
"Email filters" entry in both `FolderDrawer` (the per-account drawer)
|
||
and the popup menu in `AccountListScreen`, so IMAP accounts now see it
|
||
too. The Add and Edit account screens grew a collapsed `ExpansionTile`
|
||
labelled "ManageSieve (email filters)" containing host / port / SSL
|
||
fields — collapsed by default so the form stays the same height for
|
||
users who accept the defaults (which avoided pushing the Save button
|
||
off the bottom of the Linux Xvfb 1280x720 viewport in the integration
|
||
test).
|
||
|
||
`scripts/check_coverage.dart` excludes `managesieve_client.dart` from
|
||
the unit-coverage gate (real-socket network code, like
|
||
`imap_client_factory.dart`). Updated `add_account_screen_test` to
|
||
expect 2 visible `SwitchListTile`s on the IMAP form (the third toggle
|
||
lives inside the collapsed ExpansionTile).
|
||
|
||
## Render HTML email bodies
|
||
|
||
`lib/ui/screens/email_detail_screen.dart` now renders the message's
|
||
`htmlBody` with the `flutter_html` widget instead of stripping tags via
|
||
`htmlToPlain`. Plain-text-only messages still render through
|
||
`SelectableText` (no HTML widget instantiated when `htmlBody` is empty).
|
||
|
||
Added `flutter_html: ^3.0.0` to `pubspec.yaml`.
|
||
|
||
Remote (`http(s)`) images are blocked by default — defeats tracking
|
||
pixels. A small "Load remote images" button appears at the top of an
|
||
HTML body and flips a per-screen flag to re-render with images. Inline
|
||
`cid:` and `data:` images fall through to the default handler. Blocking
|
||
is implemented via a small `HtmlExtension` subclass
|
||
(`_BlockRemoteImagesExtension`) that matches `<img>` whose `src` starts
|
||
with `http://` or `https://` and renders `SizedBox.shrink()`.
|
||
|
||
`htmlToPlain` is kept — it's still used by `_quotedBody` for reply /
|
||
forward quoting where plain text is correct.
|
||
|
||
No DB schema, no codegen, no migrations.
|
||
|
||
## SMTP TLS enabled by default for new accounts
|
||
|
||
User report: when creating a new Account, the SMTP SSL/TLS toggle is off by
|
||
default. The toggle should default to on so the user perception matches
|
||
"secure by default".
|
||
|
||
Changed defaults from port 587 + `smtpSsl=false` (STARTTLS on submission) to
|
||
port 465 + `smtpSsl=true` (implicit TLS on submission-over-TLS) in:
|
||
|
||
- `lib/core/models/account.dart` (constructor defaults)
|
||
- `lib/ui/screens/add_account_screen.dart` (initial state and reset path
|
||
when user picks "IMAP / SMTP" from the choose-type fallback)
|
||
- `lib/ui/screens/edit_account_screen.dart` (initial state before
|
||
loading)
|
||
- `lib/core/services/account_discovery_service.dart` (MX-record
|
||
fallback when autoconfig returns nothing)
|
||
|
||
Autoconfig XML parsing is unchanged: when a server's autoconfig advertises
|
||
`STARTTLS`, the discovery still returns `smtpSsl=false` so the client uses
|
||
STARTTLS on whichever port was advertised. Only the *fallback / blank-form*
|
||
defaults flipped.
|
||
|
||
Tests updated to match the new defaults: `test/unit/account_model_test.dart`
|
||
and the MX-fallback case in
|
||
`test/unit/account_discovery_service_test.dart`. Widget tests that pass an
|
||
explicit `ImapSmtpDiscovery(smtpPort: 587, smtpSsl: false)` to simulate
|
||
STARTTLS discovery are intentionally untouched.
|
||
|
||
## IMAP delete: locally-deleted message no longer reappears after sync
|
||
|
||
User report: deleting an IMAP message removes it from the list, but tapping
|
||
the sync button before the next background flush makes it pop back in.
|
||
|
||
Reproduced in `test/integration/email_repository_imap_test.dart` with a new
|
||
case `syncEmails after local delete does not resurrect message`: it deletes an
|
||
email locally (which queues a pending change and drops the cached row), then
|
||
calls `syncEmails` directly — exactly what the sync button does — and
|
||
asserts the row stays gone and the pending change stays queued.
|
||
|
||
Root cause: the incremental IMAP sync issues `UID ${lastUid + 1}:*` to look
|
||
for new mail. Per RFC 3501 §6.4.4 a sequence range `n:*` reverses to `*:n`
|
||
when `n` exceeds the largest UID. With one message at UID 1 and `lastUid=1`,
|
||
`UID 2:*` reverses to `*:2` and the server returns UID 1, which then gets
|
||
re-fetched and re-inserted — undoing the optimistic local delete.
|
||
|
||
Fix in `lib/data/repositories/email_repository_impl.dart`: in
|
||
`_fetchAndUpsertImap`, look up the UIDs in this mailbox that have a pending
|
||
`delete` or `move` queued and skip the insert for those. Keeping the `UID n:*`
|
||
search untouched preserves the existing E2E flow where re-fetching freshly
|
||
delivered SMTP messages drives the StreamBuilder rebuild.
|
||
|
||
Same protection guards the `move`-on-delete path (when a Trash mailbox is
|
||
configured) for free, since `moveEmail` enqueues a `move` and drops the cached
|
||
row in the source mailbox.
|
||
|
||
## task deploy-android works end-to-end
|
||
|
||
The original "Emulator did not become ready within 120 s" was already resolved in
|
||
commit `d222638` by running `adb start-server` before booting the AVD; without the
|
||
adb daemon running first, the emulator can never register as a device.
|
||
|
||
Running `task deploy-android` after that surfaced an Android-specific integration-test
|
||
failure: `aliceTile` had 0 widgets at `tester.tap()` time even though the immediately
|
||
preceding `pumpUntil(aliceTile)` had just found it. On the slow software-rendered
|
||
emulator the route-pop animation finalises during `pumpUntil`'s trailing 300 ms settle
|
||
and the tile is briefly absent right after. Fixed in
|
||
`integration_test/app_e2e_test.dart` by re-confirming `aliceTile` with a second
|
||
`pumpUntil` (5 s timeout) before the tap.
|
||
|
||
Bundled with a coherent set of pre-existing infrastructure changes that make the full
|
||
pipeline (Linux + Android UI tests, APK upload) work in `nix develop`:
|
||
|
||
- `flake.nix`: adds Linux desktop runtime libs (gtk3, mesa, libGL, libsecret, …) plus
|
||
`PKG_CONFIG_PATH`, `LD_LIBRARY_PATH`, `LIBGL_ALWAYS_SOFTWARE=1`, and the libglvnd
|
||
vendor-dir env vars so `flutter build linux` and `xvfb-run` work without a real GPU.
|
||
- `pubspec.yaml`: pins `path_provider_android` to `>=2.2.0 <2.3.0` to dodge the SIGSEGV
|
||
in `libdartjni.so` (FindClassUnchecked) on Android startup with 2.3+.
|
||
- `lib/main.dart` + `lib/data/db/database.dart`: resolves the DB path during `main()`
|
||
after `WidgetsFlutterBinding.ensureInitialized()` so the path_provider plugin channel
|
||
is registered before the first DB access.
|
||
- `stalwart-dev/integration_ui_test.sh`: passes `-screen 0 1280x720x24 +iglx` to Xvfb
|
||
so GTK3/Flutter can create a GLX OpenGL context under the virtual framebuffer.
|
||
- `.envrc`: adds `$HOME/Android/Sdk/platform-tools` to PATH so `adb` resolves outside
|
||
`nix develop`.
|
||
- `Taskfile.yml`: drops the `/usr/bin/pkg-config` hardcode in favour of PATH so the
|
||
nix-provided wrapper is found.
|
||
- `.pre-commit-config.yaml` + `scripts/pre_commit_check.sh`: consolidates `dart format`
|
||
and `task check-fast` into a single script invoked by one hook (one `nix develop`
|
||
startup instead of two).
|
||
|
||
## Replace custom search TextField with Flutter SearchBar
|
||
|
||
Replaced the hand-rolled `TextField`-in-`AppBar` search UI with Flutter's built-in `SearchBar`
|
||
widget (Material 3). The `SearchBar` is now always visible in the `AppBar`'s `bottom` slot — no
|
||
toggle needed.
|
||
|
||
Removed: `bool _searching` state field, `TextEditingController _searchCtrl`, `Timer? _searchDebounce`,
|
||
and the `_searchBar()` / `_closeSearch()` helpers.
|
||
|
||
Added: `SearchController _searchController` with a listener that clears results when text is
|
||
emptied. `onChanged` fires search immediately (no debounce); `onSubmitted` also fires it. A clear
|
||
`IconButton` appears in `trailing` when the controller has text.
|
||
|
||
Updated `integration_test/app_e2e_test.dart`: search section now enters text directly into
|
||
`find.byType(SearchBar)` — no icon tap or `TextField` lookup needed.
|
||
|
||
Updated widget tests in `test/widget/email_list_screen_test.dart`: replaced the "tapping back
|
||
arrow" test with "SearchBar is always visible in the AppBar"; fixed "clear results" test to use
|
||
`emails: []` so the stream body stays empty after clearing.
|
||
|
||
## Sieve Scripts editing is discoverable
|
||
|
||
The Sieve script editor ("Email filters") was already implemented. It became reachable
|
||
via the "Email filters" entry added to `FolderDrawer` in the previous task — no further
|
||
code changes needed.
|
||
|
||
## MX record fallback in account auto-discovery
|
||
|
||
When JMAP well-known and autoconfig XML both fail, `AccountDiscoveryServiceImpl` now
|
||
queries `https://dns.google/resolve?name={domain}&type=MX` (DNS-over-HTTPS, no new
|
||
dependency). The highest-priority MX hostname is used as both IMAP host (port 993, SSL)
|
||
and SMTP host (port 587, STARTTLS). Three unit tests cover: basic MX hit, priority
|
||
sorting, and NXDOMAIN/error fallback to `UnknownDiscovery`.
|
||
|
||
## Email filters accessible from inside an account
|
||
|
||
Added "Email filters" entry to `FolderDrawer` (below "All accounts", above the folder
|
||
list). Visible only for JMAP accounts (`accountAsync.valueOrNull?.type == AccountType.jmap`).
|
||
Tapping it closes the drawer and pushes `/accounts/:id/sieve`. Previously the Sieve script
|
||
editor was only reachable via the hidden popup menu on the account list.
|
||
|
||
## Bulk actions on search results
|
||
|
||
Long-pressing a search result enters selection mode; tapping additional results adds/removes
|
||
them. The existing bottom bar (Archive, Delete, Mark as spam, Move to folder) works on the
|
||
selection. Implementation in `email_list_screen.dart`:
|
||
|
||
- `_selectedSearchIds` (`Set<String>`) tracks selected email IDs in search results.
|
||
- `_selecting` is true when either `_selectedThreadIds` or `_selectedSearchIds` is non-empty.
|
||
- `_selectedEmailIds` returns `_selectedSearchIds` when searching, thread-resolved IDs otherwise.
|
||
- `_buildEmailList` shows checkboxes in selection mode, highlights selected tiles, and routes
|
||
taps to toggle-vs-open depending on mode.
|
||
|
||
## Multi-word search uses AND semantics
|
||
|
||
Searching for "foo bar" now returns emails that contain **both** words, not the exact
|
||
phrase. Fixed in `email_repository_impl.dart`:
|
||
|
||
- **IMAP search** (`searchEmails`): query is split on whitespace; each word becomes
|
||
`OR SUBJECT "word" TEXT "word"`, joined by spaces. Multiple top-level IMAP criteria
|
||
are implicitly ANDed by the protocol.
|
||
- **Local DB search** (`searchEmailsGlobal`): each word adds
|
||
`& (subject LIKE '%word%' | preview LIKE '%word%')` to the Drift where-clause.
|
||
|
||
## Navigate back to account list from inside an account
|
||
|
||
Added an "All accounts" tile (with `Icons.switch_account`) at the top of `FolderDrawer`,
|
||
above a divider and the folder list. Tapping it closes the drawer and navigates to
|
||
`/accounts` via `context.go`. The drawer is shown in both `MailboxListScreen` and
|
||
`EmailListScreen`, so this entry point is reachable from anywhere inside an account.
|
||
|
||
## Speed up `task deploy-android`
|
||
|
||
Parallelism improvement:
|
||
|
||
- `_integrations` internal task: runs `integration` and `integration-ui` in parallel (they use
|
||
random Stalwart ports and different Flutter build targets so there is no conflict).
|
||
|
||
## Android E2E test verifies APK before deploy
|
||
|
||
`task deploy-android` now runs `integration-android` (the full Android E2E test) before
|
||
uploading the APK. If the app crashes on start or any E2E step fails, the deploy is skipped.
|
||
|
||
Key fixes to make the Android E2E test reliable:
|
||
|
||
- `Taskfile.yml`: moved `integration-android` to a sequential `cmds` step after `check`,
|
||
so the two E2E suites don't compete for CPU and slow the emulator.
|
||
- `stalwart-dev/integration_android_test.sh`: wrapped `force-stop`/`pm clear`/`uninstall`
|
||
in a `pm list packages | grep -qF` check — only runs when the package is installed, so
|
||
any real failure is surfaced instead of silently suppressed.
|
||
- `integration_test/app_e2e_test.dart`:
|
||
- `pumpUntil` uses `pump(300ms)` instead of `pumpAndSettle()` so a concurrently
|
||
running spinner never blocks settling.
|
||
- `accountConnectionStatusProvider` overridden to complete immediately, eliminating the
|
||
`CircularProgressIndicator` in `_AccountTile` that caused `pumpAndSettle` to deadlock.
|
||
- Search section: `FocusManager.instance.primaryFocus?.unfocus()` dismisses the Android
|
||
IME keyboard before polling for results — without this, the soft keyboard reduces
|
||
`viewInsets.bottom` to near-zero and `ListView.builder` renders 0 items even though
|
||
search results are present.
|
||
|
||
---
|
||
|
||
## Override accountConnectionStatusProvider in E2E test (fix Android pumpAndSettle deadlock)
|
||
|
||
`accountConnectionStatusProvider` overridden in `integration_test/app_e2e_test.dart` so
|
||
`_AccountTile` never shows a `CircularProgressIndicator` during tests. The spinner's
|
||
continuous animation prevented `pumpAndSettle()` from settling on Android. Reverted
|
||
`pumpUntil` to use `pumpAndSettle()` again. Commit: e50ff3c.
|
||
|
||
---
|
||
|
||
## Fix task check: unencrypted IMAP error + coverage gate
|
||
|
||
- `account_sync_manager_test.dart`: inject `_connectImapPlain` (bypasses the production SSL check) so the test works against the plain-IMAP dev Stalwart.
|
||
- `scripts/check_coverage.dart`: add three new screens (`sieve_script_edit_screen`, `sieve_scripts_screen`, `thread_detail_screen`) and `sieve_repository` to `_excluded` (all are screens/JMAP clients without unit tests).
|
||
- New unit tests: `sieve_script_test.dart`, plus `findMailboxByRole`, JMAP no-URL error, and JMAP API error tests in `mailbox_repository_impl_test.dart`.
|
||
- New widget tests: `try_connection_button_test.dart` (okMessage/errorMessage rendering) plus selection-mode, deselect, search-clear, and search-result-tap tests in `email_list_screen_test.dart`.
|
||
- Fixed `FakeEmailRepository.observeThreads` in `helpers.dart` to propagate `preview` from email to thread.
|
||
- Coverage gate now passes at 80%+ (84% with integration coverage merged).
|
||
|
||
## Android integration test via Stalwart
|
||
|
||
Added `stalwart-dev/integration_android_test.sh` and `task integration-android`. Starts Stalwart on random ports, detects a connected emulator via `adb devices`, sets `STALWART_IMAP_HOST=10.0.2.2` (emulator host alias), and runs the existing `integration_test/app_e2e_test.dart` on the emulator.
|
||
|
||
## Quote original message in reply, and add Forward button
|
||
|
||
`_reply` now passes `prefillBody` with the original message quoted as plain
|
||
text (`> line…`). New `_forward` method and Forward toolbar button added;
|
||
sets `Fwd:` subject prefix and prefills the same quoted body with To/Cc empty.
|
||
|
||
## Mark as unread button in email detail
|
||
|
||
Added `mark_email_unread_outlined` icon button to `EmailDetailScreen` toolbar.
|
||
Calls `setFlag(seen: false)` then pops back to the list.
|
||
|
||
## Pull-to-refresh on email list
|
||
|
||
Wrapped `_buildStreamBody` in a `RefreshIndicator` that calls `syncEmails`.
|
||
The empty-state is now a scrollable `ListView` so the pull gesture works even
|
||
when the folder has no messages.
|
||
|
||
## Show email preview snippet in list
|
||
|
||
Added `preview` field to `EmailThread` (populated from the latest email in
|
||
`_groupIntoThreads`). Thread tiles now show subject + a one-line preview
|
||
snippet in the subtitle.
|
||
|
||
## Extract TryConnectionButton widget shared by account screens
|
||
|
||
Extracted `lib/ui/widgets/try_connection_button.dart` — a stateless widget
|
||
rendering the result banner (ok/error text) and the spinner/button. Both
|
||
`add_account_screen` and `edit_account_screen` now use it, removing ~30 lines
|
||
of duplicated UI code.
|
||
|
||
## Extract _batchMoveToRole helper in email_list_screen
|
||
|
||
`_batchArchive()` and `_batchMarkSpam()` collapsed into a shared
|
||
`_batchMoveToRole(role, notFoundMessage)` helper, eliminating ~20 lines of
|
||
duplication.
|
||
|
||
## Enable always_use_package_imports lint rule
|
||
|
||
Added rule to `analysis_options.yaml`; `dart fix --apply` converted 125 relative
|
||
imports across 33 files to `package:sharedinbox/...` style automatically.
|
||
|
||
## Replace silent catch (_) with logged errors
|
||
|
||
5 `catch (_)` blocks in JMAP push stream setup and 2 in UI screens now use
|
||
`catch (e)` with `log(...)` via the project's `logger.dart` wrapper.
|
||
The two intentionally silent catches (malformed SSE JSON, Sent folder already
|
||
exists) were left as-is since they already had explanatory comments.
|
||
|
||
## Safety hardening before real account use
|
||
|
||
### 1. Fix non-PEEK body fetch (silently sets \Seen)
|
||
|
||
`lib/data/repositories/email_repository_impl.dart` ~line 163
|
||
Change `'(BODY[])'` → `'(BODY.PEEK[])'` so fetching the body does not set \Seen
|
||
as a side-effect of the IMAP FETCH command.
|
||
|
||
Same fix at ~line 1696 for attachment part fetches: `BODY[partId]` → `BODY.PEEK[partId]`.
|
||
|
||
### 2. Move to Trash instead of EXPUNGE
|
||
|
||
`lib/data/repositories/email_repository_impl.dart` `deleteEmail` method
|
||
Before enqueuing a hard delete, query the local mailboxes cache for a 'trash'-role
|
||
folder for that account.
|
||
|
||
- If found AND the email is not already in Trash: call `moveEmail` to that path.
|
||
- If not found OR already in Trash: fall back to the existing EXPUNGE path.
|
||
|
||
This makes delete reversible — the user can recover from Trash.
|
||
|
||
### 3. Confirmation dialog for delete
|
||
|
||
Three call sites need a `showDialog` confirmation before deleting:
|
||
|
||
a) Delete button in detail view
|
||
`lib/ui/screens/email_detail_screen.dart` ~line 97
|
||
Show AlertDialog "Delete this email?" with Cancel / Delete buttons.
|
||
|
||
b) Batch delete in list view
|
||
`lib/ui/screens/email_list_screen.dart` `_batchDelete` ~line 268
|
||
Show AlertDialog "Delete N emails?" with Cancel / Delete buttons.
|
||
|
||
c) Swipe-to-delete in list view
|
||
`lib/ui/screens/email_list_screen.dart` `Dismissible.onDismissed` ~line 436
|
||
Use `Dismissible.confirmDismiss` callback (fires before the item is removed)
|
||
to show a confirmation for the right-swipe (delete) direction only.
|
||
Return false to cancel and keep the item in the list.
|