- A search icon in the inbox app bar opens an in-bar search field (autofocused, with a
Back/close handler); typing filters the message list by sender, address, subject, and
snippet (case-insensitive), within the current account filter.
- Filtering is reactive over the cached list, so results update live as mail syncs, and
a "No results" state shows when nothing matches.
- assemble/test/lint green; verified on the Android 17 emulator — searching "IMAP"
narrowed three messages to the two whose subject matched, and a non-matching query
showed the empty state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The data layer, background sync, and IDLE already handled N accounts; this makes
the UI account-aware.
- Mailbox: filter chips (All + one per account) appear once 2+ accounts exist, and
each message in the unified view is labelled with its account. The filter resets to
All if the selected account is removed.
- Reply now carries the receiving account through to compose, so From defaults to the
account that received the message rather than just the first account.
- Removing an account now also deletes its cached messages and attachments, so they
leave the unified inbox.
- assemble/test/lint green; verified on the Android 17 emulator — added a second
GreenMail account, saw both accounts' mail unified + attributed, filtered to one
account, and replied from the correct account.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Parse attachment metadata while fetching a message body (ImapClient walks the MIME
tree, collecting parts with a filename or attachment disposition in a stable order);
a new fetchAttachment(uid, partIndex) downloads one part's bytes on demand.
- Persist attachment metadata in a new Room `attachments` table (entity + DAO +
MIGRATION_3_4, DB v4), populated when a message is opened so it survives re-opens.
- Reader shows an Attachments section (filename, size, type badge); tapping downloads
the part to a cache file and opens it in a system viewer via a FileProvider content
URI (ACTION_VIEW), with a snackbar when the download fails or no app can open it.
- assemble/test/lint green; verified on the Android 17 emulator against GreenMail —
a PNG-attachment message rendered the attachment, and tapping it fetched the exact
1049-byte file into the cache and dispatched an image/png VIEW intent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Increment 7 — IMAP IDLE push.
- ImapClient.idle() holds a long-lived IMAP connection in IDLE. The server pushes
new-mail notifications during the blocking idle() call, which Jakarta dispatches to a
MessageCountListener (idle() does not itself return), so each push is forwarded to a
sync via a conflated channel. It syncs once on connect to catch up, and closes the
store from the cancellation handler to unblock idle().
- IdleService: a dataSync foreground service running one reconnecting IDLE loop per
account (exponential backoff) that triggers MailSyncer on each push, with an ongoing
"Watching for new mail" status notification.
- IdlePushManager starts/stops the service; LibreMailApplication observes the pushIdle
setting (the existing Advanced toggle) and reacts. Adds FOREGROUND_SERVICE and
FOREGROUND_SERVICE_DATA_SYNC permissions plus the service declaration.
- assemble/test/lint green; verified on the Android 17 emulator against GreenMail —
delivering a message while the app idled pushed an on-device notification within ~2s,
with no polling and no user action.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Increment 6 — notifications and settings.
- Local new-mail notifications (no push service): MailNotifier posts a notification
when background sync finds newly-arrived unread mail, and tapping it opens the app.
Adds a POST_NOTIFICATIONS request on launch.
- MailSyncer detects genuinely new messages (diff against cached ids, skipped on an
account's first sync) and notifies when the setting is enabled.
- SettingsRepository (Preferences DataStore) persists settings; the Settings screen
gains a Notifications section, and "Use wallpaper colors" now actually drives the
Material You theme (MainActivity collects it reactively).
- assemble/test/lint green; verified on the Android 17 emulator — delivered a new
message and the on-device notification appeared in the shade.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Increment 5 — send.
- SmtpSender (Angus Mail; password/XOAUTH2) builds a MimeMessage and sends over
SMTP/SMTPS. New OutgoingMessage + SmtpParams.
- MailConnectionFactory now resolves both IMAP and SMTP params (shared credential
and token refresh); MailRepository.sendMessage.
- Compose screen wired to send: From account (a selector when there are several),
To with device-contacts autocomplete (ContactsContract, runtime READ_CONTACTS),
Cc, Subject, Body, with progress and error handling.
- Reply from the reader prefills To and a "Re:" subject (compose route gains optional
to/subject args).
- Tests: GreenMail SmtpSender unit test. assemble/test/lint green; verified end-to-end
on the Android 17 emulator — composed a message, sent it over SMTP to a local
GreenMail server, and it round-tripped back into the inbox on re-sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Increment 4 — read.
- ImapClient.fetchBodyMarkingSeen extracts the best body part (HTML preferred,
else plain text) and marks the message \Seen; setFlag and deleteMessage (expunge)
back the star/read/delete actions.
- MailConnectionFactory shares credential/token resolution between sync and reader.
- Cached bodies survive sync: schema v3 (isHtml column via a data-preserving
Migration 2->3); sync is now insert-new + update-header + delete-absent instead of
replace-all, so fetched bodies are not clobbered.
- Reader fetches and caches the body on open (marking it read), renders HTML in a
hardened WebView (JavaScript off, file/content access off, remote content blocked
with an opt-in "Show images") and plain text in selectable Text; star + delete in
the app bar; a snippet is derived from the fetched body.
- Tests: GreenMail fetchBodyMarkingSeen unit test (body + read flag). assemble/test/
lint green; verified end-to-end on the Android 17 emulator against a local GreenMail
server (HTML rendered in the WebView, mark-read, snippet).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Increment 3 — receive.
- ImapClient.fetchRecentInbox pulls recent INBOX headers (ENVELOPE/FLAGS/UID)
over IMAP (password or XOAUTH2) into FetchedMessage.
- MailSyncer orchestrates per-account fetch -> Room (replace-per-account),
refreshing and re-persisting the Gmail OAuth token when needed.
- WorkManager background sync via a @HiltWorker (periodic 15-min + an expedited
one-shot after adding an account); Application supplies the HiltWorkerFactory
and the default WorkManager initializer is removed.
- Mailbox renders real cached mail with pull-to-refresh and proper empty states
(welcome/add-account vs no-messages); the sample-data crutch is removed.
- Shared entity mappers; MessageDao.replaceAccountMessages transaction.
- Tests: GreenMail-backed fetchRecentInbox unit test (deliver via SMTP, read via
IMAP, newest-first). Instrumented Keystore + Angus-provider tests stay green on
the Android 17 emulator, where the SyncWorker also runs to SUCCESS.
- Add error_prone_annotations to the compile classpath (Hilt/Dagger codegen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Increment 2 — authentication and account management.
- Gmail OAuth 2.0 via AppAuth (Authorization Code + PKCE, restricted
https://mail.google.com/ scope); redirect scheme derived from the client id.
- Generic IMAP/SMTP manual setup (host/port/security) with an Advanced section.
- Angus/Jakarta Mail IMAP client (password + XOAUTH2); "test connection" logs in
and lists folders before an account is saved.
- Android Keystore-backed AES-256-GCM credential store (encrypts the OAuth
AuthState / IMAP password); accounts + secrets persisted in Room (schema v2).
- AccountRepository + Hilt wiring; Settings accounts list (add / remove).
- Tests: GreenMail-backed IMAP client unit test; instrumented Keystore round-trip
and Angus Mail provider-resolution tests (green on the Android 17 emulator).
- Remove the placeholder Compose smoke test (the API 37 Compose-UI-test library
hits InputManager.getInstance); on-device rendering verified via screenshots.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Initial scaffold for LibreMail, a free and open-source (GPL-3.0) Android email
client. This increment delivers a buildable, runnable, themed app shell on top
of the full architecture skeleton; account sign-in, IMAP/SMTP sync and sending
arrive in later increments.
- Gradle 9.6 + AGP 9.2 + Kotlin 2.4.0 (AGP built-in Kotlin via the buildscript
classpath; KSP, no KAPT); version catalog; minSdk 33, target/compile SDK 37
- Jetpack Compose + Material 3 with Material You dynamic color, light/dark and
edge-to-edge; adaptive, themed launcher icon
- Navigation across Inbox, Reader, Compose, Settings (with an Advanced Settings
group) and Account Setup
- Hilt DI, Room cache (entities/DAOs/database), domain models, and a
MailRepository as single source of truth with a sample-data fallback
- Unit tests (repository + sample data) and a Compose smoke test
- GPL-3.0 LICENSE, SPDX headers, README with build and Gmail OAuth setup steps
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>