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).
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)
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.
Fixes a re-download/re-prune churn loop in age-based retention (issue #193) — an efficiency bug, not data loss.
Problem
MailSyncerfetches 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
policy.ageCutoffMillis(now)before persisting — the same cutoffMailPruneruses — so sync and prune keep exactly the same set. Count/unlimited modes have anullcutoff and are unchanged.MailPrunerKDoc now documents that foreground sync is aligned in both retention modes.Tests
Closes #193
🤖 Generated with Claude Code