Deletion in IMAP is a state transition before it is removal

A typical IMAP deletion workflow marks messages with the `\Deleted` flag and then uses expunge semantics to remove messages from the selected mailbox. That distinction matters for automation: a worker should know exactly which UIDs are eligible before it changes flags, and it should understand what the server will expunge. Treat bulk cleanup as a data operation with a candidate phase, mutation phase, and verification phase rather than as one giant “delete 2,400” command.

Select candidates with durable identifiers and explicit rules

Build the candidate set using UIDs plus clear criteria such as folder, age, known warmup tag, or application-owned Message-IDs. Store the count and a sample before mutation. Avoid using current sequence ranges like `1:2400` as the only eligibility definition because mailbox contents can change. If the cleanup is intended only for old warmup traffic, the selection should be able to prove that a real prospect reply is not part of the set.

Use bounded batches to control provider load and rollback scope

Large mailbox mutations can time out, hit provider limits, or make error recovery ambiguous. Process bounded UID batches—for example, dozens or a few hundred depending on observed provider behavior—then verify progress. The exact batch size is operational, not universal. Start smaller, measure command latency and errors, and adapt. If a provider slows dramatically after several batches, pause rather than launching more parallel workers that create additional pressure.

Understand the expunge behavior your library invokes

Some APIs expose broad EXPUNGE behavior while others support UID-scoped approaches when the server capability allows it. Read the library and server capability details before assuming only your target set will be removed. If the selected mailbox already contains unrelated messages marked `\Deleted` by another client, a broad expunge can have wider effects than the cleanup job intended. Destructive automation should either control that state or fail closed when it cannot prove scope.

Make the job idempotent across crashes

A worker can crash after flagging a batch but before recording completion. On restart, it should be able to rediscover which candidates remain and continue without touching new mail. Persist a cleanup job identifier, the candidate UID set or query boundary, and progress. Re-running a completed batch should not create a new interpretation of “old messages” based on the current clock; otherwise retries can slowly widen the deletion scope.

Verify counts and spot-check after each phase

After flagging, inspect the target UIDs and confirm their state. After expunge, confirm they are gone and that a small set of protected messages remains. Log before/after mailbox counts, but do not rely on counts alone because new mail can arrive concurrently. A protected-message fixture—a known reply that must survive—is useful during testing and can catch a selection rule that is too broad.

Prefer archive or move when uncertainty is high

If the cleanup rules are new or the provider's expunge semantics are unclear, moving candidate messages to a dedicated quarantine/archive folder can be safer than immediate permanent removal. Observe the result, then delete later when confidence is high. For a solo operator, storage cost is usually cheaper than reconstructing a prospect conversation that a bulk job deleted accidentally. Destructive speed is not a deliverability advantage.

Separate marking from permanent removal

In IMAP, setting the \Deleted flag and expunging are distinct operations. Use that separation to create a safety boundary. First select candidate UIDs under explicit rules, mark a bounded batch, and record the set the job intends to remove. Then verify that the count and sampled subjects match expectation before invoking the operation that permanently expunges messages. Avoid broad sequence-number ranges because expunges can renumber the remaining mailbox view. If the library supports UID-based expunge semantics, prefer them for a worker that persists state. The principle is to make permanent removal the final confirmed step, not a side effect hidden inside a generic “delete” helper.

Design the cleanup job to survive a crash between phases

A safe cleanup worker can be restarted without deleting a different set of messages. Persist the batch identifier and target UIDs before changing mailbox state. If the process crashes after setting \Deleted but before expunge, the next run should detect the existing marks and decide whether to complete or roll back according to policy; it should not simply select the next 500 sequence numbers. After expunge, verify that the intended UIDs are gone and that protected folders or recent reply messages remain. Small batch sizes make this audit practical and limit damage if a provider behaves differently from the test account. When uncertainty is high, moving to a known archive folder is safer than irreversible deletion.

Protect recent replies from cleanup by rule, not folder position

Cleanup jobs should exclude recent messages, unread replies and any UID currently referenced by active campaign state. Do not assume that “the first 5,000 messages” or “everything below sequence number 2000” is old and disposable because sequence positions change as the mailbox is modified. Select candidates using dates, flags and durable UIDs, then cap the batch size. This lets reply detection and cleanup coexist without one process deleting evidence the other process still needs. A conservative retention rule costs mailbox space; an aggressive positional rule can cost the reply that should have stopped an outbound sequence.

Set a maximum destructive batch size independent of mailbox size

Even if the mailbox contains hundreds of thousands of messages, put a hard ceiling on how many can be permanently removed in one job. The ceiling should be small enough that a human can inspect the batch summary and that a mistaken selector cannot erase an entire history before the next alert fires. Large cleanup can still finish through repeated confirmed batches. This rate limit is a safety property rather than a performance optimization: it bounds the damage from a coding error, stale folder mapping or unexpected provider response.

Field checklist

  • Select cleanup candidates by durable UIDs and explicit criteria.
  • Process bounded batches and observe provider latency/errors.
  • Understand whether expunge can remove unrelated `\Deleted` messages.
  • Persist cleanup job scope so retries are idempotent.
  • Verify target and protected-message state after each phase.
  • Use archive/quarantine before permanent deletion when scope is uncertain.

Primary sources

Standards and provider policies can change. These links are the reference points used for this field note.

  1. RFC 9051 — IMAP4rev2IETF / RFC EditorMailbox flags, UIDs, BODY.PEEK and IMAP4rev2 behavior.