fix(sync): bound the foreground fetch window by the age cutoff in age retention #253

Merged
JMR-dev merged 6 commits from fix-193-age-retention-sync-window into main 2026-07-03 21:08:42 +00:00
JMR-dev commented 2026-07-03 18:17:32 +00:00 (Migrated from github.com)

Fixes a re-download/re-prune churn loop in age-based retention (issue #193) — an efficiency bug, not data loss.

Problem

MailSyncer fetches the newest-N headers and capped that window by the retention count only. In age mode on a low-traffic mailbox, the newest-N extend older than the age cutoff, so each sync re-inserts messages the age pruner just deleted → the next prune deletes them again. Every cycle: wasted DB writes + prune deletes (and IMAP header fetches).

Fix

  • Sync now drops fetched messages older than policy.ageCutoffMillis(now) before persisting — the same cutoff MailPruner uses — so sync and prune keep exactly the same set. Count/unlimited modes have a null cutoff and are unchanged.
  • The empty-folder wipe is keyed on the raw fetch (server truth), not the age-filtered set, so a folder holding only past-cutoff mail is left to the pruner instead of wiped. An empty filtered set is otherwise a harmless no-op through the insert path.
  • MailPruner KDoc now documents that foreground sync is aligned in both retention modes.

Tests

  • New: age retention doesn't re-insert fetched messages older than the cutoff (only the in-window one is persisted).
  • Existing count-mode window-cap + "age-only leaves the full window" tests still pass (fetch count unchanged).
  • Preflight green (build, unit tests, androidTest compile, lint, ktlint, detekt).

Closes #193

🤖 Generated with Claude Code

Fixes a re-download/re-prune **churn loop** in age-based retention (issue #193) — an efficiency bug, not data loss. ## Problem `MailSyncer` fetches the newest-N headers and capped that window by the retention **count** only. In **age** mode on a **low-traffic mailbox**, the newest-N extend older than the age cutoff, so each sync re-inserts messages the age pruner just deleted → the next prune deletes them again. Every cycle: wasted DB writes + prune deletes (and IMAP header fetches). ## Fix - Sync now drops fetched messages older than `policy.ageCutoffMillis(now)` **before persisting** — the *same* cutoff `MailPruner` uses — so sync and prune keep exactly the same set. Count/unlimited modes have a `null` cutoff and are **unchanged**. - The empty-folder wipe is keyed on the **raw fetch** (server truth), not the age-filtered set, so a folder holding only past-cutoff mail is left to the pruner instead of wiped. An empty filtered set is otherwise a harmless no-op through the insert path. - `MailPruner` KDoc now documents that foreground sync is aligned in **both** retention modes. ## Tests - New: age retention doesn't re-insert fetched messages older than the cutoff (only the in-window one is persisted). - Existing count-mode window-cap + "age-only leaves the full window" tests still pass (fetch count unchanged). - Preflight green (build, unit tests, androidTest compile, lint, ktlint, detekt). Closes #193 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.