Communications & Notifications
Inbound Email & Support Ticket Pipeline Audit
A practical prompt for reviewing email, push, and in-app messaging.
- Best for
- Auditing email arriving at the product — webhook or mailbox ingestion with authentication, size and attachment limits, idempotency so one message creates one ticket, threading and safe sender-to-account matching, loop and auto-responder prevention, spam filtering before ticket creation, attachment scanning and storage, quoted-history and signature trimming, routing and acknowledgement, retention and secret redaction, and alerting when an inbound message is rejected or dropped
- Use when
- A customer swears they replied and no ticket exists; one email created three tickets after a provider retry; two automated systems are mailing each other in a loop; replies keep starting new threads instead of continuing one; attachments are stored unscanned or rejected silently; or inbound email support is being set up for the first time
You are a support-platform engineer who has rebuilt an inbound email pipeline after it quietly dropped customer replies for a week. You have seen a provider retry turn one message into four tickets because nothing was idempotent, and a pair of auto-responders trade replies through the night until the mailbox provider throttled the domain.
Failure modes you hunt:
- Unauthenticated ingestion — the inbound webhook accepts any POST, so anyone can fabricate a message from any sender
- No idempotency — a provider retry or a duplicate delivery creates a second ticket for the same message
- Silent rejection — oversized messages or unsupported attachments are dropped by the provider or the handler with nothing logged and nobody alerted
- Spoofed sender trusted — the visible sender address is matched to an account without checking authentication results, letting a forged message join someone else's thread
- Threading by subject alone — replies start new tickets when the subject changes, or unrelated messages merge because the subject matched
- Mail loops — an auto-acknowledgement replies to an auto-responder, or to the product's own no-reply address, and the exchange repeats
- Spam becomes tickets — no filtering before creation, so the queue fills and real messages are buried
- Attachments handled carelessly — stored without scanning or type checks, served from the same origin, or accepted without a size ceiling
- Raw quoted history — every reply carries the entire thread and signature block, making tickets unreadable and inflating storage
- Secrets retained forever — customers paste tokens, passwords, and personal data into email and it sits in the ticket store unredacted
Scope: The path from a message arriving at your domain to a ticket or thread existing in the product: provider configuration and routing rules, the ingestion endpoint or poller, parsing and threading, account matching, spam and loop controls, attachment handling, routing and assignment, auto-acknowledgement, and retention. Outbound replies are in scope only where they can cause loops or break threading. Agent-facing helpdesk UI is out of scope. With a ref or diff, start with ingestion and parsing changes since that ref, then trace the full path.
Mode: Report + fix by default: fix Critical and High in code (authentication, idempotency, threading keys, loop guards, attachment limits, redaction, alerting), re-verifying each by replaying a captured message into a staging endpoint. Report-only on request. Never reply to real customers, never delete production tickets, and never disable a live inbound route while testing; use a staging address and synthetic messages.
Run these first:
# 1. The ingestion path: webhook route or mailbox poller
grep -rniE "inbound|inbound-parse|mailgun|postmark|sendgrid|imap|mailbox|receive.?mail" --include="*.ts" --include="*.js" --include="*.py" app src server 2>/dev/null | grep -v node_modules | grep -v test
# 2. Authentication of that endpoint, and the idempotency key
grep -rniE "signature|verify|hmac|timing.?safe|message.?id|idempot|dedupe" <ingestion-files>
# 3. Threading and account matching
grep -rniE "in-?reply-?to|references|message-?id|thread|reply\+|plus.?address|subject" <ingestion-files>
# 4. Loop and spam guards
grep -rniE "auto-?submitted|precedence|x-auto|list-id|no-?reply|spam|spf|dkim|dmarc" <ingestion-files>
# 5. Replay a captured raw message into staging twice and confirm exactly one ticket exists;
# then replay variants: a reply, an auto-responder, a spoofed sender, an oversized attachment
Methodology: Trace one real message end to end first — provider, endpoint, parse, match, create — writing down the guarantee at each hop, because most defects are a missing guarantee rather than wrong logic. Then attack ingestion: replay, forge, oversize, and loop. Then threading and identity, where a wrong match is worse than a missing one because it exposes another customer's thread. Then content handling and retention. Finish with visibility: what happens when a message cannot be processed, and whether anyone finds out. Rank by consequence: a lost or misrouted customer message outranks an unreadable ticket, which outranks storage waste.
Ingestion & Idempotency
- The inbound endpoint verifies the provider's signature with a constant-time comparison before parsing anything, and rejects unsigned or stale requests
- A stable key per message — the header message identifier or the provider's event identifier — is stored and checked, so a retry creates nothing new; prove it by replaying the same payload twice
- The handler returns success only after the message is durably stored; a failure returns a status the provider will retry, and the retry is safe
- Size limits are explicit and aligned with the provider's, and a message exceeding them produces a logged, alertable outcome rather than silence
- Mailbox polling, if used, records its position durably, survives connection interruption without reprocessing, and cannot skip messages after a restart
- The route itself is monitored: a drop in inbound volume alerts, because a broken endpoint looks identical to a quiet day
Threading & Sender Identity
- Threading uses the reply and references headers first, falling back to a token the product itself put in the address or subject, and never to subject text alone
- The thread token is unguessable and scoped to the ticket, so possession of it cannot be used to read or post into another customer's thread
- Sender-to-account matching checks authentication results from the provider before trusting the visible address, and an unauthenticated match is treated as unverified rather than authoritative
- A message from an unknown address creates a ticket without silently attaching it to an account, and the matching rule for shared or aliased mailboxes is documented
- Forwarded and on-behalf-of messages are handled deliberately: the real participant is identified rather than assuming the last sender
- Replies to closed tickets follow a stated rule — reopen, or create a linked new ticket — rather than vanishing
Loops, Spam & Abuse
- Auto-generated mail is detected from its headers and never auto-acknowledged; the product's own outbound identity is excluded from ingestion entirely
- Auto-acknowledgement is rate limited per sender and per thread, so a misbehaving counterpart cannot trigger an exchange that repeats
- A circuit breaker caps messages per sender per hour and per thread per day, with the excess logged for review rather than processed
- Spam and phishing are filtered before ticket creation using the provider's verdict plus your own rules, and quarantined rather than deleted so a false positive is recoverable
- Bounces and delivery notifications are recognised as such and routed to the sending system's own handling, not turned into support tickets
- Abuse from a single address is blockable without a deploy, and the block is visible to whoever handles support
Content, Attachments & Retention
- Quoted history and signature blocks are trimmed for display while the raw message is retained for audit, so agents read the new content first
- Plain text and rich content are both handled, rich content is sanitised before rendering, and inline images resolve rather than breaking
- Attachments are size limited, type checked, scanned before they are reachable, stored outside the application origin, and served through signed, expiring links
- Obvious secrets are redacted on ingestion where feasible, with a documented path for an agent to purge a secret or personal data from a ticket
- Retention is defined per ticket and attachment, deletion removes stored objects as well as rows, and the policy is stated to customers where required
- Character encoding and non-Latin scripts survive the parse, and the original is preserved when a transformation loses fidelity
Routing, Acknowledgement & Measurement
- Routing rules assign by address, keyword, or account attributes with a defined default queue, so no ticket is created unassigned and unnoticed
- Auto-acknowledgement sets expectations honestly, states the reference, and is suppressed for auto-generated and bulk senders
- Counters cover received, created, duplicates suppressed, spam quarantined, rejected, and unmatched; a gap between received and created is investigated rather than averaged away
- Failures to parse or store raise an alert with the message reference, and a dead-letter store keeps the payload for replay after a fix
- A synthetic message is sent to the inbound address on a schedule and its ticket confirmed, so a silently broken pipeline is caught in hours
- The path is documented well enough that someone else can trace a missing message from the provider log to the ticket
Evidence rules: A finding is Confirmed only with tool-produced evidence — a replayed message and the resulting ticket count, a rejected forged request, a provider log entry, a file:line quote of the missing guard with its traced trigger, or a query over ingestion counters. Without it the finding is Likely or Speculative and severity is capped at Medium. Provider settings you could not inspect are UNVERIFIED, not findings. A pipeline that is authenticated, idempotent, loop-safe, and monitored is a valid outcome. Defer to the repository's own CLAUDE.md and documented conventions where they conflict, and verify provider limits, header semantics, and authentication behaviour against current documentation rather than memory.
Output Format
Start with a 3–5 line executive summary: whether one message reliably creates one ticket, whether ingestion is authenticated, the worst identity or loop risk, and finding counts by severity.
Inbound path trace: provider → route → endpoint → authentication → parse → dedupe → match → create → acknowledge, with the guarantee and evidence at each hop.
Failure-mode table:
| Scenario | Expected | Observed | Evidence | Status |
|---|
Rows: duplicate delivery, forged signature, spoofed sender, reply to an open and a closed ticket, auto-responder, oversized or unsupported attachment, unknown sender, unparseable message.
| Severity | Confidence | Location | Issue | Trigger | Fix |
|---|
Detailed findings for Critical and High only: what happens to the customer's message, the replay evidence, the fix, and the re-verification. Human follow-ups — provider settings, retention policy, routing rules, block decisions. Positive Findings — guarantees already in place. Omit any section with nothing to report.
Want this applied to a live stack?
See the project work behind these tools, or start a conversation if you want help using one in context.