Skip to main content

Inbound email parse pipeline

Every email that lands on your inbound funnel crosses the same chain on its way into your workspace: the provider receives it at the funnel, Orbit parses the raw MIME into structured fields, the recipient is matched against a route you registered, the attachments are handed to the threat scan, and the message resolves to a conversation thread or a new ticket. This page explains that chain end to end — the email sibling of the SMS MO chain, read alongside it so the two channels share one shape.

Scope

This page covers the inbound email parse pipeline only — how a received email becomes a conversation or ticket in your workspace. Two related pages cover the other directions and are deferred to here:
  • Outbound email — the send path, status machine, bounces, and the retry queue — is Email delivery lifecycle. That page answers “what happened to a message I sent”; this one answers “what happened to a message I received.”
  • Attachment threat scanning — the byte-level scan that decides whether a part is archived or quarantined — is Inbound attachment threat scan. This page hands attachments to that scan and resumes once the verdict is in.

The inbound funnel

Orbit receives your inbound email at a dedicated funnel subdomain — send.<domain>, provisioned per tenant — not at your apex domain. The funnel is the host your MX records point at Orbit’s ingress, and it is the address the provider delivers to. The transport security model covers the MX gate that grades that record; this page starts one step later, at the moment a message arrives on the funnel. Why a subdomain and not the apex? On a platform that both sends and receives email on the same domain, the apex carries your SPF, DKIM, and DMARC records for outbound — and the return-path pair the provider issues at send.<domain> for bounces and complaints. Receiving inbound mail at the same host as the return-path would conflate two directions that have different deliverability requirements. The funnel isolates the receive side so the MX gate grades exactly the record your tenant provisioned, and so a sender-only domain that never receives is not asked for an MX it never needed.

The parse pipeline stages

A received email moves through five stages from the provider to a resolved conversation. The chain is pure ingress — no outbound path is touched, so the outbound termination rule that governs SMS exit is not implicated.
  1. The provider receives at the funnel. A sender’s mail server delivers to the provider’s MX target for send.<domain>. The provider accepts the SMTP submission, flattens its own multipart envelope, and POSTs the raw MIME to your inbound-parse receiver as a signed text/plain body.
  2. The receiver verifies and parses. The receiver (POST /email/inbound-parse/:tenantSchema) verifies the X-Orbit-Signature HMAC over the raw body, then parses the MIME into structured fields: from, to, subject, the decoded text and html bodies, and an attachments array that carries metadata only — filenames, content types, decoded byte sizes, and inline Content-ID values. Attachment bytes are never inlined into the parsed JSON.
  3. Sender and thread validation. The recipient addresses are collected in authority order: the SMTP envelope recipient (Delivered-To, X-Envelope-To, X-Original-To) first, then the To and Cc headers. Matching the envelope before the headers is why BCC, aliased, and forwarded mail — where your address never appears in To — still routes correctly.
  4. Body extraction and normalization. The text and HTML bodies are decoded per their Content-Transfer-Encoding (base64, quoted-printable, or 7bit/8bit). Folded headers are unfolded. A message with no parseable body degrades to whatever parts were extractable rather than failing the whole parse.
  5. Attachment hand-off and route resolution. The attachments are handed to the threat scan, which byte-scans each part and returns a verdict — clean (archived), suspicious (archived, flagged), or malicious (quarantined, dropped). In parallel, the recipient is matched against your active routes and the message resolves to a conversation thread or a new ticket.
This is the email analogue of the MO chain on the SMS side: the same shape — receive, normalize, route, dispatch — applied to a MIME body instead of an SMPP PDU. Read the two sections as siblings.

Idempotency and retries

A provider that does not receive a clean 2xx retries the delivery — sometimes minutes later, sometimes hours. Without dedup, a replayed delivery would create a second ticket for the same message. The parse pipeline answers that the same way the MO dead-letter queue does on the SMS side.

Replay dedup

Every inbound email carries a Message-ID header (RFC 5322). When one is present, the pipeline claims an idempotency key against it before any write — so a provider retry that lands while the first delivery is still being processed, or a duplicate that arrives hours later, collapses onto the same key and is dropped as a duplicate rather than processed twice. The provider gets its 202 acknowledgement either way, so it does not re-fire the same message indefinitely.

