Skip to content

Email Processing Pipeline

When an email arrives at a Serverless Inbox domain it passes through a chain of AWS services before it lands in a user’s mailbox. This page explains that chain and the design choices behind it.

Scope: This page covers inbound email receiving only. Outbound sending and its feedback loop (bounces, complaints, delivery notifications) are covered separately.


The receiving path is built on three AWS services acting as a reliable, ordered pipeline: SES → SNS → SQS → Lambda.

graph LR
Internet["Internet (SMTP)"]
SES["Amazon SES<br/>Receipt Rules"]
S3_raw["S3<br/>incoming-emails/"]
SNS["SNS Topic"]
SQS["SQS Queue"]
DLQ["SQS DLQ<br/>(14 day retention)"]
Lambda["Lambda<br/>email-processor"]
Internet -->|"SMTP (TLS required)"| SES
SES -->|"1. Store raw MIME"| S3_raw
SES -->|"2. Publish notification"| SNS
SNS --> SQS
SQS -->|"max 3 retries"| DLQ
SQS -->|"batch size 10"| Lambda

SES first writes the full email to S3, then publishes a lightweight notification — so the raw message is always durably stored before any processing begins.

SES receipt rules cannot publish directly to SQS. SNS provides the fan-out point: the same notification can reach multiple subscribers without changing the SES rule. In practice there is one subscriber today, but the topology is ready to add more (for example, a search indexer) without touching SES configuration.

ConcernMechanism
Transient Lambda failuresSQS visibility timeout causes automatic redelivery
Persistent failuresDead-letter queue after 3 attempts; 14-day retention
Partial batch failuresOnly failed messages in a batch are retried, not the whole batch
Data loss on cold startRaw MIME is in S3 before Lambda is ever invoked

The Lambda receives a batch of notifications and processes each one independently:

graph TD
A["Notification arrives"]
B["Identify recipients"]
C["Download MIME from S3"]
D["For each recipient:<br/>route to inbox or junk"]
E["Store email<br/>(S3 + DynamoDB)"]
F["Notify connected clients<br/>(WebSocket push)"]
A --> B --> C --> D --> E --> F

SES delivers email to an entire domain — it does not know which tenant or account owns a given address. The processor resolves each destination address by looking it up in the database.

A few things happen during this lookup:

  • Plus-addressinguser+tag@domain.com is treated as user@domain.com for delivery purposes.
  • Alias resolution — every address is an alias pointing to an account. Inactive aliases are silently skipped.
  • Deduplication — if multiple envelope addresses resolve to the same account, only one copy of the email is stored.

If an address cannot be resolved, that recipient is skipped. No delivery failure is generated by the processor itself; SES may generate a bounce at the SMTP layer.

Once recipients are resolved, SES’s spam verdict determines where the email lands:

  • Spam detected → placed in the junk mailbox (or inbox with the $junk keyword if no junk mailbox is configured), per RFC 8621 §9.
  • Everything else → inbox.

DKIM, SPF, and DMARC results are recorded on the email and visible in the UI, but they do not currently affect routing.

Each recipient gets their own copy of the email stored in per-account S3 storage, plus a record in DynamoDB. Mailbox unread counters are updated atomically. After all recipients are processed, the original raw object in the incoming-emails/ S3 prefix is deleted as a best-effort cleanup.

At the end of the batch, any accumulated change notifications are pushed to connected JMAP clients via WebSocket. This is what makes new mail appear in the UI without a page refresh.