Commit Graph
116 Commits
Author SHA1 Message Date
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 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 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 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 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 11d9805fca test: cover _resolveDatabasePath retry logic (#167) (#187) 2026-05-23 18:35:15 +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
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 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 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 04e65d2fba feat: secure account sharing via public-key encryption (#107)
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>
2026-05-16 01:19:01 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 9763a1884a feat(sync-log): add per-mailbox timing to sync log (#104)
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>
2026-05-15 22:03:36 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 9d19bdb81b feat(search): match word prefix, not arbitrary substring (#96)
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>
2026-05-15 20:20:16 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 4a25d831fb fix(sync-health): checkNow() now runs regardless of start() (#95)
checkNow() previously delegated to _runAll(), which gated each
account on the _running flag (only true after start() is called).
This meant the manual "Verify sync health" action silently did nothing
if start() had not yet been called, or in any context where the
periodic runner was not active (e.g. widget tests).

Fix: checkNow() now iterates accounts directly and calls
_runForAccount() with force:true, bypassing the _running guard.
The guard is still respected during periodic runs for graceful
shutdown.

Adds three unit tests that reproduce the bug and verify the fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 19:54:39 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 0620663630 feat(sieve): local email filters alongside server filters (#90)
Reuse the same Sieve UI for both server-side (ManageSieve/JMAP) and local
email filters. Both filter sets are stored and managed independently.

Changes:
- Add LocalSieveScripts table (DB schema v29) to store local Sieve scripts
- Add LocalSieveRepository with full CRUD and activate-script support
- Add isLocal param to SieveScriptsScreen and SieveScriptEditScreen; each
  screen shows a banner explaining whether scripts run on the server or device
- Add routes /accounts/:id/sieve/local and /accounts/:id/sieve/local/edit
- Split "Email filters" account menu entry into "Server email filters" and
  "Local email filters" (local is always available, server requires ManageSieve)
- Wire up localSieveRepositoryProvider in DI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 18:32:47 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 c649ee3414 fix(snooze): create Snoozed folder automatically on first use (#75)
Two bugs prevented snoozing in a brand-new IMAP/JMAP account:

- IMAP flush read `payload['mailboxPath']` which doesn't exist in snooze
  payloads (they use 'src'); selecting the wrong (null) mailbox caused the
  operation to fail.  Now uses `payload['mailboxPath'] ?? payload['src']`.

- JMAP flush had no path to create the Snoozed mailbox when the folder
  didn't already exist on the server.  Flush now calls `Mailbox/set` to
  create it whenever `dest == 'Snoozed'` (the sentinel used when the folder
  was absent at enqueue time), then substitutes the real JMAP mailbox ID.

Tests added for both code paths using a spy IMAP client and a mock JMAP
HTTP client respectively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:35:36 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 ef3fb72f4e fix(email): populate mimeTree for JMAP accounts in Show Mail Structure (#92)
The JMAP body-fetch path never requested or stored `bodyStructure`, so
`body.mimeTree` was always null for JMAP accounts — causing Show Mail
Structure to show nothing.

Fix: include `bodyStructure` in the JMAP `Email/get` request and convert
it to the same JSON format used by the IMAP path via the new
`_jmapBodyStructureToJson` helper.  The parsed tree is persisted in the
DB and returned from `getEmailBody`, so the cached round-trip also works.

Tests added:
- Unit: JMAP getEmailBody populates mimeTree from bodyStructure and
  survives the cache round-trip; null when bodyStructure is absent.
- Widget: Show Mail Structure dialog displays all MIME parts when
  mimeTree is present; snackbar appears when mimeTree is null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 14:23:43 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 8cdb00c0bd feat(email): show nested MIME structure in email detail screen (#88)
Adds a MimePart tree model, parses it from the IMAP BODYSTRUCTURE
when fetching the email body, caches it in a new mime_tree_json column
(schema v28), and exposes a 'Show Mail Structure' overflow menu item
that renders the indented tree (content-type, filename, size, encoding)
in an AlertDialog alongside the existing headers dialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 12:53:13 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 653ef92430 fix(email): resolve cid: inline images in multipart/related messages (#89)
Emails with multipart/related structure reference embedded images via
cid: URIs.  The WebView's CSP only allows data:/blob: sources, so those
images were never shown.  injectInlineImages() now replaces each cid:
reference with a data: URI using the decoded bytes from the MIME tree,
both for double-quoted and single-quoted src attributes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:02:22 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 69e358204d fix(undo): keep undo log entry and fix IMAP UID mismatch after sync (#81)
Two fixes for the UndoLog:

1. Don't delete the original undo log entry when undo is performed.
   The entry stays in the log alongside the new inverse action, so
   the user can retry the undo if it was silently reverted by an
   IMAP sync.

2. Fix IMAP UID mismatch: after an IMAP move is applied on the server
   the email gets a new UID in the destination folder. The undo service
   now looks up the email by its RFC 2822 Message-ID when the original
   row is gone, so the reverse-move pending change carries the correct
   UID and actually succeeds on the server.

Add findEmailByMessageId to EmailRepository interface and impl.
Add a regression test that simulates the UID change scenario.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 10:46:12 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 4e6c3d73fe chore: regenerate mocks for fetchRawRfc822
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 09:40:55 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 0ccf7b51fa fix: fetch original RFC822 from server in Show Raw Email (#84)
Instead of reconstructing the message from the local DB, fetch the
original bytes live from IMAP (BODY.PEEK[]) or JMAP (Email/get blobId
→ downloadBlob) so the view shows the true unmodified message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 09:27:12 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 99c3a1d808 feat(compose): sort address autocomplete by most recently used
Add ORDER BY receivedAt DESC to the searchAddresses query so the first
unique occurrence of each address comes from the newest email. Contacts
from recent conversations float to the top of the suggestions list.

Add a unit test verifying the sort order.

Fixes #83

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:39:38 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 2795cfe2cc fix(compose): prevent double hide() in RawAutocomplete async optionsBuilder
When focus leaves the To field while the address DB query is in flight,
the optionsBuilder Future completes AFTER RawAutocomplete has already
called hide() on the overlay. The completion triggers a second hide()
call, hitting the _zOrderIndex != null assertion in overlay.dart.

Fix: check focusNode.hasFocus after the await; return [] if focus left,
which prevents RawAutocomplete from calling show()/hide() on a closed
overlay.

Also fixes #81 partially: after undo(), push an inverse UndoAction so
the undo log retains a record and the user can re-apply the operation.

Fixes #79

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:22:20 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 a29d0e93b4 feat(sync): add View log button to sync error banner
When a sync failure banner appears in the email list screen, a new
'View log' button navigates directly to the account's sync log screen
so the user can see the full error details.

Also creates issue #75 for the first-snooze-in-new-account failure.

Closes #13

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 21:35:59 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 02b0fec0b6 feat(compose): autocomplete To/Cc from local address history
Adds RawAutocomplete<EmailAddress> to the To and Cc fields in the
compose screen. As the user types (minimum 2 chars), suggestions are
fetched from the local DB by searching from/to/cc columns of cached
emails. Selecting a suggestion appends it to any existing addresses
already in the field (comma-separated).

New repository method searchAddresses() returns deduplicated
EmailAddress objects matching the query string.

Closes #11

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 21:30:17 +02:00
Bot of Thomas Güttler 499774d1a6 feat: add 'Mark all as read' to mailbox overflow menu (U8) (#48) 2026-05-14 10:58:33 +02:00
Bot of Thomas Güttler 132b6aeb9a feat: recent searches history in SearchScreen (U3) (#47) 2026-05-14 10:51:28 +02:00
Bot of Thomas Güttler 546b06ba5a test(T3): add contract test suites for Account/Mailbox/Email repositories (#43) 2026-05-14 10:20:32 +02:00
Bot of Thomas Güttler 4f16587564 feat(P2): paginate email list — default 50 threads, Load more button (#42) 2026-05-14 10:09:05 +02:00
Bot of Thomas Güttler f0f81777b5 feat(P1): FTS5 virtual table for email search (replaces LIKE scan) (#41) 2026-05-14 10:01:42 +02:00
Bot of Thomas Güttler f9030dc1e5 perf(P4): add indexes on mailboxes and threads for observeMailboxes/observeThreads (#36) 2026-05-14 08:37:00 +02:00
Bot of Thomas Güttler 4f3a5434cc test(T4): extend migration tests to cover all schema versions up to v24 (#32) 2026-05-14 05:09:15 +02:00
Bot of Thomas Güttler dff2b5e2ca test(T1): add edge-case coverage for EmailRepositoryImpl (#30) 2026-05-14 04:43:11 +02:00
Bot of Thomas Güttler 0e291b509b feat(U2): sync local drafts with IMAP Drafts folder (#27) 2026-05-14 00:27:47 +02:00
Bot of Thomas Güttler a0c35c647a test(R6): backoff stress tests for AccountSyncManager (#24) 2026-05-13 23:37:40 +02:00
Bot of Thomas Güttler fc592c475f feat(R4): dismissible sync error banner in email list (#23) 2026-05-13 23:14:44 +02:00
Bot of Thomas Güttler beae8d8843 feat(R2): force full sync escape hatch in account edit screen (#22) 2026-05-13 22:57:36 +02:00
Bot of Thomas Güttler eddcc17c41 fix(R1): persist undo history across restarts (#20) 2026-05-13 22:35:08 +02:00
Thomas SharedInbox efdcab74d7 setup nix in CI, and reformat. 2026-05-12 21:55:06 +02:00
Thomas SharedInbox 568b63de55 fix: refine undo log and remove delete confirmations (Issue #12)
- Remove delete confirmation dialogs in list and detail screens.
- Style Undo SnackBar action button with red text color.
- Enhance Undo Log screen to show email subject, sender, and action metadata.
- Add Undo support for Snooze action.
- Fix restoreEmails to preserve snooze metadata.
- Add unit test for Undo snooze logic.
2026-05-11 07:33:22 +02:00
Thomas SharedInbox e80a7c7a0e test: ensure migrations from v1 to v22 work correctly
- Add test/unit/migration_test.dart to verify schema upgrades and data preservation.
- Fix onUpgrade logic for syncLogs table to be idempotent.
- Add fromJson/toJson/copyWith to Account and Mailbox models.
- Update unit tests for models to increase coverage.
- Adjust coverage gate exclusions for integration-heavy files.
2026-05-11 07:21:15 +02:00
Thomas SharedInbox b7ff02711b feat: implement snooze feature for IMAP and JMAP
- Add snoozedUntil and snoozedFromMailboxPath to Emails table.
- Implement snoozeEmail and wakeUpEmails in EmailRepository.
- Update IMAP and JMAP flush logic to handle snooze/unsnooze.
- Update sync logic to parse snz: keywords from server.
- Add SnoozePicker widget and integrate into UI.
- Add unit tests for Snooze logic.
2026-05-10 21:50:13 +02:00
Thomas SharedInbox 11e337ed6f feat: persist undo log to database and support selective undo across restarts (issue #10) 2026-05-10 17:38:43 +02:00
Thomas SharedInbox 310538460d feat: improve Undo Log to support undoing any action from history 2026-05-10 14:40:01 +02:00
Thomas SharedInbox c85dbbeff2 feat: implement global undo log with persistent history 2026-05-10 10:32:27 +02:00