Commit Graph
783 Commits
Author SHA1 Message Date
JMR-devandClaude Opus 4.8 e4457cfec1 test(db): expect schema v20 in encryption round-trip after v19->v20 bump
MIGRATION_19_20 (issue #187) bumped the Room cache schema to version 20,
but DatabaseEncryptionTest.schemaVersionIsCarriedOntoTheEncryptedFile still
asserted the plaintext -> encrypted conversion carried version 19, so it
failed across all E2E levels after the rebase onto main.

DatabaseEncryption.migrate() carries PRAGMA user_version dynamically
(userVersion = source.version -> target.version = userVersion), and a fresh
Room open now stamps 20, so v20 genuinely survives the conversion. Update the
expected constant to 20; the assertion's intent (the version survives the
round-trip) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 03:53:46 -05:00
Jason Ross 2e04934e19 Merge main into fix-306-outlook-redundant-token 2026-07-04 03:43:43 -05:00
Jason Ross 7bd909882f Merge main into fix-reporting-pii-mainthread 2026-07-04 03:43:43 -05:00
Jason Ross 9110915629 Merge pull request #321 from JMR-dev/fix-303-304-ui-correctness
fix(ui): reader Reply quotes original + guard one-shot actions against double-tap
2026-07-04 03:43:14 -05:00
Jason Ross def89c4e2a Merge main into fix-reporting-pii-mainthread 2026-07-04 03:21:30 -05:00
Jason Ross 8cb57cba50 Merge main into fix-306-outlook-redundant-token 2026-07-04 03:21:29 -05:00
Jason Ross 1649154ac4 Merge main into fix-303-304-ui-correctness 2026-07-04 03:21:27 -05:00
Jason Ross 5125a6674c Merge pull request #320 from JMR-dev/fix-310-311-312-dao
perf/fix(data): batch sync updates, paging tiebreaker, migration-registration test
2026-07-04 03:20:59 -05:00
Jason Ross c3d335a753 Merge main into fix-303-304-ui-correctness 2026-07-04 03:01:24 -05:00
Jason Ross 42b8bcc91e Merge main into fix-310-311-312-dao 2026-07-04 03:01:22 -05:00
Jason Ross 2289f402e5 Merge main into perf-187-covering-index 2026-07-04 03:01:21 -05:00
Jason Ross 97b782ecb2 Merge main into fix-306-outlook-redundant-token 2026-07-04 03:01:19 -05:00
Jason Ross 61dc35e290 Merge main into fix-reporting-pii-mainthread 2026-07-04 03:01:17 -05:00
Jason Ross cbf284b5f5 Merge pull request #315 from JMR-dev/fix-account-lifecycle-integrity
fix(data): account-lifecycle data integrity (non-destructive upsert, id normalization, deleteAccount cleanup)
2026-07-04 03:00:44 -05:00
Jason Ross c645e0e1fe Merge main into fix-reporting-pii-mainthread 2026-07-04 02:45:04 -05:00
Jason Ross b33f73273d Merge main into fix-306-outlook-redundant-token 2026-07-04 02:45:03 -05:00
Jason Ross 61437e8670 Merge main into fix-account-lifecycle-integrity 2026-07-04 02:45:01 -05:00
Jason Ross 1d4bd6346c Merge main into perf-187-covering-index 2026-07-04 02:44:59 -05:00
Jason Ross 2b616df3e0 Merge main into fix-310-311-312-dao 2026-07-04 02:44:58 -05:00
Jason Ross f821e0274e Merge main into fix-303-304-ui-correctness 2026-07-04 02:44:57 -05:00
Jason Ross 3062a3a3d9 Merge pull request #318 from JMR-dev/fix-295-targeted-batch-expunge
fix(mail): targeted + batch expunge (stop deleting unrelated \Deleted mail)
2026-07-04 02:44:21 -05:00
Jason Ross 205f1f6de1 Merge main into fix-303-304-ui-correctness 2026-07-04 02:37:40 -05:00
JMR-devandClaude Opus 4.8 6245533368 fix(ui): reader Reply quotes original + guard one-shot actions against double-tap
#303: the reader's Reply now routes through MailRepository.buildReplyDraft
(quotes the original into a <blockquote>, bakes the signature, prefixes Re:/Fwd:
without double-prefixing) and opens compose on the built draft via
ReaderEvent.OpenCompose — the same high-fidelity path the mailbox uses — instead
of a bare compose prefill with an empty body. Adds Reply-All and Forward via an
app-bar overflow menu.

