## Summary
Closes#377
- Adds a new `ImageTrustedSenders` Drift table (schema v37) that stores email addresses for which remote images are loaded automatically (per device, not per account)
- When the user taps "Load remote images", the sender's address is saved and a 3-second snackbar appears with a "Settings" hyperlink to undo the choice in preferences
- Both `EmailDetailScreen` and `ThreadDetailScreen` check the trusted senders list on open and auto-load images for known senders
- The Preferences screen gains a new "Trusted image senders" section listing all saved senders with individual remove buttons
## Test plan
- [x] `dart run build_runner build` regenerates `database.g.dart` cleanly (schema v37)
- [x] `flutter analyze` — no issues
- [x] Migration test updated: checks `image_trusted_senders` table exists after upgrade and fresh install
- [x] `FakeUserPreferencesRepository` updated with three new interface methods
- [x] All 490 unit + widget tests pass (1 pre-existing golden test failure unrelated to this change)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/378
## What changed
`searchAddresses` (used by the To/Cc/Bcc autocomplete) now runs two passes over the candidate email rows:
1. **Sent-folder rows first** — the mailboxes table is queried for mailboxes with `role='sent'`; any email row whose `mailboxPath` matches gets processed before inbox/other rows. Within this group addresses are ordered by `receivedAt` DESC as before.
2. **All other rows** — processed after sent rows, also by `receivedAt` DESC.
Within sent-folder rows, `toAddresses` and `ccJson` are checked before `fromJson` (the sender in a sent email is our own address, not a useful suggestion). For non-sent rows the original order (`fromJson`, `toAddresses`, `ccJson`) is kept.
This means: if you wrote to `info@foo.de` yesterday and received spam from `info@spam.de` today, typing "i" surfaces `info@foo.de` first.
## How verified
- All 492 unit tests pass (`task test`).
- Added a dedicated test `searchAddresses prioritises sent-folder addresses over newer received` that inserts an older sent email and a newer received email matching the same query prefix and asserts the sent-folder address is returned first.
Closes#375
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/380
Closes#372
## What changed
- **New widget** `lib/ui/widgets/email_headers_dialog.dart`: full-screen header browser that organises headers into collapsible groups:
- **Headers** — all standard headers (expanded by default)
- **List- Headers** — all `List-*` headers grouped together (expanded)
- **Received** — all `Received` headers, **collapsed by default**; shows the inter-hop duration between consecutive entries and highlights delays in colour (green < 30 s, orange < 5 min, red >= 5 min)
- **ARC- Headers** — all `ARC-*` headers (above X-, expanded)
- **X-Prefix Headers** — X- headers split by their second component (e.g. `X-Google-*` → "X-Google Headers"), sorted alphabetically, at the very bottom
- **`email_detail_screen.dart`**: `_showHeaders` now uses `EmailHeadersDialog`; `_showStructure` converted from `AlertDialog` to `Dialog.fullscreen()` — satisfying "Make popup windows full screen."
- **`scripts/check_coverage.dart`**: new widget file added to the `_excluded` set (UI widgets are covered by integration tests, not unit tests).
## Verified
`task check` passes (analyze: no issues, 491 unit tests pass, coverage >= 80 %).
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/374
During _load(), check whether a password exists in secure storage and track the result
in _hasStoredPassword. The password field validator now requires user input when no
password is stored, so _tryConnection() fails fast at form validation instead of
throwing an unhandled StateError.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two issues from #179:
- crash_screen.dart now reads GIT_HASH compile-time constant and includes
'Git Commit: <hash>' in both the on-screen UI and the copied report, so
crash reports always show the exact build that crashed.
- _resolveDatabasePath() retry delays extended from [100, 300, 600] ms
(total ~1 s, 4 attempts) to [200, 500, 1000, 2000, 4000] ms (total
~7.7 s, 6 attempts) to handle slow/non-standard Android devices where
the path_provider Pigeon channel takes several seconds to become ready.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
- Increases the retry delays in `_resolveDatabasePath()` from `[100, 300, 600]` ms (~1 s) to `[200, 500, 1000, 2000]` ms (~3.7 s).
- Adds a regression test (`test/unit/database_path_test.dart`) that verifies `initDatabasePath()` does not throw when the `path_provider` channel is unavailable.
## Root cause
On some slow Android devices (e.g. the Motorola reported in #166), the `path_provider` Pigeon channel is not ready even several seconds after `runApp()` returns. The previous back-off budget of ~1 s was not enough, causing `_resolveDatabasePath()` to exhaust all retries and throw a `PlatformException`, crashing the app with the message shown in the issue.
## Test plan
- [ ] `flutter test test/unit/database_path_test.dart` passes (new regression test)
- [ ] `flutter test test/unit/` — all 325 unit tests pass
- [ ] `flutter analyze` — no issues
Fixes#166
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/169
On some Android versions the path_provider Pigeon channel
('dev.flutter.pigeon.path_provider_android.PathProviderApi.getApplicationSupportPath')
is not ready when initDatabasePath() runs before runApp(). The existing code
already catches PlatformException there, leaving _dbPath null — but the
LazyDatabase callback called getApplicationSupportDirectory() a second time
without any protection, causing an unhandled crash on those devices.
Fix: extract _resolveDatabasePath() which retries three times with back-off
(100 ms → 300 ms → 600 ms) before re-throwing with a descriptive error
message. By the time the database is first accessed (after runApp()), the
channel is almost always available; if it still isn't, the CrashScreen is
shown with a clear explanation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
flutter_markdown 0.7.7+1 has been discontinued in favour of
flutter_markdown_plus. Switch the dependency and update both import
sites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On some Android devices (e.g. Android S1RXS32.50-13-25) the WorkManager
platform channel fails to connect at startup, throwing
PlatformException(channel-error, ...). registerBackgroundSync() now catches
PlatformException and MissingPluginException (plus any other unexpected
failure) and silently disables background sync rather than crashing the app.
Test added: test/unit/background_sync_test.dart verifies the function
completes without throwing in the unit-test environment (where the native
plugin is absent).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes:
1. notification_service.dart: initNotifications() now catches
MissingPluginException (and any other init failure) so the app no
longer crashes when flutter_local_notifications is unavailable on
some Android devices. _initialized tracks success; showNewMailNotification
skips the plugin call when it never initialised.
2. crash_screen.dart: "Report Issue on Codeberg" no longer puts the full
report in the URL query string. Long stack traces exceeded browser
URL-length limits and caused "create issue failed". The URL now
carries only the pre-filled title; the user copies the full report
via "Copy to Clipboard" and pastes it in the issue body.
Tests added:
- test/unit/notification_service_test.dart: verifies initNotifications()
completes without throwing when the plugin channel is unavailable.
- test/widget/crash_screen_test.dart: verifies the Codeberg URL contains
the title but no &body= parameter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs caused the crash-at-startup report:
1. CrashScreen used the widget's build context (above its own MaterialApp)
for ScaffoldMessenger.of() in button callbacks. When the screen is the
root widget — the runApp() path after a startup crash — there is no
ScaffoldMessenger above it, so both 'Copy to Clipboard' and 'Report Issue
on Codeberg' crashed with a null check error. Fix: wrap Scaffold.body in
Builder to obtain a context that is a descendant of the Scaffold.
2. path_provider_android 2.2.21 updated to Pigeon 26, which causes a
channel-error on startup for some Android devices. Pin to <2.2.21
(resolves to 2.2.20, which uses the stable pre-Pigeon-26 implementation).
Additionally, make initDatabasePath() catch PlatformException so a
channel error at the very start of main() no longer hard-crashes the app;
_openConnection()'s lazy fallback retries after runApp() completes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add LocalSieveApplied table (schema v32) keyed by (accountId, messageId)
so each email is processed by Sieve at most once, even across restarts.
- Implement EmailRepository.applySieveRules(): loads the active local Sieve
script, runs the interpreter against new INBOX emails, and queues pending
move/delete/flag_seen changes for any matched rules.
- Wire applySieveRules() into both _AccountSync._sync() and
_JmapAccountSync._sync() after the per-mailbox email sync loop.
- Make _flushPendingChangesImap() treat NONEXISTENT / not-found errors as
silent no-ops (counts as flushed) so a second device racing on the same
email does not accumulate retries.
- Add migration test assertions and a dedicated unit test suite covering
rule matching, deduplication, discard, and multi-email processing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
unawaited saveAction/deleteAction calls in pushAction could outlive the
test and access the SQLite connection after tearDown closed it, causing
the native FFI layer to hit freed memory (SIGBUS / exit code -7).
Making both DB calls awaited ensures pushAction only returns once the
action is fully persisted, eliminating the race condition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements a three-phase Sieve email filtering pipeline:
- Data models (SieveCondition, SieveAction, SieveRule) as sealed Dart classes
- SieveParser: converts RFC 5228 Sieve scripts to a flat SieveRule list,
supporting if/elsif/else, allof/anyof, header/size/exists tests, and all
common actions (fileinto, keep, discard, flag, mark)
- SieveInterpreter: evaluates compiled rules against a SieveEmailContext,
tracking routing state in SieveExecutionContext with implicit keep behaviour
- 40 unit tests covering parser correctness and interpreter execution
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Save the .eml file to the temporary directory (reliable on all
platforms) and display a Share action in the SnackBar so users can
send the file to any app — including the Files app — which properly
registers it with Android's MediaStore and makes it visible in the
recently-used list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>