Skip to main content

SMTP relay ingress model

Orbit’s SMTP relay is the email pillar’s wire surface — the protocol endpoint a standard SMTP client points at, the same way the SMPP edge is the SMS pillar’s wire surface for an SMPP client. Where SMPP terminates a bind_transceiver + submit_sm conversation, the relay terminates an AUTH + MAIL FROM / RCPT TO / DATA conversation, then hands the captured submission to the same email pipeline POST /api/v1/messages/email uses — with zero duplication of suppression, consent, billing, verified-sender enforcement, and the delivery log. The SMTP send guide covers the client side: ports, the apikey sentinel, and client snippets. This page covers the platform model underneath — who owns each decision in the handshake, and what a given SMTP reply tells you about which stage rejected it.

1. The wire surface alongside SMPP

Both wire surfaces share one property: the protocol endpoint is a projection of your account configuration, not a bespoke gateway you configure directly. The SMPP edge fronts a Jasmin relay whose users mirror the smpp_credentials rows you write; the SMTP relay front terminates the submission conversation and resolves the same API-key identity your HTTP traffic uses, so no new credential class exists — a key from Settings → API Keys is the only credential the relay understands. Because the two surfaces are twins, the same question answers both: “do I integrate on the wire, or over HTTP?” Pick the wire when the client already speaks the protocol (a legacy app, a mail-enabled device, an SMPP sender); pick HTTP when the integration is new and first-class API features (idempotency keys, templates, URL attachments) matter.

2. Ingress auth — one chokepoint, one verdict

The AUTH stage resolves to exactly one verdict — accepted or rejected — and maps to one SMTP reply when it fails.
  1. Username sentinel. The client must present apikey as the AUTH username (SendGrid parity). Any other username is a rejection, before the password is even considered.
  2. Password is the API key. The password carries the full API key value (dv_live_sk_… or dv_test_sk_…). The relay resolves it through the same API-key cache the HTTP middleware warms, so an already-recognized key costs no extra DB round-trip, and a revoked key fails identically on both surfaces the moment it is evicted from that cache.
  3. Single opaque rejection. Every AUTH failure — wrong username, unknown key, revoked key, expired key — returns one 535 5.7.8 reply. The wire never reveals which check failed; the dashboard’s API-keys surface is where you diagnose it.
Once a credential resolves, the relay has the same tenant scope the HTTP path would have: organization id, tenant id, and the tenant schema name every downstream write targets. One tenant’s key can only ever land submissions in its own schema — the isolation rule from Tenant isolation applies unchanged at the wire. After AUTH, a single acceptance check decides whether an envelope is relayed. The reply code tells you which stage said no: Two guardrail defaults matter here: a submission carries at most 1000 recipients and at most 30 MiB of declared message size (the same ceiling the inbound-parse path enforces, so inbound and outbound share one limit). Both are envelope-level — they run before any MIME parsing, so an oversize or over-recipient envelope is refused without reading the payload.

3. Message capture — from DATA into the email pipeline

An accepted envelope enters the send path as one transform with three steps:
  1. Parse. The raw DATA payload is parsed by the same MIME parser the inbound-parse receiver uses — one parser, not two. An empty or unparseable payload maps to a single 554 5.6.0 reply, not a wire fault.
  2. Resolve recipients and body. The envelope RCPT TO list is the authoritative recipient set — the only list that carries Bcc — with To/Cc headers as a defensive fallback when no envelope list arrived. The body prefers the non-empty HTML part over plain text, matching the HTTP controller’s selection so the two ingress paths cannot drift. An empty body is a 554.
  3. Dispatch per recipient. One message send per accepted recipient, the same fan-out the HTTP route performs. Sender is the message From: header (what verified-sender enforcement checks), with the envelope MAIL FROM as fallback. The submission is tagged with source smtp-relay in metadata so the delivery log attribute it back to the relay.
