Previously claude was launched with -p (print mode) which produces no
visible TUI. Attaching to the session with `tmux attach -t issue-NNN`
showed a blank terminal. Removing -p makes Claude run its interactive
TUI inside the tmux pane, so the session is fully watchable.
Add scripts/test_agent_loop.py covering _start_agent command
construction and state file round-trips.
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>
The CI self-hosted runner can leave a stalwart process alive from a prior
run that was interrupted externally, causing the next run to fail with
"port already in use". Kill any existing stalwart before starting a new one.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace `import nixpkgs { inherit system; }` with the idiomatic flake
pattern `nixpkgs.legacyPackages.\${system}`, which avoids the evaluation
warning: 'system' has been renamed to/replaced by 'stdenv.hostPlatform.system'.
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>
The UI rename in #108 changed the welcome screen text from
"Welcome to SharedInbox" to "Welcome to sharedinbox.de" but the
E2E test still searched for the old string, causing a pumpUntil timeout.
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>
- Add Dagger to flake.nix
- Create Dagger module in ci/ with Flutter build/test logic
- Update .forgejo/workflows/ci.yml to use Dagger
- Move Android emulator tests to separate disabled workflow
- Add .daggerignore to exclude host junk
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>
The standalone "Check mocks are up to date" step ran build_runner AOT
compilation separately, then task check ran it again (check-mocks is
already a dep of check). The double invocation caused the build_runner
AOT compile to receive SIGTERM on the CI runner in run 4027578.
task check already verifies mocks via its check-mocks dep, so the
standalone step is redundant.
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>
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>
The deploy steps in build-linux and deploy-playstore already use
continue-on-error: true when SSH secrets may be absent, but
publish-website did not — causing a hard failure when SSH_USER is unset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The builds page at /builds/ was empty because generate-build-history
only ran inside deploy-playstore; if that job failed early (e.g. Play
Store secrets not configured) the website was never updated, and the
build-linux job never triggered a website update at all.
Changes:
- generate_build_history.py: extend to cover Linux tarballs in addition
to Android APKs, capped at MAX_BUILDS_PER_PLATFORM (30) each
- Taskfile: add website-publish task (generate-build-history +
website-deploy), exclude *.tar.gz from rsync, update descriptions
- .forgejo/workflows/ci.yml: add publish-website job that waits for
both build-linux and deploy-playstore (using always() so it runs
even when deploy-playstore fails), then removes the duplicate
generate/deploy steps from deploy-playstore
- .github/workflows/ci.yml: add deploy job that deploys Linux build,
generates build history, builds Hugo site, and rsyncs to server
- .gitignore: ignore website/content/builds/_index.md (generated),
Python __pycache__, and widget test failure screenshots
- stalwart-dev/integration_ui_test.sh: use ${USER:-$(id -un)} for
robustness in environments where USER is unset
- scripts/test_generate_build_history.py: unit tests for parse_builds
and render_entries covering both platforms
Generated content (builds/_index.md and per-day pages) is not tracked
in git; it is produced at CI time and rsynced to the server.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Without `< /dev/null`, claude detects the tmux PTY as stdin and blocks
waiting for user input that never arrives (the PTY never sends EOF).
The 3-second stdin-timeout only fires for pipe stdin, not TTY stdin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Replace bare subprocess.Popen with `tmux new-session -d` so each agent
runs in a detached tmux session that inherits the tmux server's environment
(including ANTHROPIC_API_KEY / keychain access, which cron's minimal env
lacks — the root cause of intermittent empty log files).
- Track agents by tmux session name instead of PID; age is derived from the
state-file `started_at` timestamp rather than /proc/<pid>/stat.
- `_kill_agent` terminates via `tmux kill-session`; backward compat preserved
for old state files that stored a `pid`.
- Operators can now `tmux attach -t issue-<N>` to watch live output, or
`claude --resume issue-<N>` to continue the conversation afterward.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
Cron runs with a minimal environment that doesn't include ~/.nix-profile/bin,
causing every invocation to crash with FileNotFoundError on 'tea'.
Closes#93
Polls Codeberg CI and State/Ready issues every 10 minutes, launching
Claude Code agents for CI fixes and issue work, with PID-based liveness
tracking and automatic timeout after 1 hour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
After deleting all selected emails from a search view, re-run the
search query. If no emails match any more, clear the search bar so
the user returns to the normal thread list view instead of seeing
a stale list of already-deleted messages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add check-mocks task that re-runs build_runner and fails if any
*.mocks.dart file differs from what is committed. Wired into
check-fast (pre-commit) and added as an early CI step so stale
mocks are caught before the full test suite runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>