perf(icloud): respect iCloud Mail IMAP connection & message-size limits

Adds two iCloud-specific, provider-scoped policies that build on the
existing shared throttling framework (#360's AccountThrottleGate, #356's
BackfillPacer) without refactoring either:

- IcloudConnectionLimiter: a per-account permit gate capping an iCloud
  account at 5 simultaneous connections (Apple documents 5-8; pinned to
  the conservative low end). Wired into MailBackfiller around both
  connection-opening call sites (the header-page fetch and the
  body/attachment prefetch loop - the "1 + K + attachments" per-page
  connection count issue #363 describes). A no-op passthrough for every
  other provider, so it composes cleanly with #360's reactive backoff
  (already consulted first, per account) and #356's slice pacing
  (BackfillWorker composes BackfillPacer.runPaced around
  MailBackfiller.runBackfill, so the cap sits one layer beneath the
  pacer's cooldown/cap in the same call graph).

- IcloudSendLimits: enforces Apple's ~20 MB outgoing message-size cap
  before SmtpSender ever opens a connection, estimating the actual
  encoded wire size (base64 inflates binary attachment bytes by ~4/3)
  rather than comparing raw file bytes, mirroring GraphSender's existing
  pre-send attachment-size guard. An over-cap send throws
  MessageTooLargeException, caught by SendWorker's existing runCatching
  and turned into a clean, PII-free outbox error - no crash, no raw
  provider rejection.

Both are new, self-contained files kept intentionally separate from a
shared cross-provider table, per the parallel-safety note on this ticket
(sibling issues #361/#362/#364 add their own provider's limits the same
way).

Touches two shared files: MailBackfiller.kt (new constructor dependency
+ two call sites wrapped in icloudConnectionLimiter.withPermit) and
SendWorker.kt (one guard call before smtpSender.send). Both are
minimal, additive edits — flagged for conflict-awareness with the
sibling provider tickets.

Also excludes MailBackfillerTest.kt from detekt's LargeClass rule
(config/detekt/detekt.yml), mirroring the existing MailRepositoryImplTest
exclusion: one cohesive single-SUT suite tipped over the LLOC boundary
by the new connection-cap wiring tests.

Closes #363
This commit is contained in:
2026-07-08 19:31:03 -05:00
parent d7429dfef0
commit 8a70b329ab
12 changed files with 805 additions and 7 deletions
+6 -1
View File
@@ -28,7 +28,12 @@ complexity:
# logging (issue #358) added its required android.util.Log mock + one breadcrumb test, tipping it
# over. Excluded rather than artificially split — same "operation-rich cohesive suite" rationale as
# the TooManyFunctions relaxation above.
excludes: ['**/data/repository/MailRepositoryImplTest.kt']
#
# MailBackfillerTest is the same pattern: one cohesive single-SUT suite (a test per backfill concern
# — #12 core paging, #94/#95 boundary edge cases, #322 batching, #360 throttle, #355 interactive
# priority, #329 logging) already at the boundary; the iCloud connection-cap wiring tests (issue
# #363) tipped it over.
excludes: ['**/data/repository/MailRepositoryImplTest.kt', '**/data/sync/MailBackfillerTest.kt']
naming:
FunctionNaming: