Skip to content

About

Local email sandbox + catcher for individual developers — single binary, SMTP + Web UI + JSON API, MIT.

Resources

Code of conduct

Contributing

Security policy

Stars

113 stars

Watchers

0 watching

Forks

Repository files navigation

mailtrap-local

CI

A local email sandbox + catcher for individual developers. Runs on your laptop, catches every email your app sends, and gives you a web sandbox to browse, search, and inspect them in. One self-contained binary. Open source. MIT-licensed.

What it does

  • SMTP server on 127.0.0.1:3535 — point any app at it; every message you send locally gets caught.
  • Web sandbox on 127.0.0.1:3550 — search, view, inspect HTML/text/raw/headers/attachments.
  • REST API on 127.0.0.1:3550 — JSON over HTTP; existing API clients only need a host override.
  • Validation tools: HTML email-client compatibility check across major mail clients.
  • Workflow: categories, message release to production SMTP, outbound webhooks, sendmail-replacement binary mode.

Screenshots

Browse caught messages, preview rendered HTML, and inspect client compatibility — all offline, on your machine.

Inbox + HTML preview HTML Check Getting started (code samples)
Inbox with message list and HTML preview HTML Check tab with market support and issue breakdown Empty inbox with SMTP code samples for Node.js, Python, Ruby, and more

Quick tour — browse the inbox, open a message, switch between HTML preview and HTML Check

What it is not

  • Not a team tool. No user accounts, no authentication, no multi-tenancy, no shared sandboxes, no API tokens, no RBAC.
  • Not infrastructure. No Postgres support, no high-availability, no Prometheus metrics, no Kubernetes probes. SQLite only. Localhost only.
  • Not a Mailtrap Sandbox replacement. If you need team features and cloud setup, use Mailtrap — the hosted product this tool complements.

Stack

Component Technology
Single binary Go 1.26+ — SMTP, HTTP/JSON API, embedded SPA, all in one process
SMTP ingest emersion/go-smtp + jhillyerd/enmime
HTTP routing go-chi/chi
Storage SQLite via modernc.org/sqlite (pure Go — no CGo)
Web UI React 19 + tailwindcss + Radix UI + Vite, embedded into the binary at build time via //go:embed
Realtime Plain WebSocket (/cable)
Distribution brew install, snap install, nix run, docker run, single static binary (macOS + Linux; Windows not yet)

Quick start

Homebrew (macOS / Linux)

brew tap mailtrap/local
brew trust --formula mailtrap/local/mailtrap-local
brew install mailtrap-local
brew services start mailtrap-local

Then open http://127.0.0.1:3550 and point your app's SMTP client at 127.0.0.1:3535.

Snap (Ubuntu)

sudo snap install mailtrap-local
mailtrap-local

Then open http://127.0.0.1:3550 and point your app's SMTP client at 127.0.0.1:3535. Caught mail is stored in ~/snap/mailtrap-local/common/ (survives snap refreshes).

Nix

nix run github:mailtrap/mailtrap-nur#mailtrap-local
# or
nix profile add github:mailtrap/mailtrap-nur#mailtrap-local
# Nix older than 2.25: nix profile install …

Package lives in mailtrap/mailtrap-nur; goreleaser opens a PR there on every tag (same flow as the Homebrew tap).

Docker

docker run --rm \
  -p 127.0.0.1:3535:3535 -p 127.0.0.1:3550:3550 \
  -v mailtrap-local:/var/lib/mailtrap-local \
  mailtrap/mailtrap-local:latest

mailtrap-local only accepts connections from your own machine. The image allows the container's own networks (--allow-attached-networks), since your machine's traffic reaches the container from the network's gateway and other containers come from inside it; publishing on 127.0.0.1 keeps the ports off your LAN. Don't run the image with --network host or on a macvlan or ipvlan network: the container's own subnet is then your LAN, so the LAN counts as an attached network.

With Docker Desktop and other VM-based engines (OrbStack, Colima, Podman Machine, …), ports published on all interfaces (-p 3550:3550) are refused even from your own machine: every connection, local or from the LAN, reaches the container from the VM's gateway, so the two can't be told apart. Publish on 127.0.0.1 as shown above.

The same image is also published to GHCR at ghcr.io/mailtrap/mailtrap-local:latest.

Prebuilt binary

Download the right archive for your platform from the Releases page, extract, and run:

tar xzf mailtrap-local_<version>_<os>_<arch>.tar.gz
./mailtrap-local

On macOS, a one-time Gatekeeper bypass is required because the binary isn't notarized:

xattr -d com.apple.quarantine ./mailtrap-local

From source

git clone https://github.com/mailtrap/mailtrap-local && cd mailtrap-local
scripts/build.sh         # builds frontend + Go binary → bin/mailtrap-local
bin/mailtrap-local

