A reply event must be safe to process twice

Queues retry, IMAP connections drop, and processes crash between database writes. Reply detection should assume the same inbound message can be delivered to the processor more than once. The second pass must not create a duplicate reply record, cancel an already-cancelled sequence in a conflicting way, or resurrect a follow-up. Idempotency is therefore part of recipient safety: without it, infrastructure retries can change outbound behavior even though the human sent only one message.

Choose a durable inbound identity

Store the mailbox identity, UIDVALIDITY, and UID for IMAP processing, and also retain Message-ID when available for mail-level correlation. Build an idempotency key from stable fields appropriate to the connector. Do not use sequence number or polling timestamp. If the same reply is observed again, the worker should find the existing processed event and return its prior outcome. Keep the raw header identifiers so ambiguous duplicates can be audited later.

Apply recipient-state changes atomically

The dangerous window is between “reply stored” and “future sends cancelled.” Use a transaction or compare-and-set so the application can record the reply event and move the contact into a replied/paused state consistently. If the queue system is separate, write an outbox or cancellation task tied to the same durable event and make that task idempotent too. A crash should result in a retry that completes missing work, not a state where the database says replied while a follow-up still fires.

Classify before applying irreversible business meaning

Not every inbound message is a positive human reply. Delivery reports, out-of-office messages, automated acknowledgements, and human responses need different state. The idempotent core should store the event first, then apply a classification that can be reviewed or corrected. A hard bounce routes to suppression; a human negative reply stops outreach; an out-of-office may pause until a return window. Separating event identity from classification lets corrections happen without pretending the message arrived twice.

Re-check state at send time even with perfect reply processing

Inbound processing can still be delayed by provider polling or outages. Every outbound worker should perform a final recipient-state read immediately before send. This creates a second safety layer: once the reply processor catches up, queued follow-ups are skipped even if they were scheduled earlier. It also prevents a queue restore from reviving old jobs. Idempotent inbound and send-time eligibility checks reinforce each other.

Test the crash boundaries deliberately

Write tests that crash after discovery, after storing the event, after classification, and after queue cancellation. Then run the same inbound message again and confirm the final state is identical to a clean one-pass execution. Also test two workers receiving the same UID concurrently. If database uniqueness causes one to lose the race, it should read the winner's result and exit cleanly rather than treating the conflict as a reason to repeat side effects.

Keep an audit trail that explains why a follow-up disappeared

When a salesperson asks why the next touch was cancelled, the system should point to the inbound message, classification, processing time, and state transition. Idempotency can otherwise feel invisible because “nothing happened” on the duplicate pass. A clear event log proves that duplicate processing was intentionally suppressed and helps diagnose edge cases such as two real replies arriving close together.

Use the inbound message as an event with a unique processing key

Create a processing key from mailbox identity, UIDVALIDITY and UID, and store it in a table with a uniqueness constraint. When the detector sees the same inbound message again after reconnect or retry, the insert conflicts and the handler can return without repeating side effects. Keep Message-ID as a secondary diagnostic field because it is useful for threading and cross-mailbox analysis, but the mailbox UID tuple is better for the event that actually arrived in this account. The handler should then update contact state and cancel follow-ups inside one transaction or other atomic boundary so a crash cannot leave “reply processed” true while a scheduled message remains active.

Make automatic responses idempotent without treating them as human intent

Out-of-office and other automatic replies still need durable processing because they can arrive repeatedly, but they should not necessarily produce the same state change as a human response. RFC 3834 provides the Auto-Submitted field as one signal that a message was generated automatically. Store the classification result alongside the idempotency key. A genuine human reply can stop the sequence immediately; an automatic response might pause until a stated return date, route to manual review, or simply be logged depending on policy. If the same automatic message is fetched twice, the unique event key prevents duplicate calendar changes or repeated notifications. Idempotency and semantic classification solve different problems and both are required.

Cancel scheduled sends with a compare-and-set guard

When a human reply changes a contact from active to replied, update the campaign state only if it is still in a sendable state, then cancel or invalidate pending jobs using the same version or state token. A worker that already claimed the next follow-up must re-check that token immediately before submission. This compare-and-set pattern closes the race where reply processing succeeds but a send worker is already awake. The inbound event remains idempotent, and the outbound worker becomes state-aware at the final boundary. Together those controls make “reply arrived before send” a deterministic stop condition instead of a timing lottery.

Keep a dead-letter path for messages the classifier cannot map

Not every inbound message will map cleanly to a campaign contact. Instead of dropping ambiguous replies or repeatedly reprocessing them, store them in a dead-letter or manual-review queue with mailbox identity, UID, Message-ID, sender and classification notes. Mark the inbound event as observed so the detector remains idempotent, but do not apply campaign state until the mapping is resolved. This prevents an unusual alias, forwarded reply or malformed header from causing either duplicate processing or silent loss of a potentially important stop signal.

Field checklist

  • Use a stable idempotency key; never sequence number or poll time.
  • Persist IMAP UID identity and Message-ID where available.
  • Apply reply event/state changes transactionally or with compare-and-set.
  • Keep event identity separate from reply classification.
  • Re-check recipient eligibility immediately before outbound send.
  • Test retries and crashes at every boundary where side effects occur.

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.
  2. RFC 3834 — Automatic Responses to Electronic MailIETF / RFC EditorAuto-Submitted semantics for distinguishing automatic responses from human replies.