Dead-letter behaviour for transient resolution failures

A Postgres failover, a connection-pool spike, or contention on a per-sender lock can raise a transient error exactly as the conversation or ticket is being written. The carrier already got its acknowledgement — the message is gone from the network’s point of view — so without a second path that inbound email would be silently lost. Orbit’s answer mirrors the MO dead-letter queue: when the resolution step fails transiently, the message is enqueued to an internal retry queue carrying the tenant, sender, destination, body, and the original wire payload. A background worker re-runs the resolution on a widening schedule — one minute, five minutes, thirty minutes, then two hours — and if it still fails after the fourth attempt, the row is held as a dead letter for seven days instead of being discarded. Once a retry succeeds, the rest of the chain runs as usual. Do not confuse this with the webhook delivery retry queue: that one covers re-delivering an event to your subscriber endpoint when your server cannot accept it. This one covers the opposite direction — the inbound resolution path itself, before your webhook was ever in play.

Where the parsed message lands

After the route match, the message resolves to one of three destinations. The tenant rules you configure decide which one: The threading key is the sender address plus a normalized subject — prefixes like Re: and Fwd: are stripped so a reply to a reply threads onto the original. A reply without a parseable sender always opens a fresh ticket, because the threading key has nothing to match against. Alongside the ticket, the platform fires a message.received webhook event to your subscribers — the same event the SMS MO path emits, with channel: "email" and the email-specific metadata under metadata. This is the email analogue of the message.received event on the SMS side: a tenant that listens on the canonical webhook bus receives the same signal whether the inbound message arrived over SMS or email.

Failure modes

Observability

Each stage of the pipeline surfaces in two places:
  • Webhook events. A matched inbound email fires message.received to your subscribers (channel email, with message_id, from, to, body, and metadata carrying the subject, route id, matched recipient, and attachment count). The webhook delivery semantics page covers the retry rules for that event.
  • Route health in the dashboard. Each route tracks failureCount, lastForwardedAt, and lastFailureAt — a successful forward resets the failure count; a non-2xx response or a network error increments it. Read these on Email → Inbound routes to spot a destination URL that has been failing. A route whose tenant schema has not yet had the inbound-routes table applied degrades to an empty list rather than surfacing a 500, so a brand-new tenant mid-provision does not show a broken state.
The 202 acknowledgement the receiver returns carries the matched state at each hop: matched, routeId, matchedRecipient, forwarded, destinationStatus, ticketId, and ticketThreaded. That response is the per-delivery health signal — the same shape a provider webhook consumer reads to confirm the message was accepted.

Worked example: one inbound email from provider to conversation

A customer sends a reply to your support address. Here is the JSON state at each hop, from the provider webhook to the resolved ticket. 1. The provider delivers to the funnel. The provider POSTs the raw MIME to your receiver, signed with the platform HMAC:
2. The receiver verifies and parses. The signature is checked; the MIME is parsed into structured fields. No attachments on this message:
3. The route matches. The recipient support@send.acme.io is collected from the envelope, matched against your active route for that pattern, and the parsed payload is forwarded to your destination URL as a signed JSON POST:
4. The conversation resolves. The sender address and the normalized subject (order #1234, with the Re: prefix stripped) match an open ticket from the same requester — so the reply threads onto that ticket as a new comment rather than opening a fresh one. The message.received event fires to your webhook subscribers in parallel:
5. The receiver acknowledges. The provider gets a 202 with the matched state at every hop — the forward succeeded, the ticket threaded, and the event was dispatched:
Had the subject not matched an open thread, ticketThreaded would be false and ticketId would carry a fresh ticket id — the message opens a new ticket instead of threading.

See also

Email delivery lifecycle

The outbound twin — how a sent email moves from provider acceptance to a mailbox outcome.

DLR and MO gateway pipeline

The SMS sibling — the carrier-to-tenant chain this page mirrors for email.

Inbound attachment threat scan

The byte-level scan this pipeline hands attachments to.

Inbound message routing

The tenant-level rule engine that decides where a received message goes.

Inbound email and SMS routing

The end-to-end guide for configuring inbound parse on a domain.

Inbound email-to-ticket recipes

Recipes for routing inbound email into tickets and queues.