Commit Graph
13 Commits
Author SHA1 Message Date
JMR-dev df5b99aff9 Merge remote-tracking branch 'origin/main' into feat-backup-optin 2026-07-01 09:49:24 -05:00
JMR-devandClaude Opus 4.8 ffbb882227 feat(backup): opt-in Android Backup for settings only
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>
2026-06-30 23:56:18 -05:00
JMR-devandClaude Opus 4.8 657af48052 refactor(auth): remove dead Gmail OAuth code path
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>
2026-06-30 23:44:17 -05:00
JMR-devandClaude Opus 4.8 96111b40df feat: long-press multi-select, aggressive prefetch, and download indicators
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>
2026-06-30 17:20:22 -05:00
JMR-devandClaude Opus 4.8 317ce24054 Fix Outlook sign-in crash and complete the OAuth login flow
"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>
2026-06-29 23:15:14 -05:00
JMR-devandClaude Opus 4.8 038cfd6153 Fix correctness, security, and concurrency issues from code review
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>
2026-06-29 18:15:53 -05:00
JMR-devandClaude Opus 4.8 1f63773faa Add Outlook / Microsoft account support (OAuth)
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>
2026-06-27 18:20:41 -05:00
JMR-devandClaude Opus 4.8 6d3360fdb9 Add attachments: download and open from the reader
- 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>
2026-06-26 23:25:13 -05:00
JMR-devandClaude Opus 4.8 92e5005474 Add instant push via a foreground IMAP IDLE service
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>
2026-06-26 22:11:33 -05:00
JMR-devandClaude Opus 4.8 40b0d9b3ad Add new-mail notifications and persisted settings
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>
2026-06-26 20:21:49 -05:00
JMR-devandClaude Opus 4.8 22ddc08a24 Add composing and sending: SMTP send + contacts + reply
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>
2026-06-26 19:58:18 -05:00
JMR-devandClaude Opus 4.8 5386ae76d3 Add IMAP receive: background sync of inbox into Room
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>
2026-06-26 18:25:54 -05:00
JMR-devandClaude Opus 4.8 c931c73745 Scaffold LibreMail: Material You email app foundation
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>
2026-06-26 00:17:59 -05:00