feat(sync): default fetch-all history + device-only retention (#12, #13)

Replace the fixed 50-message-per-folder header cap with a background,
resumable full-history backfill, and add a user-configurable device-only
retention limit that prunes local mail beyond it without ever deleting from
the server.

- ImapClient.fetchOlderThan pages a folder backwards in bounded batches,
  locating the boundary by binary search over message numbers (O(log n) tiny
  UID fetches, memory bounded to one batch).
- MailBackfiller + BackfillWorker page each synced folder newest→oldest,
  persisting a per-folder boundary in a new backfill_progress table so a run
  interrupted by process death / network loss resumes exactly where it stopped.
  Runs off the sync mutex, so foreground sync / pull-to-refresh stay responsive.
- MailSyncer now reconciles server deletions only within the recent UID window
  (deleteSyncedInWindowNotIn) instead of wiping everything outside the recent
  50, so backfilled history survives each foreground sync. A materialized
  messages.uid column powers the windowed reconcile and backfill boundary.
- Body/attachment prefetch still honours FetchPolicy (headers first).

- Per-account count/age overrides (nullable) with a global default; 0 = keep
  everything (the default, matching #12).
- MailPruner + PruneWorker delete local rows beyond the limit (cascading
  attachment rows + on-disk cache), never issuing a server delete. Deletes are
  chunked under SQLite's 999-parameter limit.
- Precedence with backfill: backfill pauses (does not complete) at the
  retention floor and both jobs share a maintenance mutex, so they never
  contend; foreground fetch is also capped by the count so it can't re-download
  what pruning just trimmed.
- Settings UI for the global default and per-account override, with copy making
  clear it is device-only, not the server.

Room schema v9→v10 (migration + exported schema + MigrationTestHelper test).
GreenMail tests prove the backfill caches >50 and resumes after interruption;
pruning tests cover count/age limits and never touch the server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 13:17:22 -05:00
co-authored by Claude Opus 4.8
parent 4175b3f440
commit bad597bc42
41 changed files with 2362 additions and 39 deletions
+16
View File
@@ -196,6 +196,22 @@
<string name="settings_backup_include">Include settings in Android Backup</string>
<string name="settings_backup_include_summary">Let Android back up your LibreMail preferences (Google Auto Backup) so they restore when you set up a new device. Your mail, accounts, passwords, and encryption keys are never backed up — only app settings. Off by default; uses Google infrastructure.</string>
<!-- Storage / retention (device-only; issue #13) -->
<string name="settings_retention">Storage on this device</string>
<string name="settings_retention_summary">These limits apply to this device only. Mail beyond them is removed from local storage but never deleted from the server, so it can always be downloaded again. By default LibreMail keeps everything.</string>
<string name="settings_retention_default">Default for all accounts</string>
<string name="retention_count_title">Keep by message count</string>
<string name="retention_age_title">Keep by age</string>
<string name="retention_use_default">Use the global default</string>
<string name="retention_keep_all">Keep everything</string>
<string name="retention_count_500">Newest 500 per folder</string>
<string name="retention_count_1000">Newest 1,000 per folder</string>
<string name="retention_count_5000">Newest 5,000 per folder</string>
<string name="retention_age_3m">Last 3 months</string>
<string name="retention_age_6m">Last 6 months</string>
<string name="retention_age_1y">Last year</string>
<string name="retention_age_2y">Last 2 years</string>
<!-- Per-account settings -->
<string name="settings_account_title">Account</string>
<string name="settings_signature">Signature</string>