## What PR #356 (Renovate) was blocked with "Artifact file update failure" because `ci/go.sum` was out of sync with `ci/go.mod`. **Root cause**: The `require` section listed otel log packages at v0.17.0 while `replace` directives pinned them to v0.19.0, but `go.sum` only had hashes for v0.16.0. Renovate couldn't auto-update go.sum because the Dagger module's `internal/dagger` generated package isn't in version control, so standard `go mod tidy` couldn't resolve the full dependency graph. ## Changes - Bumps `go.opentelemetry.io/otel` + `otel/trace` + `otel/sdk` v1.43.0 → v1.44.0 (implementing PR #356's intent) - Updates all related otel exporters and sub-packages to v1.44.0 / v0.20.0 - Aligns `replace` directives from v0.19.0 → v0.20.0 (consistent with require section) - Also picks up `grpc` v1.79.3→v1.80.0 and `proto/otlp` v1.9.0→v1.10.0 (from `go mod tidy`) - Adds all missing `h1:` and `/go.mod` hashes to `go.sum` ## Verification - `go mod verify` passes - Hashes fetched directly via `go mod download -json` from the official Go module proxy Closes #359 Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de> Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/363
SharedInbox 
IMAP/SMTP email client written in Flutter.
Targets Android, iOS, and Desktop (Linux done; macOS, Windows, Android, iOS scaffolded). Supports multiple accounts — each synced independently via IMAP IDLE.
Design philosophy: offline-first
IMAP/SMTP server
↓
AccountSyncManager ←→ Drift (SQLite, local DB)
(IMAP IDLE per account) ↓
UI (reads only from DB)
The UI never touches the network. The sync engine runs in the background and writes to a local Drift database. Screens observe reactive streams from that DB.
Platform support
| Platform | Status |
|---|---|
| Linux desktop | Working (task run) |
| Android | APK builds (task build-android) |
| macOS desktop | Scaffolded |
| Windows desktop | Scaffolded |
| iOS | Scaffolded |
Key packages
| Package | Role |
|---|---|
enough_mail |
IMAP / SMTP / MIME |
drift |
Local SQLite ORM (offline-first store) |
flutter_riverpod |
State management / DI |
go_router |
Navigation |
flutter_secure_storage |
Password storage |
For users
Run the app, tap +, and enter your IMAP/SMTP server details. The app syncs your INBOX in the background using IMAP IDLE and works offline — the network is only needed during initial sync and when sending mail.
For developers
Prerequisites
Nix with flakes enabled and direnv.
# One-time: allow direnv to load the Nix dev shell
direnv allow
# One-time: install the pinned Flutter version (fvm is provided by Nix)
fvm install
direnv loads the Nix flake automatically — it provides go-task, fvm, Android SDK, Stalwart, and Linux build tools. Flutter itself is managed by FVM (pinned in .fvmrc) rather than Nix, which avoids glibc compatibility issues on non-NixOS hosts. task check also runs fvm install automatically if Flutter is missing.
First-time setup
# Generate the Drift database layer (required before first build)
task codegen
# Verify everything compiles and tests pass
task check
Daily workflow
task analyze # flutter analyze (uses analysis_options.yaml)
task test # pure-Dart unit tests + coverage gate (≥85%)
task test-widget # widget tests — headless, no device needed
task test-flutter # full Flutter test suite (unit + widget + integration)
task integration # IMAP/SMTP integration tests via local Stalwart server
task build-linux # flutter build linux --debug (compile check)
task run # flutter run -d linux
task analyze-fix # dart fix --apply
task check runs analyze + test + test-widget + build-linux + integration in parallel — use it before every commit.
Running the app on desktop in mobile screen resolution
Start the app on the Linux desktop target:
task run # or: flutter run -d linux
After the window opens, resize it to a phone-like size. Typical reference dimensions:
| Device profile | Width × Height |
|---|---|
| Compact phone (e.g. Pixel 6a) | 360 × 800 |
| Large phone (e.g. iPhone 14 Pro) | 393 × 852 |
| Tall phone (e.g. Samsung S24) | 360 × 780 |
Drag the window border to those dimensions, or use your window manager's "set window size" feature. The Flutter layout engine responds to the window size exactly as it would on a real device — breakpoints, overflow, and scrolling behave identically. Hot-reload (r in the terminal) preserves the window size between reloads.
Building and installing an Android APK
Build a release APK with:
task build-android # or: flutter build apk --release
The signed APK is written to:
build/app/outputs/flutter-apk/app-release.apk
Install via ADB (USB cable or Wi-Fi ADB, device must have "Install from unknown sources" enabled):
adb install build/app/outputs/flutter-apk/app-release.apk
Install by side-loading (no cable):
- Copy
app-release.apkto the device (e.g. via USB file transfer, cloud storage, oradb push). - Open a file manager on the device, tap the
.apkfile, and confirm the install prompt.
Tip — split APKs for smaller size:
flutter build apk --split-per-abiproduces three smaller APKs (one per CPU architecture). Install the one matching the device:app-arm64-v8a-release.apkcovers almost all modern Android phones.
Widget tests
test/widget/ contains Flutter widget tests for every screen. They run headlessly — no display server, no device, no database, no network. Each test pumps the screen into a virtual render canvas and uses in-memory fakes for the Riverpod repository providers.
Run them locally:
task test-widget # or: flutter test test/widget/
They also run in CI on every push (see the Widget tests step in .forgejo/workflows/ci.yml).
After changing the DB schema
Edit lib/data/db/database.dart, then:
task codegen # regenerates lib/data/db/database.g.dart
database.g.dart is git-ignored; every developer must regenerate it after cloning or pulling schema changes.
Integration tests
task integration
Starts a local Stalwart mail server on random ports, runs the tests in test/integration/, then stops it. No manual setup needed — Stalwart is provided by the Nix flake.
Adding a screen
- Create
lib/ui/screens/my_screen.dart— extendConsumerWidget. - Add a
GoRouteinlib/ui/router.dart. - Read from Riverpod providers in
lib/di.dart; never call the network directly from UI.
Project layout
lib/
core/
models/ — plain Dart data classes (Account, Email, Mailbox, …)
repositories/ — abstract interfaces
sync/ — AccountSyncManager (IMAP IDLE + backoff)
utils/ — htmlToPlain, fmtSize (pure functions, unit-tested)
data/
db/ — Drift schema + generated code
imap/ — connectImap / connectSmtp helpers
repositories/ — concrete implementations
ui/
screens/ — one file per screen
router.dart — go_router route tree
di.dart — Riverpod providers
main.dart — entry point
packages/
enough_mail/ — vendored IMAP/SMTP library (editable)
stalwart-dev/ — local mail server config + start/test scripts
test/
unit/ — pure-Dart unit tests (no device)
widget/ — Flutter widget tests (headless, no device)
integration/ — IMAP/SMTP tests against local Stalwart
Working features
- Multiple accounts — add any number of IMAP/SMTP accounts; each syncs independently
- IMAP IDLE — background sync with push-like latency; exponential backoff (5 s → 5 min) on error
- Mailbox list — shows all folders with unread / total counts
- Email list — sender, subject, date; bold for unread; manual sync button
- Email detail — renders plain text; falls back to HTML→plain conversion; marks as read on open; shows attachment names and sizes
- Reply / Reply all — pre-fills To, Subject (
Re:), Cc from original - Compose — To, Cc, Subject, Body fields; sends via SMTP
- Flag / unflag — star button in detail view; amber star indicator in list; synced to server
- Move to folder — bottom-sheet folder picker; moves on server via IMAP MOVE
- Attachment indicators — paperclip icon in email list; filename + size in detail
- Delete email — removes from server (IMAP expunge) and local DB
- Settings — list and remove accounts
- Search — IMAP server-side search (subject + body); results shown inline, no navigation change
- Offline-first — all reads come from local Drift/SQLite DB; network only for sync and send