Commit Graph
232 Commits
Author SHA1 Message Date
692fa14d4d feat: remember show images per sender (#378)
## 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
2026-06-04 01:41:50 +02:00
5e029a1365 feat: prioritise sent-folder addresses in To/Cc/Bcc autocomplete (#380)
## 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
2026-06-04 00:27:04 +02:00
87244de7da feat: group email headers in full-screen dialog (#374)
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
2026-06-03 22:14:14 +02:00
Thomas Güttler 8ea8d71f42 fix: format, analyze-fix and update mocks 2026-06-02 17:10:16 +02:00
Thomas Güttler 1e2d1b6063 chore: migrate to SOPS and SSH for Dagger engine access 2026-06-02 11:10:29 +02:00
Bot of Thomas Güttler 264ce7e349 fix: guard against empty IMAP fetch message list (#346) 2026-06-01 21:48:21 +02:00
Bot of Thomas Güttler b3f5ad4110 fix: add try-catch to _measureHeight() in secure_email_webview.dart (#345) 2026-06-01 21:47:53 +02:00
Bot of Thomas Güttler c6e7c035f2 fix: guard threadEmails.last against empty list (#343) 2026-06-01 21:47:47 +02:00
Bot of Thomas Güttler 05d00bdf09 fix: move overflow actions into popup menu so three-dot menu is always visible (#312) (#323) 2026-05-28 07:19:11 +02:00
Bot of Thomas Güttler c45775be92 fix: move sync health report to own row below each account (#311) (#322) 2026-05-28 06:53:11 +02:00
Bot of Thomas Güttler f0f210e5ab feat: configurable next action after single mail view (#300) (#308) 2026-05-27 23:33:14 +02:00
Bot of Thomas Güttler 41550eb4b5 feat: configurable menu bar position for mailbox view (#298) (#303) 2026-05-27 22:07:12 +02:00
Bot of Thomas Güttler 633fc5d9da fix: show full discrepancy details in account list (#296) (#301) 2026-05-27 21:20:19 +02:00
Bot of Thomas Güttler 14f64cd2a5 feat: show URL tooltip on long-press of unsubscribe chip (#294) (#295) 2026-05-27 21:02:30 +02:00
Bot of Thomas Güttler e2b08e07b7 fix: prevent HTML email content from being cut off (#288) (#292) 2026-05-27 19:52:14 +02:00
Bot of Thomas Güttler c0dd13be5d feat: align single and multi-mail actions, add archive (#287) (#291) 2026-05-27 19:36:13 +02:00
Bot of Thomas Güttler 4e32984ecc fix: prompt to create or pick folder when archive is missing (#286) (#290) 2026-05-27 19:06:37 +02:00
Bot of Thomas Güttler f57a8c502d feat: syncLog add Copy button, stack trace, isPermanent (#266) (#269) 2026-05-26 07:55:07 +02:00
Bot of Thomas Güttler 8709e9f38d feat: add Locale, Text Scale, DB Schema Version, Device Model to About page (#258) (#263) 2026-05-25 22:18:09 +02:00
Bot of Thomas Güttler 7997ff0980 feat: Reply All dialog on Reply button, add Mark as Spam (#260) (#261) 2026-05-25 21:51:08 +02:00
Bot of Thomas Güttler a7783d46cf fix: disable Save button when no password available; fix changelog fetch-depth (#246, #229) (#248) 2026-05-25 14:47:25 +02:00
Bot of Thomas Güttler 3868c160d3 fix: disable Try connection button when no password is available (#235) (#247) 2026-05-25 14:30:13 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 885906b204 fix: show password required error instead of crashing when no stored password (#235)
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>
2026-05-25 12:49:29 +02:00
Bot of Thomas Güttler e03c7708ba feat: show app version as link on crash screen and in MD report (#236) (#245) 2026-05-25 11:40:53 +02:00
Bot of Thomas Güttler 09c90c244b fix: load changelog via DefaultAssetBundle for testability (#214) (#225) 2026-05-24 17:50:10 +02:00
Bot of Thomas Güttler 357ed9af31 fix: about page version unknown and link crash on Android (#213) (#224) 2026-05-24 17:20:09 +02:00
Bot of Thomas Güttler e7ff9243c9 feat: add build mode, Dart version, timestamp to crash report (#205) (#222) 2026-05-24 16:10:09 +02:00
Bot of Thomas Güttler d51e67ddcc fix: probe scanner method channel to detect MissingPluginException (#204) (#221) 2026-05-24 15:55:08 +02:00
Bot of Thomas Güttler 43068509d2 fix: show live countdown with seconds on receive account screen (#203) (#220) 2026-05-24 15:15:12 +02:00
Bot of Thomas Güttler 50ae7df8a3 fix: fall back to text input when mobile_scanner plugin is unavailable (#202) (#219) 2026-05-24 14:55:07 +02:00
Bot of Thomas Güttler 5925cee4f2 fix: show git hash as clickable link above stacktrace (#201) (#211) 2026-05-24 12:56:27 +02:00
Bot of Thomas Güttler 0293cb5845 fix: stop retrying on MissingPluginException from flutter_secure_storage (#200) (#209) 2026-05-24 08:50:06 +02:00
Bot of Thomas Güttler ac0e16adcb feat: about page - sharedinbox.de heading link and git commit row (#199) (#206) 2026-05-24 08:10:07 +02:00
Bot of Thomas Güttler 71ccf24d0c fix: survive permanently broken path_provider channel on Android (#192) (#194) 2026-05-24 03:50:07 +02:00
Bot of Thomas Güttler 4f6f1d9437 fix: migrate to Riverpod 3.x and update dependencies (#175) (#190) 2026-05-23 19:50:11 +02:00
Bot of Thomas Güttler 11d9805fca test: cover _resolveDatabasePath retry logic (#167) (#187) 2026-05-23 18:35:15 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 6e22683f5b fix(crash_screen): remove duplicate gitLine definition left by rebase conflict resolution
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 17:02:39 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 dc181d0d85 fix: add git hash to crash screen and extend DB path retries (#179)
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>
2026-05-23 16:53:07 +02:00
826488192d fix: update flutter packages (#148) (#165)
## Summary

- Upgrades 9 direct dependencies and their transitive peers to resolve the CI warning: *"38 packages have newer versions incompatible with dependency constraints"*
- Reduces incompatible-version count from **38 → 21** (the remaining 21 are either deliberately pinned, constrained by transitive dep ceilings, or require a separate riverpod 2→3 migration)
- Adapts two source files to breaking API changes in the upgraded packages:
  - `notification_service.dart`: `flutter_local_notifications` 21.x changed positional args to named params (`initialize(settings:…)`, `show(id:…, title:…, body:…, notificationDetails:…)`)
  - `compose_screen.dart`: `file_picker` 12.x removed `FilePicker.platform` static getter; calls are now `FilePicker.pickFiles()`

## Packages changed

| Package | Before | After |
|---|---|---|
| `go_router` | ^14.8.1 | ^17.2.3 |
| `flutter_local_notifications` | ^18.0.1 | ^21.0.0 |
| `file_picker` | ^8.0.0 | ^12.0.0-beta.4 |
| `mobile_scanner` | ^5.0.0 | ^7.2.0 |
| `package_info_plus` | ^8.0.0 | ^10.1.0 |
| `share_plus` | ^12.0.2 | ^13.1.0 |
| `sqlite3_flutter_libs` | ^0.5.28 | ^0.6.0+eol |
| `flutter_lints` | ^4.0.0 | ^6.0.0 |
| `flutter_secure_storage` | 10.2.0 | 10.3.0 (patch) |

## Test plan

- [x] `flutter analyze` — no issues
- [x] Unit tests (324 passed)
- [x] Widget tests (116 passed)
- [ ] CI full check suite

🤖 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/165
2026-05-23 14:58:54 +02:00
77fc6964f6 fix: extend path_provider retry budget on slow Android devices (#166) (#169)
## 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
2026-05-23 14:40:17 +02:00
Bot of Thomas Güttler a6c231f293 feat: show git commit link on crash screen (#150) (#170) 2026-05-23 13:45:08 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 8a4ca223e9 fix: retry path_provider on PlatformException at database open (#153, #157)
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>
2026-05-23 10:08:04 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 acd9483e8b chore: replace flutter_markdown with flutter_markdown_plus (#147)
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>
2026-05-22 16:44:10 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 a1cd31a2eb fix: survive PlatformException(channel-error) in registerBackgroundSync (#149)
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>
2026-05-22 14:23:40 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 f7d021c62a fix: survive MissingPluginException on startup, fix crash report URL (#146)
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>
2026-05-22 13:01:34 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 23cbe4611c fix: resolve startup crash and CrashScreen button crashes (#127)
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>
2026-05-22 11:16:09 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 517f799b99 feat: apply local Sieve rules after sync (#119)
- 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>
2026-05-17 10:34:21 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 01409a164b fix(undo): await DB writes in pushAction to prevent SIGBUS in tests
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>
2026-05-17 07:37:43 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 606958e675 feat: sieve transpilation to intermediate rule-list with parser and interpreter (#117)
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>
2026-05-16 22:55:46 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 de66081813 feat: save raw email to temp dir and add Share action to SnackBar (#115)
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>
2026-05-16 17:57:31 +02:00