Commit Graph
157 Commits
Author SHA1 Message Date
Thomas SharedInboxandClaude Sonnet 4.6 4f9a4e666f fix: make ChangeLog screen testable with DefaultAssetBundle (#198)
Switch ChangeLogScreen from rootBundle to DefaultAssetBundle.of(context)
so the asset source can be overridden in widget tests. Add
test/widget/changelog_screen_test.dart with a happy-path test (asset loads
and content is rendered) and an error-path test (missing asset shows the
error message).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:51:41 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 ff488746a0 fix: show git hash as clickable link above stacktrace on crash screen (#201)
Previous PRs (#150, #179) added partial implementations that left duplicate
code via a rebase conflict: plain (non-linked) text above the stacktrace and a
clickable link section below it. This consolidates both into a single clickable
link above the stacktrace.

Also makes `gitHash` an injectable constructor parameter so tests can exercise
the link without needing a release build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 10:55:38 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 a569177637 fix: treat MissingPluginException from secure storage as permanent sync error (#200)
When flutter_secure_storage's platform channel is unavailable (e.g. on
certain Android devices), getPassword() throws MissingPluginException.
Previously this was not recognised as a permanent error, so the IMAP and
JMAP sync loops retried indefinitely with exponential back-off, filling
the sync log with repeated failures (as shown in the screenshot).

Treat MissingPluginException as a permanent error in both _AccountSync
and _JmapAccountSync so the loop stops immediately instead of retrying.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 08:46:29 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 7ece6f09e5 feat: make sharedinbox.de heading a link and add git commit row to about table (#199)
- Wrap the '## sharedinbox.de' heading in a markdown hyperlink to https://sharedinbox.de
- Add a dedicated 'Git Commit' table row with a clickable link to the commit on Codeberg when GIT_HASH is set
- Update clipboard test to assert the heading link is present in copied markdown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 08:04:54 +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
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 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
GuettliBot2 90ab0a6905 ci: rename integration tests to test-backend and migrate to Dagger 2026-05-17 08:47:15 +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
Thomas SharedInboxandClaude Sonnet 4.6 e327b42312 fix: close Raw Email dialog automatically after download (#114)
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>
2026-05-16 13:32:22 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 651110b389 fix: do not show snackbar for stale undo actions on startup (#113)
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>
2026-05-16 09:23:49 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 aeb4c5ab41 feat: improve About screen with labeled versions, dark mode, account counts, and bottom buttons (#111)
- Rename "Version" to "App Version"; rename "OS Version" to platform-prefixed label (e.g. "Android Version")
- Link app version to its Codeberg git commit (via GIT_HASH dart-define)
- Add "Dark Mode" yes/no row
- Add IMAP Accounts and JMAP Accounts rows
- Move copy/create-issue actions from AppBar icons to labeled buttons below the table
- Pass GIT_HASH dart-define in Taskfile APK/AAB build commands

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 08:03:13 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 dc8c1cb08d feat: introduce Local Filters / Remote Filters terminology (#109)
- 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>
2026-05-16 01:49:11 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 67880929bc feat: rename SharedInbox to sharedinbox.de in UI and website (#108)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:33:13 +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 7fa19dd39a feat(about): add About page with device/app info and issue reporting (#106)
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>
2026-05-15 23:50:55 +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 SharedInbox 1fd37cc966 feat(account-menu): move force full sync button from edit screen to account menu (#99)
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.
2026-05-15 21:29:43 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 a38691a760 fix(html-mail): force light color-scheme to prevent black-on-black in dark mode (#98)
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>
2026-05-15 21:01:57 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 1fa4d4911a fix(raw-email): save to Downloads and show size in raw email view (#97)
- 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>
2026-05-15 20:39:39 +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 a44a2e4834 fix(ux): pop back after deleting the last search result from detail view (#85)
- Filter deleted emails locally in _batchDelete so the pop-back fires
  immediately instead of waiting for the IMAP server to catch up.
- Add _openSearchResultAndRefresh / _refreshSearchAndPopIfEmpty so that
  returning from EmailDetailScreen after deleting the last match also
  pops EmailListScreen back to the caller.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 18:09:35 +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 99df6f5fd0 feat(accounts): share account settings via QR code / JSON export (#66)
Add Export account screen (QR code + copy-to-clipboard) and Import
account screen (paste JSON code) so users can transfer IMAP/JMAP
account configuration to another device without re-entering every field.

- Account list popup: "Export account" opens a QR code with a password
  warning and a copy-code button.
- Add Account screen: "Import account" button opens the import flow
  where pasting the exported JSON pre-fills the account and one tap
  saves it with a fresh generated ID.
- New routes: /accounts/:id/export and /accounts/import.
- Widget tests cover export display, import parsing, validation,
  and the happy-path save-and-navigate flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:53:36 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 122358c9a2 fix(ux): navigate back after deleting all search results (#85)
When the user searches in a mailbox, selects all results, and deletes
them, re-evaluate the search. If no results remain and there is a
previous screen in the navigation stack, pop back to it instead of
clearing the search and showing the regular inbox.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:36:05 +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 f96f9216cd feat: replace flutter_html with SecureEmailWebView (#21)
Swap the flutter_html renderer for a webview_flutter-based widget that
enforces strict security by default: scripts blocked via CSP
(script-src 'none'), remote images opt-in, and every link click routed
through a confirmation dialog that bolds the registered domain for
phishing detection.  Links open in the system browser via url_launcher.

On Linux (no webview_flutter platform support) the widget falls back to
plain text extracted via the existing htmlToPlain() utility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 08:18:42 +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
Thomas SharedInboxandClaude Sonnet 4.6 26a9a5e6f3 feat(crash): add app version and device info to crash reports
Issue reports now include:
- App version (from package_info_plus)
- OS name and version (non-personal, from dart:io Platform)
- Error and stack trace wrapped in triple-backtick code blocks
  so Codeberg renders them as preformatted text

Closes #59

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:52:40 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 ca28bd01af fix(imap): fetch full message for attachment download to fix base64 decoding
A partial BODY.PEEK[n] fetch omits the section's MIME headers, so
enough_mail's decodeContentBinary() has no Content-Transfer-Encoding
and returns the raw base64 string instead of the decoded bytes.
Fetching BODY.PEEK[] gives enough_mail the full MIME structure and
getPart(fetchPartId) correctly decodes the attachment.

Also adds an integration test that creates an email with a binary
attachment, syncs it, and asserts the downloaded bytes match the
original — this test failed before the fix.

Closes #70

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:44:09 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 f6f10700f8 feat: select all, human-readable build version, release gated on CI
- Add Select All button to AppBar during selection mode (#15)
- Replace Unix timestamp build number with yymmdd-hhmm format (#63)
- Gate release.yml on CI workflow success via workflow_run event
- Update golden for email_list_selection to reflect new Select All button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 14:23:54 +02:00
Bot of Thomas Güttler 12639d1e24 feat: onboarding walkthrough for first-time users (U7) (#55) 2026-05-14 11:57:08 +02:00
Bot of Thomas Güttler dd66c3834d test: golden tests for key EmailListScreen states (T5) (#52) 2026-05-14 11:33:45 +02:00