Email capture
An SMTP listener that accepts mail for your sandboxes and never delivers it anywhere.
Try it in one command
Everything below follows from this working, so start here. Create a sandbox and paste its id and secret in:
curl smtp://smtp.trapit.dev:587 \ --user "YOUR-SANDBOX-ID:YOUR-SANDBOX-SECRET" \ --mail-from "dev@example.com" \ --mail-rcpt "anything@example.com" \ --upload-file - <<'EOF' Subject: Hello from curl It's trapped. EOF
Pointing your app at it
Same four values, wherever your app already reads its SMTP settings from:
host smtp.trapit.dev port 587 user <sandbox-id> pass <sandbox-secret> tls required (STARTTLS before AUTH)
Both credentials come from the sandbox — the username is its id, the same string its capture host is built from, and the password is its secret.
If port 587 doesn't suit you
587 is the submission port and the one to reach for.
If your mailer wants TLS from the first byte rather than a STARTTLS upgrade, use
465 instead.
Port 25 also answers, and behaves identically — it is there for
senders already pointed at it.
Authentication is required on every port and never offered in the clear, so a sandbox secret only ever crosses an encrypted connection. Worth knowing if your app runs on a cloud host: outbound port 25 is blocked by default on Azure VMs and by plenty of other networks, which is exactly why 587 is the one quoted everywhere.
The recipient address doesn't matter
This is the part worth reading twice. The sandbox is decided by the credentials that
authenticated, not by who the mail is addressed to. Send to
anything@example.com, to a customer's real address, to whatever your fixtures
already generate — it is trapped either way, by the sandbox that signed in.
So there is nothing to create before sending, no address to derive, and no address to rewrite in the app under test. You point it at TrapIt's SMTP settings and change nothing else. That is the whole reason this is credential-routed: the address is your application's business, and it stays that way.
The flip side: mail TrapIt cannot authenticate is refused. A third party sending on your behalf — a transactional email provider, say — cannot be trapped here, because it connects with its own credentials, not your sandbox's. Point the app at TrapIt directly instead.
Isolating parallel runs
Give each run its own sandbox. An ephemeral one is minted in a single call, expires on a TTL, and takes everything it captured with it — the usual choice in CI. Two runs then share nothing at all: not a mailbox, not a filter, not a naming convention they both have to honour. How to create one →
Within one sandbox, wait_for_email(to: "…") matches the envelope recipient
exactly, so a run that does want to sort its own mail out by address can. That is a
filter over what was trapped, though — not what decided to trap it.
What gets recorded
- Envelope sender and recipients, plus the parsed
From,ToandSubject. - Every address the message was accepted for, which is what
to:matches on — worth knowing, because theToheader is only ever what the sender wrote. - Both bodies —
text/plainandtext/html— when the message carries them. - Attachments, downloadable individually.
- The original RFC 822 source, kept verbatim, so you can inspect headers the parser did not surface.
Messages over 10 MB are rejected at the protocol level
and never stored. The limit is advertised over ESMTP SIZE, so a well-behaved
sender knows before it uploads.
Ingress is rate limited at
120 messages per
60 s per source IP and
600 per workspace. Over the limit
the listener answers a 421 temp-fail — a well-behaved sender queues and retries,
so bursts are delayed rather than dropped, and back-to-back sends (an invite plus an immediate
resend, say) are fine well inside those numbers. The full table of limits is on the
REST API page.
Reading it back
In the dashboard, or over the API — GET https://trapit.dev/api/v1/sandboxes/{sandboxId}/messages
for the list (filterable by capturedFor, since and isRead)
and …/messages/{messageId}/raw for the original source. Rather than polling, both
programmatic surfaces can block until a matching message arrives: the MCP
wait_for_email tool, and GET …/messages/wait over REST.