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 abind_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 thesmpp_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.- Username sentinel. The client must present
apikeyas the AUTH username (SendGrid parity). Any other username is a rejection, before the password is even considered. - Password is the API key. The password carries the full API key value
(
dv_live_sk_…ordv_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. - Single opaque rejection. Every AUTH failure — wrong username, unknown
key, revoked key, expired key — returns one
535 5.7.8reply. The wire never reveals which check failed; the dashboard’s API-keys surface is where you diagnose it.
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:- Parse. The raw
DATApayload is parsed by the same MIME parser the inbound-parse receiver uses — one parser, not two. An empty or unparseable payload maps to a single554 5.6.0reply, not a wire fault. - Resolve recipients and body. The envelope
RCPT TOlist is the authoritative recipient set — the only list that carriesBcc— withTo/Ccheaders 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 a554. - 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 envelopeMAIL FROMas fallback. The submission is tagged with sourcesmtp-relayin metadata so the delivery log attribute it back to the relay.
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.
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 withRATE_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:
- AUTH fails →
535 5.7.8, session usable for a retry. MAIL FROMfails →553 5.1.7; the transaction resets.- A bad
RCPT TO→501 5.1.3; other recipients in the same envelope are unaffected. - Over-recipient envelope →
452 4.5.3; re-send in batches under the cap. - Oversize
SIZE→552 5.3.4; the envelope is refused beforeDATA.
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.- AUTH
apikey/ $ORBIT_API_KEY → resolves through the API-key cache → tenant scope (org, tenant, schema) returned → proceed. MAIL FROM:<you@your-domain.com>→ syntax-valid → accepted.RCPT TO:<customer@example.com>→ syntax-valid, one recipient → accepted.DATA→ MIME parser extracts subject, theFrom:header for verified-sender enforcement, and the body (HTML preferred over text).- Dispatch → one send into the shared email pipeline for
customer@example.com, taggedsource: smtp-relay. - Reply
250 2.0.0→ the swaks run printsMessage accepted for delivery; the row isqueuedin 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.
554 only when every recipient was
rejected.
Who owns which failure
Related
- Send outbound email over SMTP — the client-side
guide: ports, the
apikeysentinel, 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.