Because the dispatch lands in the shared email pipeline, the captured message inherits the full email delivery lifecycle: it enters as queued, transitions to sent when the provider accepts it, then resolves to delivered, bounced, or complained per the provider’s webhooks — exactly what an API-sent message does. Per-recipient outcomes are collected: a suppressed or rejected recipient is reported in the relay result, while every accepted recipient still delivers. The transaction-level reply after DATA is 250 2.0.0 when at least one recipient was queued; 554 only when every single one was rejected. Attachment bytes are not carried through the relay — the parser records the attachment count in metadata only. Send attachment-bearing email over the HTTP API, which exposes attachments as a first-class field.

4. Sandbox vs live behavior

The relay draws no sandbox/live split of its own — it inherits one from the API key you authenticate with:
  • Live key (dv_live_sk_…) — the submission dispatches to the provider, bills against your wallet, and appears in the delivery log as a real send.
  • Test key (dv_test_sk_…) — AUTH resolves identically, the envelope checks run identically, and the per-recipient dispatch runs — but the downstream send pipeline short-circuits the provider call, mints a synthetic row, and skips billing. A test-key submission proves your SMTP client handshake end-to-end without sending real mail.
That split means the relay itself is not a separate sandbox surface: pick the mode by picking the key. The swaks probe in the send guide is safe to run repeatedly on a test key for exactly this reason.

5. Rate limiting and what a failed handshake sees

Relayed submissions draw from the shared email rate pool — the same per-tenant pool the HTTP email path draws from (default 200 messages per minute per tenant; see Rate limits). Moving traffic between HTTP and SMTP does not dodge the limit, because both paths consume one pool. When the pool is exhausted the send pipeline rejects the dispatch with RATE_LIMITED, and the SMTP session sees it as a per-recipient rejection — the envelope itself is already accepted at that point. A failed handshake (before any envelope is accepted) is always a protocol-level rejection the client observes directly — never a silent drop and never an HTTP-style status. The ordering your client sees is fixed:
  1. AUTH fails → 535 5.7.8, session usable for a retry.
  2. MAIL FROM fails → 553 5.1.7; the transaction resets.
  3. A bad RCPT TO → 501 5.1.3; other recipients in the same envelope are unaffected.
  4. Over-recipient envelope → 452 4.5.3; re-send in batches under the cap.
  5. Oversize SIZE → 552 5.3.4; the envelope is refused before DATA.
For a step-2+ rejection the check that fired tells you the surface to fix: 553/501 mean the client’s envelope is malformed, 452 means batching, 552 means your message is over the 30 MiB ceiling, and 535 means the key, not the envelope. None of those failures touch the pipeline — no row is written, nothing is billed, nothing is queued.

Worked example — one submission end to end

The send guide’s swaks probe is the same handshake; here is what the relay does with it.
Stage by stage:
  1. AUTH apikey / $ORBIT_API_KEY → resolves through the API-key cache → tenant scope (org, tenant, schema) returned → proceed.
  2. MAIL FROM:<you@your-domain.com> → syntax-valid → accepted.
  3. RCPT TO:<customer@example.com> → syntax-valid, one recipient → accepted.
  4. DATA → MIME parser extracts subject, the From: header for verified-sender enforcement, and the body (HTML preferred over text).
  5. Dispatch → one send into the shared email pipeline for customer@example.com, tagged source: smtp-relay.
  6. Reply 250 2.0.0 → the swaks run prints Message accepted for delivery; the row is queued in your tenant schema and follows the email delivery lifecycle from there on a live key, or is a synthetic no-bill row on a test key.
If step 5 rejects the recipient — the address is suppressed, the sender domain is unverified, or the rate pool is exhausted — the reply is still the protocol-level one: a per-recipient rejection collected into the result, with the transaction reply dropping to 554 only when every recipient was rejected.

Who owns which failure

  • Send outbound email over SMTP — the client-side guide: ports, the apikey sentinel, client snippets, and the feature limits.
  • SMPP edge model — the SMS twin of this surface: reconciled credentials, DLR delivery modes, and BYO carrier routing on the other wire.
  • Email delivery lifecycle — what a captured message transitions through once it leaves the relay.
  • SMTP relay recipes — the HTTP-side request / response / error loop for the same pipeline, with the shared rate pool the relay draws from.
  • Tenant isolation — the schema-level rule an authenticated relay submission inherits for every downstream write.
  • Rate limits — the shared per-tenant pool both HTTP and SMTP email traffic draw from.