Gmail-specific IMAP connection and bandwidth caps, applied as provider-scoped config/policy that
feeds the existing #360 (AccountThrottleGate, reactive backoff) and #356 (BackfillPacer,
proactive inter-slice cooldown) machinery — neither is modified.
Gmail caps applied (GmailSyncLimits, pure constants + appliesTo(account))
Connection cap: already satisfied by the existing architecture. ImapConnectionCache
(#125/#357) keeps at most one reused connection per account plus one dedicated IMAP-IDLE
connection — 2 total, well under 14. GmailSyncLimitsTest pins that invariant against the
documented ceiling so a future change that grows per-account concurrency (e.g. a real connection
pool) trips a test before it could approach Gmail's real limit. No enforcement code was needed (or
added) beyond that assertion — building a pool here risked colliding with the sibling Yahoo/iCloud
tickets' own connection-cap work on the same ImapConnectionCache/ImapClient.
Bandwidth-aware pacing: the genuinely new mechanism. GmailBandwidthTracker (new, @Singleton, mirrors AccountThrottleGate's shape — ConcurrentHashMap state, injectable clock,
PII-free once-per-crossing AppLog breadcrumb) tracks per-account, per-day download bytes. MailRepositoryImpl.prefetchMessage — the single funnel both MailBackfiller and MailSyncer's
background prefetch already share — records bytes actually pulled over the network (body chars +
actually-downloaded attachment bytes, not cache hits) for Gmail accounts only. MailBackfiller/MailSyncer's prefetchIfEnabled each consult isOverDailyBudget once per
account before starting a prefetch batch and defer for the rest of the day once Gmail's budget is
reached. Header paging/sync is never gated, and interactive fetches (message open, attachment tap,
inline images) are never gated either — same interactive-priority principle #355/#360 already
apply elsewhere.
10k-messages-per-label: captured as a documented constant only, deliberately NOT wired into a
backfill stop condition — issue #12's full-history backfill is intentional and a real large mailbox
can exceed 10k messages, so treating this as a hard ceiling would silently truncate history for
exactly the users the feature is for.
AccountThrottleGate (#360) is unmodified — it still handles the reactive case (a real
provider throttle/lockout response); ThrottleClassifier's existing generic patterns already
match Gmail's real-world throttle text (e.g. "too many simultaneous connections"), so no
Gmail-specific classifier changes were needed either.
BackfillPacer (#356) is unmodified — it still paces slice cadence, not bytes.
GmailBandwidthTracker is a new, orthogonal, proactive mechanism — the same relationship InteractiveImapGate documents having to AccountThrottleGate: composes with the other two,
doesn't duplicate or replace either.
Shared files touched (for conflict-awareness with #362/#363/#364)
Kept additive and Gmail-scoped throughout — no changes to MailProvider.kt, AccountThrottleGate.kt, BackfillPacer.kt, ThrottleSignal.kt/ThrottleClassifier.kt/ThrottleBackoff.kt, InteractiveImapGate.kt, or ImapClient.kt/ImapConnectionCache.kt. Files touched that a sibling
provider ticket could also want to touch:
MailBackfiller.kt / MailSyncer.kt — small additive change to prefetchIfEnabled (new
Gmail-only budget check + a new GmailBandwidthTracker constructor param). Yahoo/iCloud connection
caps are more likely to land in ImapClient/ImapConnectionCache instead (untouched here), but
flagging in case #362/#363 also touch these two files.
MailBackfiller.prefetchIfEnabled also gained an account: Account parameter (previously just ids: List<String>) since the Gmail check needs the account to detect the provider.
MailRepositoryImpl.kt — new bandwidthTracker: GmailBandwidthTracker constructor param; prefetchMessage now records downloaded bytes; ensureAttachmentFile (private) now returns a
small AttachmentFetch(file, downloadedBytes) instead of a bare File (its 3 call sites updated
accordingly). MailRepository's public interface is unchanged. Unlikely to overlap with
#362/#363 (no byte-budget in their issues); #364 (Outlook/Graph) would likely land in a separate
Graph REST client rather than this IMAP path, but flagging since Outlook accounts currently still
flow through ImapClient/this same repository.
config/detekt/detekt.yml — one appended line, excluding the new GmailBandwidthTrackerTest.kt from the android.util.Log forbidden-import rule (it mockkStatic(Log::class) the same way AccountThrottleGateTest/BackfillPacerTest already do).
Tests
GmailSyncLimitsTest — documented constants, the connection-cap architecture invariant, appliesTo for Gmail (incl. the legacy imap.googlemail.com host) vs. Yahoo/iCloud/AOL/Outlook.
GmailBandwidthTrackerInstrumentedTest (new, mock-free, mirrors BackfillPacerInstrumentedTest's
idiom) — concurrent-update correctness and account isolation on the real dispatcher/JVM, and appliesTo resolving the real MailProvider presets on-device.
New/updated cases in MailBackfillerTest, MailSyncerTest, MailRepositoryImplTest composing
the new Gmail gating with the real backfill/sync/prefetch paths (incl. proving a non-Gmail account
is unaffected by an over-budget tracker entry for the same account id, and that header
paging/sync keeps running while prefetch is deferred).
Verification
Fast gate green locally (JDK 21): assembleDebug, testDebugUnitTest, jacocoTestCoverageVerification, compileDebugAndroidTestKotlin, lintDebug, ktlintCheck, detekt. Per the dispatch instructions, local emulator E2E was skipped as flaky/non-authoritative —
CI's full matrix is authoritative here.
Not armed for auto-merge; please review.
Closes #361
## What
Gmail-specific IMAP connection and bandwidth caps, applied as provider-scoped config/policy that
feeds the existing #360 (`AccountThrottleGate`, reactive backoff) and #356 (`BackfillPacer`,
proactive inter-slice cooldown) machinery — neither is modified.
## Gmail caps applied (`GmailSyncLimits`, pure constants + `appliesTo(account)`)
- `MAX_IMAP_CONNECTIONS = 15`, `INTERACTIVE_RESERVED_CONNECTIONS = 1` ->
`MAX_BACKGROUND_IMAP_CONNECTIONS = 14`.
- `DAILY_DOWNLOAD_BUDGET_BYTES = 2,500 MB`, `DAILY_UPLOAD_BUDGET_BYTES = 500 MB`.
- `MAX_MESSAGES_PER_LABEL = 10,000`, `MAX_LABELS = 10,000`.
**Connection cap:** already satisfied by the existing architecture. `ImapConnectionCache`
(#125/#357) keeps at most one reused connection per account plus one dedicated IMAP-IDLE
connection — 2 total, well under 14. `GmailSyncLimitsTest` pins that invariant against the
documented ceiling so a future change that grows per-account concurrency (e.g. a real connection
pool) trips a test before it could approach Gmail's real limit. No enforcement code was needed (or
added) beyond that assertion — building a pool here risked colliding with the sibling Yahoo/iCloud
tickets' own connection-cap work on the same `ImapConnectionCache`/`ImapClient`.
**Bandwidth-aware pacing:** the genuinely new mechanism. `GmailBandwidthTracker` (new,
`@Singleton`, mirrors `AccountThrottleGate`'s shape — `ConcurrentHashMap` state, injectable clock,
PII-free once-per-crossing `AppLog` breadcrumb) tracks per-account, per-day download bytes.
`MailRepositoryImpl.prefetchMessage` — the single funnel both `MailBackfiller` and `MailSyncer`'s
background prefetch already share — records bytes actually pulled over the network (body chars +
actually-downloaded attachment bytes, not cache hits) for Gmail accounts only.
`MailBackfiller`/`MailSyncer`'s `prefetchIfEnabled` each consult `isOverDailyBudget` once per
account before starting a prefetch batch and defer for the rest of the day once Gmail's budget is
reached. Header paging/sync is never gated, and interactive fetches (message open, attachment tap,
inline images) are never gated either — same interactive-priority principle #355/#360 already
apply elsewhere.
**10k-messages-per-label:** captured as a documented constant only, deliberately NOT wired into a
backfill stop condition — issue #12's full-history backfill is intentional and a real large mailbox
can exceed 10k messages, so treating this as a hard ceiling would silently truncate history for
exactly the users the feature is for.
## How this composes with #360 / #356
- `AccountThrottleGate` (#360) is unmodified — it still handles the *reactive* case (a real
provider throttle/lockout response); `ThrottleClassifier`'s existing generic patterns already
match Gmail's real-world throttle text (e.g. "too many simultaneous connections"), so no
Gmail-specific classifier changes were needed either.
- `BackfillPacer` (#356) is unmodified — it still paces slice *cadence*, not bytes.
- `GmailBandwidthTracker` is a new, orthogonal, *proactive* mechanism — the same relationship
`InteractiveImapGate` documents having to `AccountThrottleGate`: composes with the other two,
doesn't duplicate or replace either.
## Shared files touched (for conflict-awareness with #362/#363/#364)
Kept additive and Gmail-scoped throughout — no changes to `MailProvider.kt`, `AccountThrottleGate.kt`,
`BackfillPacer.kt`, `ThrottleSignal.kt`/`ThrottleClassifier.kt`/`ThrottleBackoff.kt`,
`InteractiveImapGate.kt`, or `ImapClient.kt`/`ImapConnectionCache.kt`. Files touched that a sibling
provider ticket *could* also want to touch:
- `MailBackfiller.kt` / `MailSyncer.kt` — small additive change to `prefetchIfEnabled` (new
Gmail-only budget check + a new `GmailBandwidthTracker` constructor param). Yahoo/iCloud connection
caps are more likely to land in `ImapClient`/`ImapConnectionCache` instead (untouched here), but
flagging in case #362/#363 also touch these two files.
- `MailBackfiller.prefetchIfEnabled` also gained an `account: Account` parameter (previously just
`ids: List<String>`) since the Gmail check needs the account to detect the provider.
- `MailRepositoryImpl.kt` — new `bandwidthTracker: GmailBandwidthTracker` constructor param;
`prefetchMessage` now records downloaded bytes; `ensureAttachmentFile` (private) now returns a
small `AttachmentFetch(file, downloadedBytes)` instead of a bare `File` (its 3 call sites updated
accordingly). `MailRepository`'s public interface is unchanged. Unlikely to overlap with
#362/#363 (no byte-budget in their issues); #364 (Outlook/Graph) would likely land in a separate
Graph REST client rather than this IMAP path, but flagging since Outlook accounts currently still
flow through `ImapClient`/this same repository.
- `config/detekt/detekt.yml` — one appended line, excluding the new
`GmailBandwidthTrackerTest.kt` from the `android.util.Log` forbidden-import rule (it
`mockkStatic(Log::class)` the same way `AccountThrottleGateTest`/`BackfillPacerTest` already do).
## Tests
- `GmailSyncLimitsTest` — documented constants, the connection-cap architecture invariant,
`appliesTo` for Gmail (incl. the legacy `imap.googlemail.com` host) vs. Yahoo/iCloud/AOL/Outlook.
- `GmailBandwidthTrackerTest` — accumulation, per-account isolation, day-rollover reset,
threshold-crossing, no-op on non-positive bytes, PII-free once-per-crossing logging.
- `GmailBandwidthTrackerInstrumentedTest` (new, mock-free, mirrors `BackfillPacerInstrumentedTest`'s
idiom) — concurrent-update correctness and account isolation on the real dispatcher/JVM, and
`appliesTo` resolving the real `MailProvider` presets on-device.
- New/updated cases in `MailBackfillerTest`, `MailSyncerTest`, `MailRepositoryImplTest` composing
the new Gmail gating with the real backfill/sync/prefetch paths (incl. proving a non-Gmail account
is unaffected by an over-budget tracker entry for the same account id, and that header
paging/sync keeps running while prefetch is deferred).
## Verification
Fast gate green locally (JDK 21): `assembleDebug`, `testDebugUnitTest`,
`jacocoTestCoverageVerification`, `compileDebugAndroidTestKotlin`, `lintDebug`, `ktlintCheck`,
`detekt`. Per the dispatch instructions, local emulator E2E was skipped as flaky/non-authoritative —
CI's full matrix is authoritative here.
Not armed for auto-merge; please review.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #361
What
Gmail-specific IMAP connection and bandwidth caps, applied as provider-scoped config/policy that
feeds the existing #360 (
AccountThrottleGate, reactive backoff) and #356 (BackfillPacer,proactive inter-slice cooldown) machinery — neither is modified.
Gmail caps applied (
GmailSyncLimits, pure constants +appliesTo(account))MAX_IMAP_CONNECTIONS = 15,INTERACTIVE_RESERVED_CONNECTIONS = 1->MAX_BACKGROUND_IMAP_CONNECTIONS = 14.DAILY_DOWNLOAD_BUDGET_BYTES = 2,500 MB,DAILY_UPLOAD_BUDGET_BYTES = 500 MB.MAX_MESSAGES_PER_LABEL = 10,000,MAX_LABELS = 10,000.Connection cap: already satisfied by the existing architecture.
ImapConnectionCache(#125/#357) keeps at most one reused connection per account plus one dedicated IMAP-IDLE
connection — 2 total, well under 14.
GmailSyncLimitsTestpins that invariant against thedocumented ceiling so a future change that grows per-account concurrency (e.g. a real connection
pool) trips a test before it could approach Gmail's real limit. No enforcement code was needed (or
added) beyond that assertion — building a pool here risked colliding with the sibling Yahoo/iCloud
tickets' own connection-cap work on the same
ImapConnectionCache/ImapClient.Bandwidth-aware pacing: the genuinely new mechanism.
GmailBandwidthTracker(new,@Singleton, mirrorsAccountThrottleGate's shape —ConcurrentHashMapstate, injectable clock,PII-free once-per-crossing
AppLogbreadcrumb) tracks per-account, per-day download bytes.MailRepositoryImpl.prefetchMessage— the single funnel bothMailBackfillerandMailSyncer'sbackground prefetch already share — records bytes actually pulled over the network (body chars +
actually-downloaded attachment bytes, not cache hits) for Gmail accounts only.
MailBackfiller/MailSyncer'sprefetchIfEnabledeach consultisOverDailyBudgetonce peraccount before starting a prefetch batch and defer for the rest of the day once Gmail's budget is
reached. Header paging/sync is never gated, and interactive fetches (message open, attachment tap,
inline images) are never gated either — same interactive-priority principle #355/#360 already
apply elsewhere.
10k-messages-per-label: captured as a documented constant only, deliberately NOT wired into a
backfill stop condition — issue #12's full-history backfill is intentional and a real large mailbox
can exceed 10k messages, so treating this as a hard ceiling would silently truncate history for
exactly the users the feature is for.
How this composes with #360 / #356
AccountThrottleGate(#360) is unmodified — it still handles the reactive case (a realprovider throttle/lockout response);
ThrottleClassifier's existing generic patterns alreadymatch Gmail's real-world throttle text (e.g. "too many simultaneous connections"), so no
Gmail-specific classifier changes were needed either.
BackfillPacer(#356) is unmodified — it still paces slice cadence, not bytes.GmailBandwidthTrackeris a new, orthogonal, proactive mechanism — the same relationshipInteractiveImapGatedocuments having toAccountThrottleGate: composes with the other two,doesn't duplicate or replace either.
Shared files touched (for conflict-awareness with #362/#363/#364)
Kept additive and Gmail-scoped throughout — no changes to
MailProvider.kt,AccountThrottleGate.kt,BackfillPacer.kt,ThrottleSignal.kt/ThrottleClassifier.kt/ThrottleBackoff.kt,InteractiveImapGate.kt, orImapClient.kt/ImapConnectionCache.kt. Files touched that a siblingprovider ticket could also want to touch:
MailBackfiller.kt/MailSyncer.kt— small additive change toprefetchIfEnabled(newGmail-only budget check + a new
GmailBandwidthTrackerconstructor param). Yahoo/iCloud connectioncaps are more likely to land in
ImapClient/ImapConnectionCacheinstead (untouched here), butflagging in case #362/#363 also touch these two files.
MailBackfiller.prefetchIfEnabledalso gained anaccount: Accountparameter (previously justids: List<String>) since the Gmail check needs the account to detect the provider.MailRepositoryImpl.kt— newbandwidthTracker: GmailBandwidthTrackerconstructor param;prefetchMessagenow records downloaded bytes;ensureAttachmentFile(private) now returns asmall
AttachmentFetch(file, downloadedBytes)instead of a bareFile(its 3 call sites updatedaccordingly).
MailRepository's public interface is unchanged. Unlikely to overlap with#362/#363 (no byte-budget in their issues); #364 (Outlook/Graph) would likely land in a separate
Graph REST client rather than this IMAP path, but flagging since Outlook accounts currently still
flow through
ImapClient/this same repository.config/detekt/detekt.yml— one appended line, excluding the newGmailBandwidthTrackerTest.ktfrom theandroid.util.Logforbidden-import rule (itmockkStatic(Log::class)the same wayAccountThrottleGateTest/BackfillPacerTestalready do).Tests
GmailSyncLimitsTest— documented constants, the connection-cap architecture invariant,appliesTofor Gmail (incl. the legacyimap.googlemail.comhost) vs. Yahoo/iCloud/AOL/Outlook.GmailBandwidthTrackerTest— accumulation, per-account isolation, day-rollover reset,threshold-crossing, no-op on non-positive bytes, PII-free once-per-crossing logging.
GmailBandwidthTrackerInstrumentedTest(new, mock-free, mirrorsBackfillPacerInstrumentedTest'sidiom) — concurrent-update correctness and account isolation on the real dispatcher/JVM, and
appliesToresolving the realMailProviderpresets on-device.MailBackfillerTest,MailSyncerTest,MailRepositoryImplTestcomposingthe new Gmail gating with the real backfill/sync/prefetch paths (incl. proving a non-Gmail account
is unaffected by an over-budget tracker entry for the same account id, and that header
paging/sync keeps running while prefetch is deferred).
Verification
Fast gate green locally (JDK 21):
assembleDebug,testDebugUnitTest,jacocoTestCoverageVerification,compileDebugAndroidTestKotlin,lintDebug,ktlintCheck,detekt. Per the dispatch instructions, local emulator E2E was skipped as flaky/non-authoritative —CI's full matrix is authoritative here.
Not armed for auto-merge; please review.
Merge Queue Status
2026-07-09 00:45 UTC· Rule:default· triggered by merge protections2026-07-09 01:15 UTC· atb1a7931dfac27eec4cb5cc195d3e02af66b8d5e0· mergeThis pull request spent 29 minutes 44 seconds in the queue, including 26 minutes 46 seconds running CI.
Required conditions to merge
-conflict-draftbase = maincheck-success = CI passedgithub-review-approved[🛡 GitHub repository ruleset rulemain]label != brokencheck-success = Debug buildcheck-neutral = Debug buildcheck-skipped = Debug buildcheck-success = Unit testscheck-neutral = Unit testscheck-skipped = Unit testscheck-success = CI passedcheck-neutral = CI passedcheck-skipped = CI passedmain]:check-success = @github-actions/CI passedcheck-neutral = @github-actions/CI passedcheck-skipped = @github-actions/CI passed@Mergifyio refresh
✅ Pull request refreshed