Commit Graph
783 Commits
Author SHA1 Message Date
JMR-devandClaude Opus 4.8 fe0593dba1 ci: pin ANDROID_AVD_HOME so the API 37 preview emulator finds its AVD
The preview emulator failed every boot with "Unknown AVD name [api37]" and
exited immediately (no device -> the bounded wait timed out). avdmanager had
created the AVD under $ANDROID_SDK_HOME/.android/avd while the emulator searched
$ANDROID_SDK_HOME/avd and $HOME/.android/avd. Pin ANDROID_AVD_HOME to one path
both tools use, carry it to the boot step via $GITHUB_ENV, and list AVDs after
creation to verify.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:59:35 -05:00
JMR-devandClaude Opus 4.8 b8086f89df ci: make API 37 preview emulator boot fail-fast and diagnosable
The custom-provisioned preview job hung in 'Boot emulator and run E2E' until
the 35-min cap: adb wait-for-device had no timeout and the emulator's own
output was never captured, so a failed boot was both invisible and unbounded.
Bound the wait with a single 300s timeout covering device registration + full
boot, retry once, capture the emulator log, and dump it (plus logcat) on
failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:45:41 -05:00
Jason Ross e175ac13be Merge pull request #3 from JMR-dev/feat-folder-view
Add IMAP folder navigation drawer + per-API E2E CI matrix
2026-06-30 12:36:30 -05:00
JMR-devandClaude Opus 4.8 957da0c46f ci: run debug-build and unit-tests on x86_64
Linux-arm64 runners can't set up the SDK here: android-actions/setup-android's
sdkmanager fails (exit 1) on the android-37.0 preview platform, and the emulator
package has no arm64-Linux build. These are pure build/JVM-unit jobs whose
results are host-arch-independent, so x86_64 loses no device coverage (real
arm64 ABI coverage would require arm64 emulators, i.e. macOS hosts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:28:55 -05:00
JMR-devandClaude Opus 4.8 13a32df873 Add IMAP folder navigation drawer + per-API E2E CI matrix
Folder view: a left navigation drawer lists each account's IMAP folders;
tapping one browses and caches that folder's mail. IMAP UIDs are unique only
within a folder, so message identity, the fetch/read/flag/delete paths, sync,
and the Room cache all became folder-aware (id = "accountId:folder:uid"; new
`folder` column; schema v7->v8). Standard folders (Inbox/Sent/Drafts/Spam/Trash/
Archive) surface with friendly names + icons via RFC 6154 SPECIAL-USE attributes
with a case-insensitive name fallback; the multi-account drawer adds an account
switcher and a unified "All Inboxes". INBOX stays the only auto-synced,
IDLE-watched, notifying folder; other folders sync on demand.

Lower minSdk 33 -> 29 for a rolling ~7-year Android support window; guard the
API-33 POST_NOTIFICATIONS runtime request accordingly.

Tests and CI:
- Bump espresso-core 3.6.1 -> 3.7.0 so Compose UI tests run on API 37
  (3.6.1's InputManagerEventInjectionStrategy reflects a removed hidden method).
- New coverage across layers: FolderRoleTest, ImapClientTest folder cases,
  MailboxViewModelTest, MailRepositoryImplTest folder routing, a FolderDrawer
  Compose UI test, and LibreMailDatabaseTest folder DAO/reconcile tests.
- Gradle Managed Devices + a CI E2E matrix over every API 29-36; a single
  "CI passed" gate job fans in all jobs and is required by branch protection.
- Non-blocking, custom-provisioned API 37 (preview) E2E job with image caching.
- Build + unit-test jobs run on arm64 (ubuntu-24.04-arm); emulators stay x86_64.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:23:00 -05:00
Jason Ross 7e427e1295 Merge pull request #2 from JMR-dev/fix-outlook-login-flow
Fix Outlook sign-in crash and complete the OAuth login flow
2026-06-29 23:26:09 -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
Jason Ross 12598629f5 Merge pull request #1 from JMR-dev/fix/code-review-findings
Fix/code review findings
v0.1.0
2026-06-29 22:02:15 -05:00
JMR-devandClaude Opus 4.8 669946ea34 Add CI/release workflows and first Compose UI tests
GitHub Actions (every action pinned to its commit SHA, version in a comment):
- ci.yml, on pull_request to main, runs three jobs: a debug build, the
  unit tests, and the instrumented suite on a headless emulator.
- release.yml, on workflow_dispatch, builds the release APK, archives the
  source as zip + tar.gz, and publishes a GitHub release.

Compose UI tests (app/src/androidTest), driving the real screens with
fake-backed view models so they need no network, database, or Hilt graph:
- ManualSetupScreen: submit-button validation, advanced-options toggle,
  and add-account success/failure.
- ComposeScreen: send-button enablement and the send -> close flow.
- LibreMailBottomBar: tab rendering and selection callback.

Mark gradlew executable (100755) so it runs on the Linux CI runners.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 21:36:41 -05:00
JMR-devandClaude Opus 4.8 96c9f71a26 Harden transport security and add opt-in encrypted local cache
From a whole-repo security review (no critical/high issues; TLS cert and
hostname validation were already intact):

- Don't offer the plaintext "None" transport in manual account setup; it
  would send credentials in the clear. The enum value stays only for local
  test servers.
- Relabel the advanced toggle "Allow insecure STARTTLS fallback" with a
  warning subtitle: it relaxes (does not enable) STARTTLS and permits a
  plaintext downgrade when on. Default stays off/secure.
- Set mail.<proto>.ssl.checkserveridentity=true explicitly on IMAP/SMTP as
  insurance over the (already-true) Angus default.
- Add a Content-Security-Policy meta to the reader WebView (JavaScript is
  already disabled).
- Strip Log.d/Log.v in release builds and drop the account address from the
  IDLE log; mark new-mail notifications VISIBILITY_PRIVATE.

Add opt-in at-rest encryption of the Room cache (Settings -> "Encrypt local
cache", off by default) using SQLCipher. The DB passphrase is a random key
sealed by the existing Keystore crypto and kept in a separate DataStore.
DatabaseEncryption performs a self-healing, atomic plaintext<->encrypted
migration at startup that preserves PRAGMA user_version, so toggling applies
on next launch without data loss.

Verified end-to-end on an API 37 emulator (DatabaseEncryptionTest round-trip,
7 instrumented tests) plus 12 unit tests and a release R8 build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:50:27 -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 f0b4ee6f57 Remove the "Sign in with Google" account-setup option
Gmail's restricted https://mail.google.com/ scope needs a paid CASA assessment to ship a
public release, so a dedicated Google OAuth button is a dead end. Drop it from the setup
screen — Gmail is still reachable via "Other (IMAP/SMTP)" with an app password.

- Removes the Google button and its now-unused launcher, coroutine scope, imports, and
  strings. The setup screen now offers Microsoft and Other (IMAP/SMTP).
- The underlying Gmail OAuth plumbing (GmailAuthManager, addGmailAccount, the ViewModel's
  Gmail methods) is left intact but unexposed; it can be ripped out entirely or re-surfaced
  later. assemble/lint/test green; verified on the emulator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:55:15 -05:00
JMR-devandClaude Opus 4.8 3d93f76fc1 Send Outlook mail via Microsoft Graph (SMTP fallback)
Microsoft is steadily restricting OAuth SMTP, and Graph sendMail is their first-class send
path, so Outlook now sends through Graph with SMTP/XOAUTH2 as a fallback.

Graph (graph.microsoft.com) and Exchange Online (outlook.office.com) are separate OAuth
resources, so one consent requests all scopes (Graph Mail.Send + IMAP + SMTP) and
OutlookAuthManager mints per-resource access tokens from the single refresh token on demand
(freshGraphToken / freshOutlookToken).

- GraphSender POSTs me/sendMail with a JSON message (recipients, text body, base64
  fileAttachments, saveToSentItems); a unit test covers the payload building.
- SendWorker tries Graph first for Outlook accounts and falls back to SmtpSender on failure;
  Gmail/IMAP accounts are unchanged. MailConnectionFactory.graphTokenFor supplies the token.
- Verified: assemble/lint/test green; on the emulator the two-resource consent is accepted
  (Microsoft renders its sign-in page, no AADSTS multi-resource error). The post-login token
  exchange + actual Graph send need a real Outlook account.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:18:10 -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 23afe78fe0 Enable R8 for release builds
- Turn on minification for the release build and sign it with the debug key so it is
  installable for testing (a public release would use a dedicated keystore).
- proguard-rules.pro keeps the reflection-heavy mail/auth stack: Jakarta/Angus Mail
  (IMAP/SMTP providers resolved via reflection + service files) and AppAuth.
- Verified on the Android 17 emulator: the release APK builds with R8, installs, and
  syncs mail over IMAP — confirming Angus Mail's provider resolution survives shrinking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:06:50 -05:00
JMR-devandClaude Opus 4.8 57b628f90f Add server-side IMAP search
Search previously only filtered the cached inbox. Now a query also runs an IMAP SEARCH
on the server and folds the matches into the cache, so messages beyond the synced
window surface in the results.

- ImapClient.search(query) ORs SUBJECT/FROM/BODY terms and fetches matching headers
  (extracted a shared toFetchedMessage mapper, reused by fetchRecentInbox).
- MailRepository.searchServer inserts/updates matches into the message cache (no
  pruning); MailboxViewModel triggers it from a debounced, deduplicated search query.
- assemble/test/lint green, including a new ImapClient test asserting SEARCH returns
  only the matching message against GreenMail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:00:28 -05:00
JMR-devandClaude Opus 4.8 f65226a4d2 Send attachments
- Compose gains an "Attach file" picker (OpenMultipleDocuments) and shows each pick as
  a removable chip; OutgoingMessage carries the picked URIs.
- On send the repository copies the picked files into the outbox message's own cache
  directory; SendWorker passes them to SmtpSender, which builds a multipart message
  (text body + a part per file via attachFile). Files are cleaned up on success/cancel.
- assemble/test/lint green, including a new SmtpSender test that sends an attachment and
  asserts GreenMail received a multipart message containing it; the compose "Attach file"
  affordance verified on the Android 17 emulator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:54:39 -05:00
JMR-devandClaude Opus 4.8 5c98f3ba1c Add an outbox folder: view, retry, and cancel queued sends
- New Outbox screen lists queued messages with status (Queued, or "Couldn't send" in
  red after a failed attempt), an app-bar Retry, and a per-message cancel.
- The inbox shows an "Outbox (N)" entry while anything is queued. Repository gains
  observeOutbox/cancelOutboxMessage/retryOutbox; OutboxDao.observeAll + OutboxMessage.
- SendScheduler now enqueues the drain with REPLACE rather than APPEND_OR_REPLACE so
  newly-queued mail and manual retries run promptly, overriding a pending retry-backoff
  (previously a queued message could sit behind an exponential backoff for minutes).
- assemble/test/lint green; verified on the Android 17 emulator — a message stuck from an
  earlier offline send showed as failed in the outbox, and tapping Retry (server back up)
  drained it to "Outbox is empty".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:39:36 -05:00
JMR-devandClaude Opus 4.8 dc3147a138 Add drafts: save-for-later and resume
Composing now auto-saves a draft when you leave with anything entered, and sending
deletes it.

- New `drafts` Room table (entity + DAO + Draft model + MIGRATION_5_6, DB v6), with
  repository observe/get/save/delete.
- ComposeViewModel loads a draft by id (resume), saves/updates one on exit (or deletes
  it when emptied), and deletes it after sending; the screen closes via a finished event
  so the save completes before navigating away.
- New Drafts screen (list with per-row delete, resume on tap); the inbox shows a
  "Drafts (N)" entry when any exist. Compose gains a draft nav arg.
- assemble/test/lint green; verified on the Android 17 emulator — the v5->v6 migration
  kept existing mail, a backed-out compose saved a draft, the draft listed and reopened
  pre-filled, and sending it removed the draft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:24:15 -05:00
JMR-devandClaude Opus 4.8 85ba9d9846 Add an outbox: reliable, WorkManager-backed send
Sending was synchronous and failed outright if the network or server hiccupped.
Compose now enqueues to an outbox and a worker delivers in the background.

- New `outbox` Room table (entity + DAO + MIGRATION_4_5, DB v5) holds queued mail.
- MailRepository.sendMessage inserts into the outbox and triggers SendScheduler instead
  of sending inline, so compose returns immediately.
- SendWorker (@HiltWorker) drains the outbox over SMTP, deleting each row on success and
  returning Result.retry() on failure so WorkManager reattempts with backoff (under a
  network constraint); a removed account's queued mail is dropped.
- assemble/test/lint green; verified on the Android 17 emulator — the v4->v5 migration
  preserved existing mail, and a composed message was queued, sent by the worker over
  SMTP, and round-tripped back into the inbox.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:46:49 -05:00
JMR-devandClaude Opus 4.8 fcd84fb933 Add inbox search over cached mail
- 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>
2026-06-27 15:20:19 -05:00
JMR-devandClaude Opus 4.8 1d918cdd75 Stop an account's IDLE watcher when the account is removed
IdleService read the account list once at startup, so a removed account's IDLE
loop kept reconnecting (backing off harmlessly) until the service restarted, and a
newly-added account wasn't watched until then either.

- IdleService now observes the accounts and reconciles one IDLE watcher per account:
  it starts a watcher for an added account and cancels the watcher for a removed one,
  which closes that account's IDLE connection promptly via the existing cancellation
  path.
- The app runs the service only while push is enabled AND at least one account exists,
  so it auto-starts on the first account and stops on the last.
- Verified on the Android 17 emulator: two accounts held two IDLE connections; removing
  one dropped to a single connection with no retry loop, and the other kept idling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:09:27 -05:00
JMR-devandClaude Opus 4.8 ee0bbacb98 Add multi-account: unified inbox with per-account filter
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>
2026-06-27 13:16:49 -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 2acff395a1 Renew IMAP IDLE periodically to survive idle-socket timeouts
A single IDLE connection held indefinitely gets dropped by servers after ~29 min
(RFC 2177) and severed by NAT/firewalls sooner — silently stranding push.

- IdleService bounds each IDLE session with withTimeoutOrNull(IDLE_RENEWAL_MS = 9 min)
  and reconnects, re-issuing IDLE well within those limits. Each reconnect catch-up
  syncs, so no mail is missed across renewals.
- ImapClient.idle() now closes the store from an awaitCancellation() child that runs at
  cancellation *start*. A Job completion handler never runs while idle()'s blocking read
  is stuck cancelling, so it could not unblock idle(); this can, so both renewal and
  service stop break out of idle() promptly.
- Verified on the Android 17 emulator against GreenMail: the IDLE connection
  re-established on schedule (each cycle = fresh connect + IDLE), and a message delivered
  mid-run still pushed a notification within ~2s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:41:02 -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 d81e94717b Add message reading: body fetch + hardened WebView + flag actions
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>
2026-06-26 19:05:55 -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 9c74510832 Add account setup: Gmail OAuth + IMAP/SMTP with encrypted credentials
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>
2026-06-26 10:26:57 -05:00
JMR-devandClaude Opus 4.8 98842a2300 Add .gitattributes for consistent cross-platform line endings
Keeps gradlew as LF (Unix shell), *.bat as CRLF, and treats jars/images as
binary, independent of each contributor's core.autocrlf setting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 00:18:37 -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