# 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.