go install github.com/mailtrap/mailtrap-local/cmd/mailtrap-local@latest also works for Go developers, but installs the build-time placeholder UI instead of the real React SPA — Vite isn't part of go install. Use one of the channels above for the real UI.

bin/mailtrap-local listens on 127.0.0.1:3535 (SMTP) and 127.0.0.1:3550 (HTTP+UI+API) by default. Pass --http-listen, --smtp-listen, --db, or --version to override. Connections from other machines are refused; --allow-from (comma-separated IPs or CIDR ranges) admits extra source addresses, and --allow-attached-networks admits the subnets of this machine's network interfaces.

Sendmail-replacement mode

Symlink (or rename) the binary to sendmail and put it on $PATH:

ln -sf "$(brew --prefix)/bin/mailtrap-local" /usr/local/bin/sendmail

PHP's mail(), cron, mailx, and anything else that shells out to sendmail -t -i will hand the message off to the local sandbox. The binary detects it was invoked as sendmail (or mailtrap-sendmail, or via mailtrap-local sendmail …) and switches modes automatically.

Development setup

# Prereqs: Go 1.26+, Node 22+, foreman (optional, for bin/dev)
# Install via mise/asdf using .tool-versions, or directly.

bin/setup           # go mod download + npm install
bin/dev             # boots backend + Vite concurrently (Procfile.dev)

In dev:

Port Served by Purpose
3535 go run ./cmd/mailtrap-local SMTP ingest
3540 Vite dev SPA — proxies /api + /cable to backend
3550 Go binary JSON API + WebSocket

Open http://127.0.0.1:3540 for the dev UI (with HMR). The production single-binary serves the same UI at http://127.0.0.1:3550 out of the embedded dist/.

Repository layout

mailtrap-local/
├── cmd/mailtrap-local/      # main + sendmail dispatch + embed staging
├── internal/
│   ├── api/                 # chi handlers, wire types, SPA serving
│   ├── smtpd/               # go-smtp wrapper + enmime ingest
│   ├── store/               # SQLite schema + queries
│   ├── jobs/                # cloud mirror / relay / webhook / retention
│   ├── htmlcheck/           # HTML email compatibility rule engine
│   ├── live/                # in-process pub-sub for WebSocket
│   ├── relay/               # outbound SMTP relay client
│   ├── webhook/             # outbound webhook signed-POST client
│   ├── cloud/               # Mailtrap sandbox API client
│   └── config/              # YAML config loader
├── frontend/                # React + Vite SPA (built into cmd/mailtrap-local/dist/)
├── docs/
│   ├── api/openapi.yaml    # API spec (embedded + served at /api/v1/openapi.yaml)
│   └── images/             # README screenshots + demo GIF
├── snap/gui/icon.png        # Snap Store listing icon
├── scripts/build.sh         # frontend build + go build → bin/mailtrap-local
├── Dockerfile               # multi-stage source build (Node → Go → distroless)
├── Dockerfile.goreleaser    # release-time wrapper (goreleaser-built binary → distroless)
├── .goreleaser.yaml         # release pipeline: binaries + Homebrew + NUR + Snap + Docker Hub + GHCR
└── .github/workflows/       # ci.yml + release.yml

The Homebrew formula lives in mailtrap/homebrew-local; the Nix package lives in mailtrap/mailtrap-nur. goreleaser opens a PR in each on every tag, and also packs/uploads Snap Store packages.

API

The full OpenAPI 3.1 specification is at docs/api/openapi.yaml. When the app is running, the same spec is served at http://127.0.0.1:3550/api/v1/openapi.yaml. A bare GET on /api/v1 redirects to the hosted docs at https://docs.mailtrap.io/.

Configuration

Optional YAML config at $XDG_CONFIG_HOME/mailtrap-local/config.yml (or ~/.config/mailtrap-local/config.yml). Sections:

  • storage.max_messages — retention cap (oldest evicted past the limit).
  • cloud — Mailtrap sandbox API token + sandbox ID for the "Send to Cloud" button.
  • relay — outbound SMTP relay credentials for the "Release" button.
  • webhook — outbound webhook URL + signing secret.
  • allowed_hosts — extra hostnames you open the UI/API with on this machine (e.g. john.dev from /etc/hosts). localhost and *.localhost always work.

Values support ${ENV_VAR} interpolation. Keys present in the config file override the equivalent values stored in the SQLite DB; pinned fields are read-only in the UI.

Tests

go test ./...

Unit + integration tests cover the store, API, SMTP server, jobs dispatcher, HTML check, and sendmail mode. The OpenAPI drift test (internal/api/openapi_drift_test.go) fails the build if a route is added to chi without speccing it (or vice versa).

License

MIT — see LICENSE.

About

Local email sandbox + catcher for individual developers — single binary, SMTP + Web UI + JSON API, MIT.

Resources

Code of conduct

Contributing

Security policy

Stars

113 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages