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>
## 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 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>
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>
After a successful download, Navigator.pop is called so the dialog
dismisses without requiring a manual close. Adds a widget test that
verifies this using a fake PathProviderPlatform and IOOverrides so the
entire async chain runs as pure microtasks inside the Flutter test zone.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Actions persisted to the database triggered a snackbar when the app
restarted. Added a 30-second recency check so only actions created in
the current session show the snackbar; added widget tests covering both
cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename 'Local email filters' → 'Local Filters' and 'Server email
filters' → 'Remote Filters' in AppBar titles
- Update banner text on each filter page to focus on the current type
and mention that the other type exists separately
- Add 'Remote Filters' and 'Local Filters' as two distinct drawer
entries so both types are discoverable from the navigation
- Add widget tests verifying titles and banner text for both pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the insecure plaintext QR export/import flow with an
end-to-end-encrypted account-transfer mechanism:
- Receiver generates an ephemeral X25519 key pair (20-minute lifetime,
stored in the new share_keys DB table at schema v31) and displays it
as a QR code (sharedinbox.de:pubkey:v1:…).
- Sender scans the public-key QR, selects accounts (or auto-selects
when only one exists), encrypts them with ECIES (X25519-ECDH +
HKDF-SHA256 + AES-256-GCM) and displays an encrypted QR
(sharedinbox.de:encrypted-accounts:v1:…).
- Receiver scans the encrypted QR, decrypts, verifies the 20-minute
expiry and MAC authentication tag, then imports the accounts.
New screens: AccountReceiveScreen (/accounts/receive) and
AccountSendScreen (/accounts/send), accessible from the account-list
drawer and per-account popup menu respectively.
Remove the old insecure AccountExportScreen and AccountImportScreen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows version, platform, OS version, screen resolution, Dart version, and
processor count in a markdown table. Buttons let users copy the info to
clipboard or open a pre-filled Codeberg issue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Track how long each mailbox takes to sync and display it in the
sync log expanded view (e.g. "2 new · 5 up-to-date · 1.3s").
- Add optional `duration` field to `MailboxSyncStats`
- Capture per-mailbox start/end time in both IMAP and JMAP sync loops
- Store as `duration_ms` in `sync_log_mailboxes` (schema v30 migration)
- Read back and reconstruct `Duration` in repository
- Show timing alongside fetch/skip counts in per-mailbox breakdown
- Extract `_fmtDuration` helper, reuse for the existing total duration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add "Force full sync" popup menu item below "Verify sync health" in the
per-account menu on the account list screen, with a confirmation dialog.
Remove the button and handler from the edit account screen.
HTML emails with black text became unreadable when viewed in dark mode
because the WebView inherited a dark background from the system theme.
Inject `color-scheme: light` CSS + meta tag so the WebView always renders
email content on a white background, regardless of the device theme.
Extracts `buildEmailHtml()` as a `@visibleForTesting` top-level function
and adds unit tests to cover the light-mode enforcement and CSP logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace getTemporaryDirectory() + OpenFilex.open() with
getDownloadsDirectory() (fallback to temp) so the .eml file lands in
the public Downloads folder instead of triggering Android's
"open with" dialog.
- Show a SnackBar with the saved path after download instead of
launching a file viewer.
- Display the email size (via fmtSize) at the top of the Raw Email
dialog, above the scrollable content.
- Add widget test covering the size display in the Raw Email dialog.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Searching for "foo" now finds "foobar" (prefix of a word) but not
"blafoo" (suffix). The FTS5 query already used the foo* prefix form;
this commit extends the same semantics to folder-name and address
matching in the search screen, replacing contains() with a
word-boundary regex check. Tests added for all three paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>