Email Sending Pipeline
When a user sends an email, the system separates the act of submitting the email from the act of delivering it. This keeps the JMAP API responsive and ensures delivery is retried reliably even if something fails after the user’s request completes.
Overview
Section titled “Overview”graph LR Client["JMAP client"] API["JMAP API<br/>EmailSubmission/set"] SQS["SQS Queue<br/>email-submissions"] DLQ["SQS DLQ<br/>(14 day retention)"] Lambda["Lambda<br/>email-sender"] SES["Amazon SES"] Internet["Recipient's<br/>mail server"]
Client -->|"POST /jmap"| API API -->|"enqueue"| SQS SQS -->|"max 5 retries"| DLQ SQS --> Lambda Lambda --> SES SES --> InternetThe JMAP API creates the submission record, enqueues it to SQS, and returns immediately. The
email-sender Lambda processes the queue independently.
Step 1 — The JMAP submission
Section titled “Step 1 — The JMAP submission”A client sends an email by calling EmailSubmission/set with a create operation. This requires
two things to already exist:
- An Email object in the Drafts mailbox (the MIME content, stored in S3)
- An Identity — the verified sending address to use as the
From
The API creates a submission record that links the two, then enqueues it for sending. Submission
status moves to queued on successful enqueue.
The client can also provide:
- A
sendAttime to schedule future delivery (within SQS’s 15-minute delay limit) - An
envelopeoverride to control the SMTPMAIL FROMandRCPT TOaddresses independently of the message headers - A
messageClass(transactional,mailing_list, orbulk) used for suppression list scoping
Step 2 — What the sender Lambda does
Section titled “Step 2 — What the sender Lambda does”graph TD A["Submission arrives from SQS"] B["Fetch MIME from S3"] C["Validate identity &<br/>resolve envelope"] D["Apply user signature<br/>(server-side)"] E["Tag the message<br/>(SES tags)"] F["Send via SES"] G["Move Drafts → Sent"] H["Mark submission as sent"]
A --> B --> C --> D --> E --> F --> G --> HFetch MIME from S3
Section titled “Fetch MIME from S3”The message body is not stored in the SQS message — only the submission record is. The Lambda looks up the Email record to find its S3 key and downloads the content. This keeps queue messages small.
Apply signature server-side
Section titled “Apply signature server-side”If the user has a signature configured on the sending identity, it is injected into the MIME at send time — not at compose time. This means the draft in the user’s Drafts folder stays clean (no signature), and the outgoing message gets the current signature regardless of when the draft was written.
Text and HTML parts are each updated independently. For HTML, the signature is inserted before
</body> if present, or appended at the end.
Tag the message
Section titled “Tag the message”Before calling SES, the Lambda attaches metadata tags to the message:
| Tag | Purpose |
|---|---|
tenant_id | Routes feedback events back to the correct tenant |
submission_id | Links bounce/complaint/delivery events to this submission |
identity_id | Tracks which sending identity was used |
account_id / user_id | For audit and suppression records |
domain | The sending domain, for reputation analytics |
message_class | Scopes suppression list entries correctly |
These tags travel with the message through SES and appear on every feedback event (bounce, complaint, delivery). Without them, the system would not know which submission a bounce refers to.
Send via SES
Section titled “Send via SES”The full message is handed off to SES for delivery. This is what activates the feedback event pipeline — every bounce, complaint, and delivery notification flows back through the feedback pipeline.
Draft → Sent transition
Section titled “Draft → Sent transition”Immediately after a successful SES call, the Lambda moves the email from the Drafts mailbox to
the Sent mailbox and removes the $draft keyword. This is best-effort — if it fails, the email
was still sent, and the failure is logged rather than propagated.
Error handling
Section titled “Error handling”Not all SES errors are equal:
| Error type | Examples | Action |
|---|---|---|
| Retryable | Throttling, rate limits, 5xx server errors | Return error — SQS retries up to 5 times, then DLQ |
| Permanent | Invalid parameters, identity not verified, no envelope | Mark submission as failed; no retry |
Permanent failures write a failed status back to the submission record so the sender can see what went wrong. The email remains in Drafts.
Submission lifecycle
Section titled “Submission lifecycle”A submission moves through these states:
graph LR submitted --> queued queued --> sent queued --> failed sent --> delivered sent --> bounced| Status | Meaning |
|---|---|
submitted | Created by the JMAP API |
queued | Successfully placed on the SQS queue |
sent | SES accepted the message |
failed | Permanent error before or during sending |
delivered | Recipient’s server confirmed receipt (via SES delivery event) |
bounced | Delivery failed after sending (via SES bounce event) |
The transition from sent to delivered or bounced happens asynchronously via the
SES feedback pipeline.
Related topics
Section titled “Related topics”- Bounce & Complaint Handling — how SES feedback events update submission status and protect sender reputation.
- Email Processing Pipeline — the inbound counterpart to this pipeline.
- Security Model — DKIM signing and identity verification.