Add an opt-in (off by default) toggle to include app data in system
Android Backup / Auto Backup, gated so only re-creatable user
preferences are ever backed up.
- Flip allowBackup to true and add LibreMailBackupAgent, which enforces
the runtime opt-in: onFullBackup runs only when the user enables
"Include settings in Android Backup" (default off), so no data leaves
the device otherwise. allowBackup is a manifest flag and can't be
toggled at runtime, hence the agent.
- Rewrite data_extraction_rules.xml (API 31+) and add backup_rules.xml
(API 29-30) as strict allowlists that back up ONLY the
libremail_settings DataStore. The Keystore-sealed cache passphrase
(libremail_dbkey) and the encrypted credentials + mail-cache database
(libremail.db) are excluded by omission; the cache re-downloads on
next sync.
- Add includeInBackup preference + setter (nudges BackupManager on
change) and a "Backup" settings section with F-Droid-honest copy
(off by default, uses Google infrastructure).
- BackupPolicy is the single source of truth for eligible/excluded
paths; unit tests cover the toggle default and assert the shipped XML
resources include only settings and never the secrets/DB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gmail authenticates via app password + preconfigured IMAP/SMTP (decision
in #9), never OAuth, so the Gmail-OAuth implementation was unreachable
dead code. Remove it while keeping Outlook's AppAuth OAuth path intact.
- Delete auth/GmailAuthManager.kt (shared OAuthResult/FreshToken kept).
- Drop AuthType.OAUTH_GMAIL and its exhaustive `when` branch, the
gmailAuthManager injection, and the SCOPE_GMAIL constant in
MailConnectionFactory.
- Remove GMAIL_OAUTH_* BuildConfig fields, gmailOAuthClientId, and the
gmailRedirectScheme val from app/build.gradle.kts.
- Repoint the AppAuth manifestPlaceholders["appAuthRedirectScheme"] to
the Outlook scheme (org.libremail.outlook). AppAuth's bundled manifest
now registers that scheme on RedirectUriReceiverActivity, so the app
manifest's now-redundant Outlook intent-filter is removed; the
AppCompat theme override (crash fix) is preserved. Verified the merged
manifest.
- Drop GMAIL_OAUTH_CLIENT_ID from secrets.properties.example.
authType persists as the enum name in a TEXT column, so removing a
constant needs no Room schema change or migration.
Closes#39
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a long-press contextual action bar to the mailbox: Archive, Delete,
Spam, Move, Select All, and (single-selection only) Reply, Reply All, and
Forward. Reply All/Spam/Delete are confirmed first; deleting from Trash or
Spam warns that it is permanent.
- Delete moves to Trash and Spam moves to the Spam folder; only deletes
already in Trash/Spam do a permanent IMAP expunge. Archive/Spam/Move
resolve the destination per account so the unified inbox works.
- Reply/Reply All/Forward force a fresh server fetch of the original
(recipients + body via BODY.PEEK), build a quoted draft, and open compose;
a spinner shows during the fetch and they error via snackbar if offline.
Add a top-level "Message downloading" setting (Always fetch all / Fetch all
on Wi-Fi / Always on-demand; default Always) that aggressively pre-caches
full bodies and all attachment bytes during sync (outside the sync lock,
without marking mail read), into a persistent per-part attachment cache so
messages and attachments open instantly and offline.
Show an "available offline" mark on cached list rows and a per-attachment
"downloaded" check in the reader.
Extract a Syncer interface (MailSyncer implements it) so the mailbox can be
driven end-to-end in tests.
Tests: 66 unit + 27 instrumented, all passing on the API 37 emulator.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Sign in with Microsoft" crashed on the redirect back from the browser and
never completed a login. Verified end-to-end on a real Outlook account after
three fixes, in flow order:
- Redirect crash (the reported symptom): AppAuth's RedirectUriReceiverActivity
extends AppCompatActivity, so it needs a Theme.AppCompat theme. This app is
pure Compose (framework Theme.Material), and AppAuth declares that activity
with no theme of its own, so it inherited the Material app theme and threw
"You need to use a Theme.AppCompat theme" the instant Android launched it to
deliver the redirect. Give it the translucent AppCompat theme AppAuth itself
applies to AuthorizationManagementActivity. (Not an R8 issue.)
- Token exchange rejected with AADSTS70011 ("must include a 'scope' input
parameter"): one consent spans two Microsoft resources (Graph for send,
Exchange Online for IMAP), so Microsoft mints one token per resource and the
code-to-token exchange must name a single resource. AppAuth's
createTokenExchangeRequest() sends no scope; build the request explicitly
with a single-resource scope.
- "Invalid ID Token" / nonce mismatch: the hand-built exchange request must
replicate every field createTokenExchangeRequest() sets, including the nonce
AppAuth validates the id_token against (and the PKCE code verifier).
Also harden the account-setup screen: guard the previously unguarded
createAuthIntent() launch (AppAuth throws ActivityNotFoundException when no
browser is available) so it surfaces an error instead of crashing, dispose the
AuthorizationService that createAuthIntent() leaked, and log sign-in failures
via Log.d (stripped from release builds by the existing ProGuard rule).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses findings from a full-repo review across the mail, sync, persistence,
auth, and UI layers.
Send / outbox:
- Stop the Graph->SMTP fallback from duplicating a message when a Graph send may
already have been accepted; leave indeterminate sends queued for the user.
- Parse RFC822 display-name recipients on the Graph path.
- Preserve attachment order (staged in indexed subdirectories).
Data safety (schema v7):
- Disable cloud/device backup of the Keystore-encrypted credential DB.
- Add the missing v1->v2 migration and drop the destructive migration fallback.
- Normalize the messages.isHtml default and add an attachments->messages
ON DELETE CASCADE foreign key (no more orphaned attachment rows).
Concurrency:
- Serialize syncing and per-account OAuth token refresh; cache tokens by expiry.
- Synchronize Android Keystore key creation.
- Make a sync's persist+notify non-cancellable so an IDLE renewal can't drop it.
Notifications / UI:
- Per-message notifications under a group + summary instead of one overwriting id.
- Wire the "load remote images" and "allow STARTTLS" settings.
- Harden the reader WebView (scheme allowlist + user gesture; no reload on
recomposition); refresh headers without reverting optimistic read/star flags.
- Persist draft attachments; keep server-search hits out of the inbox; encode
the reader navigation argument.
Build / test:
- Export Room schemas; support a real release keystore; add unit/db tests.
- Remove dead Gmail account-setup code left after the sign-in removal.
Verified: debug + release (R8) + androidTest compile; unit tests pass;
MIGRATION_6_7 matches the generated v7 schema.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Outlook signs in through the Microsoft identity platform via AppAuth (Authorization Code +
PKCE, no secret). One consent requests the outlook.office.com IMAP and SMTP scopes; because
they share a single resource, the resulting access token authenticates both IMAP receive and
SMTP send over XOAUTH2 — reusing the existing ImapClient and SmtpSender, with no Graph call or
second token. The "common" tenant covers personal and work/school accounts.
- OutlookAuthManager (mirrors GmailAuthManager) + AuthType.OAUTH_OUTLOOK + Account.outlook()
with the unified outlook.office365.com / smtp.office365.com endpoints.
- MailConnectionFactory refreshes either OAuth provider's token; XOAUTH2 now applies to any
non-password account. AccountRepository.addOutlookAccount verifies via IMAP, then persists.
- "Sign in with Microsoft" on the account-setup screen; the manifest registers the
org.libremail.outlook:// redirect. The client id ships in the build, overridable via
secrets.properties (OUTLOOK_OAUTH_CLIENT_ID); README documents the Azure app registration.
- Verified: assemble/lint/test green; on the emulator the button launches AppAuth and
Microsoft renders its live sign-in page (client id, redirect, and scopes all accepted).
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 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>
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>