#304: SignatureEditViewModel.save, ReportReviewViewModel.submit,
AccountSettingsViewModel.removeAccount, and ProblemReportsViewModel.createManualReport
now flip a busy/saving flag synchronously before the first suspension and gate
their buttons, so a rapid double-tap can't create duplicate signatures/reports,
enqueue two uploads, or over-pop the back stack.

Closes #303
Closes #304

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 02:36:15 -05:00
Jason Ross c98eac6c5b Merge main into fix-310-311-312-dao 2026-07-04 02:32:20 -05:00
JMR-devandClaude Opus 4.8 edbb1eb839 perf/fix(data): batch sync updates, paging tiebreaker, migration-registration test
#310: add a single-transaction MessageDao.updateHeaderContents(List<MessageEntity>)
and route MailSyncer's per-message updateHeaderContent loop through it, so a folder's
recent-window refresh commits once instead of once per message (fsync/journal write
per message, amplified on the encrypted cache).

#311: append the `id` primary key as a tiebreaker to the four paged MessageDao
`ORDER BY timestampMillis DESC` queries for a total order, so rows sharing a second
(bulk mail) can't duplicate or skip across a LIMIT/OFFSET page boundary. Pure query-text
change: the exported Room schema (identityHash) is derived from table/index structure,
not @Query SQL, so no schema re-export or version bump; id is already in the projection,
so no new index.

#312: expose the registered migration list as DatabaseModule.ALL_MIGRATIONS (spread into
addMigrations) and assert in MigrationTest that it equals the reflectively-discovered set
of every Migration val, so a migration forgotten in addMigrations fails a test instead of
crash-looping all upgrading users at DB open (there is deliberately no destructive fallback).

Tests: new MessageDaoTest cases for the batch update and the id tiebreaker (all four
pagers), and the MigrationTest registration assertion; wired updateHeaderContents into
MailSyncConcurrencyTest's fake DAO. Instrumented MessageDaoTest + MigrationTest (31 tests)
green on a local emulator; unit tests + androidTest compile + ktlint + detekt green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 02:30:50 -05:00
Jason Ross df57dcd18d Merge main into perf-187-covering-index 2026-07-04 02:26:52 -05:00
Jason Ross 6810cc5390 Merge main into fix-account-lifecycle-integrity 2026-07-04 02:26:51 -05:00
Jason Ross 5e2d65d91a Merge main into fix-reporting-pii-mainthread 2026-07-04 02:26:50 -05:00
Jason Ross e80f8333f4 Merge main into fix-306-outlook-redundant-token 2026-07-04 02:26:49 -05:00
Jason Ross 6de885d53e Merge main into fix-295-targeted-batch-expunge 2026-07-04 02:26:48 -05:00
Jason Ross b95ae2e7eb Merge pull request #314 from JMR-dev/fix-302-idleservice-fgs-timeout
fix(push): handle Service.onTimeout to survive the dataSync FGS runtime cap
2026-07-04 02:26:16 -05:00
JMR-devandClaude Opus 4.8 f72c66d291 fix(mail): targeted + batch expunge (stop deleting unrelated \Deleted mail)
ImapClient.deleteMessage and moveMessages flagged the target \Deleted then
called the untargeted Folder.expunge(), which permanently removes EVERY
\Deleted-flagged message in the folder — not just the intended UIDs. That is a
data-loss window whenever a second client, Gmail, or a partial earlier move has
left other messages flagged \Deleted. The repository's batch delete/expunge and
trash-fallback paths also looped single-UID deleteMessage, paying N logins + N
expunges for an N-message selection.

Add a batch deleteMessages(uids) that opens the folder once, flags the matched
messages \Deleted, and issues a single targeted UID EXPUNGE (RFC 4315) via
IMAPFolder.expunge(Message[]) through a shared expungeTargeted() helper. Route
moveMessages through the same helper, delegate single-UID deleteMessage to
deleteMessages, and route MailRepositoryImpl.expunge and the moveByRole trash
fallback through the batch method. moveToFolder already batches via moveMessages,
so it inherits the targeted expunge.

On a server without UIDPLUS, Angus raises "UID EXPUNGE not supported" rather than
silently falling back to the unrelated-mail-destroying untargeted expunge — a
loud failure is the safe outcome. Gmail, Outlook, and GreenMail all advertise
UIDPLUS.

Tests (GreenMail, no emulator): deleteMessages/moveMessages expunge only the
given UIDs and spare other \Deleted-flagged mail; a batch delete of three
messages opens exactly one connection and pays one LOGIN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 02:24:40 -05:00
JMR-devandClaude Opus 4.8 da2963aa86 fix(auth): drop redundant second Outlook token request on sign-in
exchangeToken's authorization-code exchange already requests
`openid email offline_access $OUTLOOK_SCOPE`, so the returned access token
is an outlook.office.com token usable for IMAP verification and the AuthState
already carries the refresh token and expiry. The immediate follow-up
refreshForScope(authState, OUTLOOK_SCOPE) was a second round-trip for the
same resource that only rotated the just-issued refresh token and added a
needless onboarding failure point (a transient network error there failed
sign-in after consent + code-exchange had already succeeded).

