Skip to content

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.


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 --> Internet

The JMAP API creates the submission record, enqueues it to SQS, and returns immediately. The email-sender Lambda processes the queue independently.


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 sendAt time to schedule future delivery (within SQS’s 15-minute delay limit)
  • An envelope override to control the SMTP MAIL FROM and RCPT TO addresses independently of the message headers
  • A messageClass (transactional, mailing_list, or bulk) used for suppression list scoping

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 --> H

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.

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.

Before calling SES, the Lambda attaches metadata tags to the message:

TagPurpose
tenant_idRoutes feedback events back to the correct tenant
submission_idLinks bounce/complaint/delivery events to this submission
identity_idTracks which sending identity was used
account_id / user_idFor audit and suppression records
domainThe sending domain, for reputation analytics
message_classScopes 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.

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.

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.


Not all SES errors are equal:

Error typeExamplesAction
RetryableThrottling, rate limits, 5xx server errorsReturn error — SQS retries up to 5 times, then DLQ
PermanentInvalid parameters, identity not verified, no envelopeMark 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.


A submission moves through these states:

graph LR
submitted --> queued
queued --> sent
queued --> failed
sent --> delivered
sent --> bounced
StatusMeaning
submittedCreated by the JMAP API
queuedSuccessfully placed on the SQS queue
sentSES accepted the message
failedPermanent error before or during sending
deliveredRecipient’s server confirmed receipt (via SES delivery event)
bouncedDelivery failed after sending (via SES bounce event)

The transition from sent to delivered or bounced happens asynchronously via the SES feedback pipeline.