How to Receive Inbound Email With Amazon SES
SES has no inbound webhook. Here is the pipeline you have to build instead — and the three things that bite everyone.
Most email APIs hand you a parsed inbound webhook. Amazon SES does not. SES receiving drops raw MIME into an S3 bucket and tells you it happened. Everything after that is yours to build. This guide covers the whole path, plus the failure modes that are not in the AWS documentation.
The Pipeline
- MX record points your domain at SES for the region you are receiving in
- Receipt rule matches recipients and writes the raw message to S3
- SNS or SQS notifies you that an object landed
- Worker long-polls the queue, fetches the object from S3, and parses the MIME
- Your application turns the parsed message into whatever it needs
A queue in the middle is worth it. Parsing inline in a Lambda triggered directly by S3 works until a malformed message or a 30 MB attachment takes the invocation down, and then you have lost the mail with no retry and no visibility.
Step 1: MX and the Receipt Rule
Point MX at the SES inbound endpoint for your region, then create a receipt rule set with a rule that has an S3 action. Two things people miss:
- The bucket policy must allow SES to write. The rule silently fails to save otherwise.
- A rule set must be active. You can have several; only one receives. Creating a rule in an inactive set produces a pipeline that looks correct and receives nothing.
Receiving works while your account is still in the SES sandbox. Sandbox restricts sending to verified addresses — inbound is unrestricted, so you can build and test the whole receive path before production access is approved.
Step 2: Parse the MIME
You get an RFC 5322 message, not JSON. Use a real parser — zbateson/mail-mime-parser in PHP, Python's email package, mailparser in Node. Do not reach for regex; multipart bodies, transfer encodings, and internationalised headers will defeat it.
Pull out what you need and normalise it early:
From,To,Cc,Reply-To,Subject- Text and HTML bodies — expect messages that have only one of the two
Message-ID,In-Reply-To,Referencesfor threading- Attachments, with a hard size cap enforced while streaming, not after
Normalising into a single value object at the boundary is worth the effort. Everything downstream then works on one shape rather than on provider-specific quirks.
Trap 1: SES Rewrites the Message-ID
This is the one that costs people a weekend.
The obvious way to thread replies is to store the Message-ID of what you send, then match an inbound In-Reply-To against it. That works on providers that preserve the ID. SES replaces it with its own. Your stored ID and the ID the recipient's mail client replies to are different strings, the match fails, and every reply opens a new record instead of continuing the thread.
Build threading in layers so no single one has to be right:
- Inject your own reference. Put a stable token of your own — say
<[email protected]>— intoReferenceson everything you send. Mail clients echoReferencesback, so this survives the rewrite. Sign it — a bare sequential id is guessable, and the consequence is a data leak rather than a threading bug; see Your Email Threading Anchor Is Forgeable. - Prefix-match the provider ID. Store the ID SES returns and match inbound references against it by prefix.
- Heuristic fallback. Sender address plus normalised subject plus a recency window catches clients that mangle headers entirely.
Scope every layer to the receiving account or tenant. An unscoped lookup on a non-unique ID will eventually attach a stranger's reply to the wrong thread — a quiet data leak rather than a visible bug.
Scoping is necessary but not sufficient. If the token you inject is a sequential integer, anyone who has received one of your emails can put a different one in their own References and land inside another customer's thread — inside the correct tenant, so scoping does not catch it. Signing the anchor is what closes that.
Trap 2: Delivery Is At-Least-Once
SQS can hand you the same message twice, and SES can redeliver. Without deduplication you get duplicate records that look exactly like a customer sending twice.
Deduplicate on the sender's original Message-ID over a window — 48 hours is generous and cheap. Scope the key to the recipient as well as the ID: the same message legitimately delivered to two of your addresses is two real copies, not a duplicate, and a global key silently drops the second.
Trap 3: The Verdict Headers Are Free Signal
SES stamps its own assessment onto the message before storing it. Read these rather than building your own filter:
X-SES-Spam-VerdictandX-SES-Virus-Verdict- SPF, DKIM and DMARC results inside
Authentication-Results
A DMARC failure is worth quarantining outright — it is the signature of someone spoofing a sender to slip into an existing thread. SPF or DKIM failing alone is weaker evidence, because legitimate forwarders break SPF routinely; treat it as a strong spam signal rather than grounds to reject.
Do Not Forget Bounces
Receiving is only half the loop. Subscribe an SNS topic to bounce and complaint notifications for your sending identity and suppress addresses that hard-bounce. On SES the reputation is yours, and unattended bounce rates are the fastest route back into the sandbox. Verify the SNS signature on those webhooks — the endpoint is public.
A Working Checklist
- MX pointed at the right regional endpoint
- Receipt rule in an active rule set, with a bucket policy permitting SES
- Queue between S3 and your parser, with a dead-letter queue behind it
- Real MIME parser, streaming attachment cap
- Layered threading that survives the Message-ID rewrite
- Deduplication keyed on message ID and recipient
- Verdict headers honoured; DMARC failures quarantined
- Bounce and complaint handling with suppression
- Alerting on the dead-letter queue — mail that fails every retry is mail a customer thinks you received
Would rather not build any of this? GoPimi runs the pipeline as a product — inbound mail becomes a ticket, replies thread correctly across clients, and bounces are suppressed automatically. See the Email Pipeline guide, or compare providers first in Mailgun vs Amazon SES.