Build OAuthResult directly from the code-exchange tokenResponse
(accessToken + authState.jsonSerializeString()), dropping the extra refresh.
The durable AuthState is still serialized and persisted for later token
refresh; the Graph token remains a distinct resource minted on demand via
freshGraphToken.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 02:22:00 -05:00
Jason Ross 64f4c64936 Merge main into fix-reporting-pii-mainthread 2026-07-04 02:19:16 -05:00
JMR-devandClaude Opus 4.8 005f8a3aca fix(reporting): scrub PII from crash stack traces + move ReportStore scan off the main thread
A crash report captured throwable.stackTraceToString() verbatim, so mail/network
exceptions (Jakarta Mail, java.net) could embed server host:port tokens and account
emails/usernames in the report's stackTrace field — violating the PII-free-reports
constraint. Add StackTraceScrubber, applied in DiagnosticsCollector before the trace
enters toSubmissionPayload()/toStorageJson(): it keeps the non-PII value (exception
class names + every frame's class/method/file/line) and drops each header line's
free-text message (where hostnames/usernames live), then redacts any residual email
or host:port left on a wrapped continuation line. Frame lines are untouched, so a
frame's File.kt:42 is never mistaken for a host:port.

ReportStore did MutableStateFlow(scan()) in its constructor — a dir list + read +
JSON-parse of every stored report. As an eager @Singleton dep of CrashReporter, whose
install() runs on the MAIN thread in Application.onCreate(), this was main-thread disk
I/O that grows with the 30-day retention. Seed the flow empty and dispatch the initial
scan to an injectable scope (Dispatchers.IO by default); reactive consumers update when
it lands, and writes still re-scan synchronously so a crash-time save is never lost.

Tests: StackTraceScrubberTest (host/ip/port/email dropped from a ConnectException +
auth-failure trace while classes/frames survive; regex redaction of a continuation
line; null-message trace preserved verbatim); DiagnosticsCollector end-to-end scrub
test; ReportStore empty-seed + off-thread populate via a StandardTestDispatcher. Store
constructions in existing tests use an Unconfined scope to keep their synchronous
reopen semantics.

Closes #294
Closes #296

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 02:18:10 -05:00
Jason Ross 659f6e1d74 Merge main into fix-account-lifecycle-integrity 2026-07-04 02:14:30 -05:00
JMR-devandClaude Opus 4.8 cae11233f3 fix(data): account-lifecycle data integrity (non-destructive upsert, id normalization, deleteAccount cleanup)
#309: AccountDao no longer uses @Insert(REPLACE). New insertIfAbsent (IGNORE)
+ @Update back a non-destructive upsert, and insertAtEnd updates an existing id
in place (preserving its sortOrder) instead of REPLACE. Re-adding an existing
account id (e.g. re-authing an Outlook account, whose id is the deterministic
outlook:<email>) therefore no longer cascade-deletes its account_settings +
signatures.

#305: normalizeEmailForAccountId always lowercases the domain (mail domains are
case-insensitive), and the whole address for the consumer providers (Gmail,
Yahoo, iCloud, AOL, Outlook). Applied at every id-derivation site
(MailProvider.createAccount, Account.outlook, ManualSetupViewModel) so
differently-cased addresses can't spawn duplicate accounts. The displayed email
keeps the user's casing.

#299: deleteAccount collects the account's message ids and draft attachment URIs
while the rows still exist, deletes the rows (now including the account's
drafts), then deleteRecursively()'s each message's on-disk attachment cache dir
and releases the drafts' now-unreferenced persistable URI grants.

Adds unit tests for id normalization + deleteAccount cleanup and DAO-level
instrumented tests for the non-destructive create/update.

Closes #309
Closes #305
Closes #299

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 02:13:21 -05:00
Jason Ross ce9e54f704 Merge main into fix-302-idleservice-fgs-timeout 2026-07-04 02:10:04 -05:00
JMR-devandClaude Opus 4.8 8904351a96 fix(push): handle Service.onTimeout to survive the dataSync FGS runtime cap
IdleService runs continuously as a FOREGROUND_SERVICE_TYPE_DATA_SYNC
foreground service (push is on by default). With targetSdk 37, Android 14+'s
dataSync FGS runtime cap (~6h per rolling 24h) calls Service.onTimeout(...)
and then force-stops the service — throwing a system FGS-timeout exception —
if it doesn't stop itself. IdleService overrode onStartCommand/onDestroy/onBind
but not onTimeout, so after ~6 cumulative hours push silently died and the app
hit the exception; on API 35+ the budget is cumulative and a restart can't
recover it until the next 24h window.

