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>
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>
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>
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>
"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>
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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
- 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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>