- Added UndoService with 10-action history stack.
- Integrated Undo Snackbar into EmailListScreen and EmailDetailScreen.
- Added EmailRepository.cancelPendingChange to optimize undo by removing
unsynced local mutations.
- Fixed sorting bug in compareMailboxes for unknown roles.
- Increased unit coverage to 83% with new model and utility tests.
- Verified with full test suite (task check).
- Extended search to support global queries across all accounts.
- Updated SearchScreen to handle optional account context and unified results.
- Centralized mailbox comparison logic in Mailbox model.
- Added copyWith to Account model.
- Fixed race conditions and incorrect overrides in unit, widget, and integration tests.
- Reached 80% unit test coverage.
- Created ThreadDetailScreen with expandable email cards and HTML support.
- Added observeEmailsInThread to EmailRepository and implementation.
- Updated navigation in EmailListScreen to route multi-message threads to the new view.
- Updated test mocks (FakeEmailRepository) across unit, widget, and integration tests.
- Documented progress in done.md and updated next.md.
- Fix IMAP attachment sizes showing as '0 B' by falling back to decoded content length.
- Fix IMAP attachment downloads failing for single-part fetches by falling back to root message.
- Implement immediate server-side sync for deletions/flags using a new onChangesQueued stream.
- Automate FVM SDK setup and add 'task setup'.
- Make MobSF optional in build-android to handle environment restrictions.
- Update test fakes to match EmailRepository interface changes.
Hides the SSL/TLS toggle in add/edit account screens when the host is
not localhost; enforces SSL in connectImap/connectSmtp for non-localhost
hosts so plaintext can never be configured accidentally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The "Email filters" screen was failing with WRONG_VERSION_NUMBER because the
ManageSieve client was opening implicit-TLS sockets on port 4190, while RFC
5804 servers (Stalwart, Dovecot, Cyrus) listen plaintext on 4190 and expect
STARTTLS. ManageSieveClient.connect now opens plaintext, reads the capability
greeting, sends STARTTLS, hands the socket to SecureSocket.secure(), and
re-reads capabilities on the encrypted stream.
The same WRONG_VERSION_NUMBER error can hit IMAP/SMTP when the SSL toggle and
the chosen port disagree (e.g. SSL=on with SMTP port 587). New helper
lib/data/imap/tls_error.dart translates that BoringSSL error into a
TlsModeMismatchException naming the host/port and suggesting which port goes
with which TLS mode. connectImap, connectSmtp, and the ManageSieve TLS
upgrade all funnel through rethrowAsTlsHint so the same readable message
reaches the UI regardless of which protocol failed.
ConnectionTestService 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 — sending mail requires it) and ManageSieve (only
when manageSieveHost is explicitly set, since the section is collapsed by
default). Failures are prefixed with "SMTP:" or "ManageSieve:" so the
user can tell which leg of the connection is broken.
connectionTestServiceProvider now also watches smtpConnectProvider so the
E2E integration tests' plaintext SMTP override applies to the connection
check as well.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Email detail screen now renders the message htmlBody with the flutter_html
widget when present, falling back to SelectableText for plain-text-only mail.
Remote http(s) images are blocked by default to defeat tracking pixels; an
opt-in "Load remote images" button reveals them per-screen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Android UI integration test failed at tap(aliceTile) with "0 widgets"
even though pumpUntil had just found the tile. On the slow software-rendered
emulator the route-pop animation finalises during pumpUntil's trailing 300 ms
settle, briefly leaving the tile out of the tree. Re-confirm with a second
pumpUntil before the tap.
Bundles the previously uncommitted infra changes that make task deploy-android
run end-to-end inside nix develop: Linux desktop runtime libs + GL software
rendering env in flake.nix, path_provider_android pin to <2.3 to avoid the
libdartjni SIGSEGV, deferred DB-path resolution after WidgetsFlutterBinding,
+iglx for xvfb-run, platform-tools on PATH, and a single pre-commit script
replacing the dart-format / task-check-fast pair.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SearchBar is always visible in the AppBar bottom slot — no toggle needed.
Removed _isSearching, manual debounce timer, and slide animation.
SearchController listener clears results when text is emptied.
Updated E2E and widget tests for the new widget tree.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When JMAP well-known and autoconfig XML both fail, query DNS-over-HTTPS
(dns.google) for MX records and use the highest-priority MX host as IMAP
(993/SSL) and SMTP (587/STARTTLS) server. No new dependencies needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Sieve script editor was only reachable via the hidden popup menu on
the account list. Adding an 'Email filters' tile to the drawer makes it
discoverable from any mailbox view for JMAP accounts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Long-pressing a search result enters selection mode; tapping further
results toggles them. The existing bottom bar (Archive, Delete, Mark
as spam, Move to folder) operates on the selected emails via a new
_selectedSearchIds set. _selectedEmailIds returns the right IDs for
each mode (search vs. normal thread list).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Searching for "foo bar" now matches emails containing both words rather
than the exact phrase. Each whitespace-separated term generates its own
IMAP criterion (OR SUBJECT "w" TEXT "w") — multiple top-level IMAP criteria
are ANDed by the protocol — and its own Drift LIKE clause for the local DB.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users had no obvious way to return to the account list after opening an
account. Adding an 'All accounts' tile at the top of the drawer (visible in
both MailboxListScreen and EmailListScreen) lets users navigate to /accounts
from anywhere inside an account.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Start MobSF as a dep of build-android (_mobsf-start) so it warms up during
the APK build instead of blocking afterwards (saves up to 90 s). Add
_integrations task to run integration and integration-ui in parallel since
they use random Stalwart ports and different Flutter build targets.
Also add `docker rm ... || true` before `docker run` in both _mobsf-start and
mobsf_scan.sh to handle stopped-but-not-removed containers gracefully.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Android, the soft keyboard keeps viewInsets.bottom non-zero while the
search TextField is focused. ListView.builder is allocated near-zero
height and renders 0 items, so find.text(subject) always finds nothing
even though the IMAP search returned results. Unfocusing the primary
focus after enterText dismisses the keyboard and gives the results list
full body height before pumpUntil starts polling.
Also fix pumpUntil to use pump(300ms) instead of pumpAndSettle() so a
continuously-running animation (spinner under CPU load) never prevents
settling, and override accountConnectionStatusProvider so _AccountTile
never shows a CircularProgressIndicator during the test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CircularProgressIndicator in _AccountTile (from accountConnectionStatusProvider)
runs continuously and prevents pumpAndSettle() from ever settling on Android,
causing frame-pump storms that drop the StreamBuilder data state and make
tap(aliceTile) find 0 widgets.
Overriding the provider to return immediately means no spinner ever enters the
tree, so pumpUntil() can use pumpAndSettle() cleanly again.
Also adds task run-android (boots sharedinbox_test AVD and runs flutter run).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reply and reply-all now prefill the compose body with the original message
quoted as plain text (> lines). New Forward button sets Fwd: subject and
the same quoted body, leaving To/Cc empty for the user to fill.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added mark_email_unread_outlined icon to the toolbar. Calls setFlag(seen:false)
then pops back to the list so the email reappears as unread.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrapped the stream body in RefreshIndicator calling syncEmails.
Empty-state is now a scrollable ListView so the pull gesture triggers
even when the folder contains no messages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added preview field to EmailThread (from latest email's preview via
_groupIntoThreads). Thread tiles now show subject + one-line body snippet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>