Override both onTimeout(startId) (deprecated, API 34) and
onTimeout(startId, fgsType) (API 35+); both route to a clean shutdown that
re-asserts the already-scheduled 15-minute periodic sync, swaps the persistent
notification to a degraded "paused" text and DETACHes it so it survives, then
stopForeground(DETACH) + stopSelf so we never leave a dataSync FGS running past
its cap (the exact condition the platform kills on). This mirrors the existing
low-battery PushMode.POLLING fallback.

The push-status text choice is pulled into a pure PushStatusNotification.statusTextRes
seam and unit-tested on the JVM; the built notification's new timed-out text is
covered by PushStatusNotificationInstrumentedTest.

Closes #302

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 02:09:01 -05:00
Jason Ross 8230dd0341 Merge main into perf-187-covering-index 2026-07-04 01:56:44 -05:00
Jason Ross b64c7f8dda Merge pull request #293 from JMR-dev/test-289-branch-coverage
test(coverage): high-value branch-coverage additions
2026-07-04 01:56:15 -05:00
Jason Ross 0e42358d27 Merge main into test-289-branch-coverage 2026-07-04 01:38:09 -05:00
JMR-devandClaude Opus 4.8 b165f72e3a test(coverage): high-value branch-coverage additions
Add focused JVM unit tests that close real (non-coroutine) branch gaps in
pure logic already >=95% line-covered (issue #289):

- richtext/RichTextEditing: removeLink, applyLink/styleAt/isStyled edges,
  quote/ordered marker detection+removal, remap* null branches.
- richtext/RichTextHtmlParser: new suite driving parseCssColor/parseFontSizePt/
  parseInlineStyles/parseBaseStyle/parseTextAlign/extractHref/unescape and the
  parser's malformed/stray/unclosed-tag edges.
- ui/compose/ComposeViewModel + ui/mailbox/MailboxViewModel: nav-arg blanks,
  signature-swap rebuild, autosave content detection, refresh/selection edges.
- data/repository/MailRepositoryImpl: non-selectable role folder, cancelOutboxMessage.
- data/repository/AccountRepositoryImpl: reorderAccounts.
- mail/HtmlToText, data/SignatureBlock, reporting/AppLog, reporting/DiagnosticsCollector.

Test-only; no production changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 01:36:31 -05:00
Jason Ross 5c34b01bf0 Merge main into perf-187-covering-index 2026-07-04 01:22:27 -05:00
Jason Ross 7966efde2f Merge pull request #291 from JMR-dev/test-288-coverage-gaps
test(coverage): close clean JVM-testable coverage gaps
2026-07-04 01:21:57 -05:00
Jason Ross 1af6ad3d52 Merge main into test-288-coverage-gaps 2026-07-04 01:05:26 -05:00
JMR-devandClaude Opus 4.8 a6c24f3cea test(coverage): close clean JVM-testable coverage gaps
Close the cleanly JVM-testable coverage gaps from the Phase-2 JaCoCo
audit (#288), bringing each targeted class to 100% line coverage:

- AccountSettingsRepository: observe Flow + the sibling setters
  (signature/notifications enabled, retention count/months incl. clamp).
- SignatureRepository: observeForAccount/get/getDefault/update + toDomain.
- CredentialStore (new): save/load/delete with a mocked KeystoreCrypto.
- EncryptedCacheGuard (new): the isCacheLocked() truth table.
- AttachmentUriGrants: releaseUnreferenced/referencedUris wiring.
- SyncScheduler: schedulePeriodicReportPurge.
- AppLockViewModel: nonce, onBackground cover branch, unwrap cancellation
  rethrow, awaitSyncEnqueue execution/interrupt branches.
- SmtpSender: send error paths (SSL/STARTTLS) + cc + inline+regular body.
- StartupReportViewModel: the @Inject real-clock constructor.

All test-only. Unit tests + jacocoTestReport + ktlintCheck + detekt green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 01:04:24 -05:00
Jason Ross f4a95a6051 Merge main into perf-187-covering-index 2026-07-04 00:59:28 -05:00
Jason Ross a8399228f6 Merge pull request #287 from JMR-dev/test-infra-284-helper-cwd
test-infra: make local_instrumented.sh CWD-independent
2026-07-04 00:58:57 -05:00
Jason Ross 991f9b77e4 Merge main into perf-187-covering-index 2026-07-04 00:38:29 -05:00