WIP: merge queue: checking main (4c3937e) and [#474 + #470] together #476

Closed
mergify[bot] wants to merge 7 commits from mergify/merge-queue/8e7dab1510 into main
7 Commits
Author SHA1 Message Date
mergify[bot] ee4f0c2974 Merge of #470 2026-07-09 01:54:35 +00:00
mergify[bot] 682b0a2122 Merge of #474 2026-07-09 01:54:35 +00:00
mergify[bot] 4c3937edde Merge pull request #469 from JMR-dev/feat-361-gmail-imap-limits
perf(gmail): respect Gmail IMAP connection & bandwidth limits in sync/backfill
2026-07-09 01:15:21 +00:00
mergify[bot] cb9e0e3e54 Merge pull request #467 from JMR-dev/fix-apppassword-intent-flake
test(accountsetup): de-flake URL-open link E2E tests via fake LocalUriHandler
2026-07-09 01:15:18 +00:00
JMR-dev c5144ebe03 perf(outlook): respect Microsoft Graph throttling with batching & chunked upload
Add a throttle-aware Microsoft Graph HTTP layer (org.libremail.mail.graph) and route
the live me/sendMail path through it, composing with #360's AccountThrottleGate:

- GraphHttpClient: the single Graph HTTP transport seam; preserves the send path's
  may-have-sent distinction (GraphTransportException) and parses Retry-After.
- GraphThrottle: caps Graph concurrency at 4, honors a 429/503 Retry-After via the
  shared per-account backoff gate (retry after the honored wait, bounded), clears it
  on a 2xx. Because the gate is shared+account-keyed, a Graph 429 also cools that
  account's IMAP background work down.
- GraphBatch: multiplexes ops via $batch (<=20/call), collapsing N calls to
  ceil(N/20); feeds per-op 429s back into the gate.
- GraphUploadSession: createUploadSession + chunked Content-Range PUTs for content
  over Graph's ~4 MB one-shot ceiling (320 KiB-multiple chunks).
- GraphSender.send now honors a Graph 429 once (Retry-After) before falling back to
  SMTP, instead of failing over on the first throttle.

Outlook mail is read over IMAP and the Graph token is Mail.Send-scoped, so $batch
reads and draft-based chunked attachment upload have no live call site yet (they need
the Mail.ReadWrite scope, a re-consent-forcing change kept out of this perf ticket);
both ship as fully-tested capabilities of the Graph layer.

Unit tests (MockK the HTTP client, coroutines-test virtual time, no real sleeps) cover
429+Retry-After backoff, $batch call-count reduction, and chunked upload; an
instrumented test exercises the toolkit under the Android runtime. PII-free AppLog
(accountLogRef) throughout; SPDX on every file.

Closes #364
2026-07-08 19:26:32 -05:00
JMR-dev b1a7931dfa perf(gmail): respect Gmail IMAP connection & bandwidth limits in sync/backfill
Add Gmail's documented IMAP caps (15 max simultaneous connections, 2,500 MB/day
download, 500 MB/day upload, 10,000 messages/labels per limit) as provider-scoped
config/policy that feeds the existing #360/#356 pacing machinery instead of
reinventing it:

- GmailSyncLimits: pure constants + `appliesTo(account)` provider detection via the
  existing MailProvider.forImapHost lookup (no changes to MailProvider itself).
- GmailBandwidthTracker: a new, per-account/per-day download-byte tracker (mirrors
  AccountThrottleGate's shape: ConcurrentHashMap state, injectable clock, PII-free
  once-per-crossing AppLog breadcrumb). Proactive and orthogonal to the #360
  AccountThrottleGate (which only reacts to a provider-issued throttle) and #356's
  BackfillPacer (which paces slice cadence, not bytes) - same relationship
  InteractiveImapGate already documents having to AccountThrottleGate.

Wiring: MailRepositoryImpl.prefetchMessage (the single funnel both MailBackfiller
and MailSyncer's background prefetch already share) records bytes actually pulled
over the network for Gmail accounts; MailBackfiller/MailSyncer's prefetchIfEnabled
consult isOverDailyBudget once per account before starting a batch and defer
body/attachment prefetch for the rest of the day once Gmail's budget is reached -
header paging/sync is never gated, and interactive fetches (open, attachment tap,
inline images) are never gated either, matching the existing interactive-priority
principle (#355/#360).

The 15-connection cap is already satisfied by the existing architecture
(ImapConnectionCache keeps one reused connection per account plus one dedicated
IDLE connection - 2 total, well under the cap); GmailSyncLimitsTest pins that
invariant against the documented ceiling so a future change that grows per-account
concurrency trips a test before it could approach Gmail's real limit. The
10k-messages-per-label figure is captured as a documented constant only - it is
deliberately NOT wired into a backfill stop condition, since issue #12's full
history backfill is intentional and a large real mailbox can exceed 10k messages.

AccountThrottleGate, BackfillPacer, ThrottleClassifier/ThrottleSignal/ThrottleBackoff,
InteractiveImapGate, and MailProvider are all untouched.

Closes #361
2026-07-08 19:23:25 -05:00
JMR-dev 65eae0f6c7 test(accountsetup): de-flake URL-open link E2E tests via fake LocalUriHandler
The outbound-link E2E tests verified the opened page with Espresso-Intents
(intending(ACTION_VIEW).respondWith(...) + intended(...)). intended() runs an
onView(isRoot()).check(...) whose RootViewPicker waits up to 10s for a
window-focused root. On the CI matrix emulator the activity window
intermittently reports has-window-focus=false, so the assertion flakes with
RootViewPicker$RootViewWithoutFocusException, failing the whole E2E leg and
forcing a 9-min retry. The intent stubs were already present and do NOT fix
this: no external activity launches, the focus loss is environmental (the same
run failed 8 unrelated RootViewPicker-based tests at once).

Verify these ACTION_VIEW/browser-open link taps by injecting a recording
LocalUriHandler and asserting the exact URL the screen opens. That keeps the
tests entirely on Compose interactions, which do not depend on window focus
(280+ Compose-only tests passed in the same failing run), so they are
deterministic without weakening the assertion (still asserts the provider
page / host).

Converted (ACTION_VIEW / UriHandler "browser-open" shape):
- AppPasswordSetupScreenTest.tappingCreateAppPasswordPage_launchesBrowserIntentToHelpUrl
- AppPasswordSetupScreenTest.imapDisabledFailure_showsThePrompt_andHelpLinkOpensTheProviderPage
- OutlookImapNoticeScreenTest.tappingImapHelpLink_opensTheMicrosoftArticle

Real-intent tests (hasComponent/Settings action, no UriHandler seam) keep
Espresso-Intents and are out of scope here.
2026-07-08 19:04:13 -05:00