Self-hosted service
tweedegolf/mailcrab avatar
tweedegolf/mailcrab

MailCrab: a Rust SMTP sink and web inbox for local development

Email test server for development, written in Rust

1,000 stars47 forksRustApache-2.0

At a glance

What is it?
MailCrab is an accept-all SMTP server with a browser inbox, written in Rust with an Axum backend and a Yew frontend compiled to WebAssembly. It runs as a single binary or a 7.77 MB Docker image, keeps every message in memory, and exposes a JSON API plus a websocket for live updates.
Who is it for?
Adopt MailCrab when a backend under development needs to prove it sends mail without involving a real mailbox, since the accept-all SMTP server, the local ports and the API make that a one-command setup for anyone on the team.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

An accept-all SMTP server that keeps everything in memory

MailCrab is an email test server for development, written in Rust, and it was created as an exercise in Rust, trying out Axum and functional components with Yew, as the project states. The audience is a developer who needs their application to be able to send email during development without that email reaching a real person, and who wants to see what would have been sent. The mechanism is an accept-all SMTP server plus a web interface to view and inspect all incoming email, with the ability to view formatted mail, download attachments, view headers, or look at the complete raw mail contents. The two properties that define the design are in the technical overview: the backend receives email over an unencrypted connection on a configurable port, and all email is stored in memory while the application is running. Nothing is written to disk, so there is no mailbox to inspect after the fact and no cleanup job, which is exactly what you want from a test sink and exactly what you must not want from a relay.

Six API endpoints and a websocket that pushes new mail

The API surface is small and explicit. GET /api/messages returns all message metadata, GET /api/message/[id] returns a complete message given its id, POST /api/delete/[id] deletes a message given its id, POST /api/delete-all deletes all messages, and GET /api/version returns version information about the executable. The sixth is the live one: GET /ws sends email metadata to each connected client when a new email is received. The frontend uses this in two steps, first calling /api/messages to receive all existing email metadata and then subscribing for new messages over the websocket connection, and when you open a message it calls /api/message/[id] to retrieve the complete message body and raw email. The backend also accepts a few commands over the same websocket, to mark a message as opened, to delete a single message, or to delete all messages. That last detail is the useful bit for anyone writing tests: the same channel that delivers mail also drives the UI, so a test can assert that a message was opened without driving a browser.

Running it: one Docker command, two ports

The shortest path to a running inbox is a single container, and the project puts it at the top of the page as the TLDR:

bash
docker run --rm -p 1080:1080 -p 1025:1025 marlonb/mailcrab:latest

The two port mappings are the whole configuration surface for a first run. The default SMTP port is 1025 and the default HTTP port is 1080, so the container publishes the SMTP listener and the web interface together, and you point your application's mail settings at 127.0.0.1:1025. Then open a browser and navigate to http://localhost:1080 to view the web interface. The image runs on all amd64 and arm64 platforms, and the project describes it as a 7.77 MB docker image, which is a pleasant surprise next to a tool that includes a browser UI. The Dockerfile in the repository is short, built on alpine:3.24, with the binary copied in and the two ports exposed. Single binary builds are also published in the repository releases, which is the option to pick when you do not want a container in a development environment.

Environment variables move the ports, the host, the prefix and the TLS

Everything else is configured with environment variables, and the set is small enough to read in one pass. SMTP_PORT and HTTP_PORT change the two default ports, and MAILCRAB_PREFIX sets a path prefix for the web interface, so the UI is then served at http://localhost:1080/emails/ for a prefix of emails, which matters when the test server shares a host with something else. HTTP_HOST sets the address the HTTP listener binds to, and the default differs by how you run it: in the docker image the default address is 0.0.0.0, while running MailCrab directly with cargo or a binary defaults to 127.0.0.1. The TLS switch is ENABLE_TLS_AUTH=true, and here the behaviour is unusual enough to read twice. MailCrab generates a key-pair and prints the self-signed certificate, and any username and password combination is accepted. You can also mount your own pair at /app/key.pem and /app/cert.pem. A container with TLS and a mounted certificate looks like this:

bash
docker run --rm --env ENABLE_TLS_AUTH=true -v key.pem:/app/key.pem:ro -v cert.pem:/app/cert.pem:ro -p 1080:1080 -p 1025:1025 marlonb/mailcrab:latest

The accepted-any-credentials behaviour is right for a test sink whose job is to let your application's login code run its normal path, and it is also the reason this should never be exposed to a network you do not control.

Memory is the storage layer, so retention is your problem

The retention behaviour is the sharpest edge in the design and the project says so plainly. By default, messages are stored in memory until MailCrab is restarted, and the documentation warns that this might cause an OOM when MailCrab lives long enough and receives enough messages. The fix is MAILCRAB_RETENTION_PERIOD, set to a number of seconds, after which older messages are cleared. That is a mitigation, not a store, and it changes how you should think about the tool. A developer running the container for the length of a sprint will accumulate every message the app under test ever sent, and if the app is in a loop that sends mail, memory grows with it. Setting the retention period to a modest number is the difference between a test sink and a process that dies mid-afternoon. A related operational point is that there is no persistence across restarts, so a container restart empties the inbox, and any test that asserts on a specific message needs the message to still be there when it runs.

The websocket queue is the throughput limit, and it is configurable

The performance section is honest in a way that many project pages are not. MailCrab is fast, but there is a bottleneck in the throughput of the websocket connection between the server and the browser, and if there are many messages sent at once, more than 100 per second, a client can lag behind and messages can get lost. The mitigation is the QUEUE_CAPACITY environment variable, which sets the internal queue size, and the default is 32, meaning MailCrab can handle 32 messages if they are all sent at the same time. So the real throughput shape of the tool is bursty by design: a burst of 32 concurrent messages is handled, a burst above that can be dropped on the websocket path, and raising the queue trades memory for tolerance. This matters for a specific and common case, a seeding script that fires a thousand confirmation emails at a freshly started application. The SMTP side will accept them and the messages will be in memory, but the live view in the browser will not show all of them, and the API remains the source of truth. Anyone who wants the browser list to be complete during a burst test should raise QUEUE_CAPACITY before the test rather than after it.

Sending mail into it with curl, cargo or a compose file

The samples directory ships a set of test messages, including normal.email, cid.email, multiple_recipients.email, no_from.email and missing_end_boundary.email, which cover the interesting MIME cases. The project gives two ways to send them. The first is a cargo test in the backend directory, marked as ignored so it only runs on request:

bash
cd backend/
cargo test send_sample_messages -- --ignored

The second is curl against the SMTP port, one invocation plain and one with TLS:

bash
curl smtp://127.0.0.1:1025 --mail-from [email protected] --mail-rcpt [email protected] --upload-file samples/normal.email

For a team that runs the service in containers, the compose file in the documentation shows the shape, with the image, the commented environment and volume options, and the two published ports:

yaml
version: '3.8'
services:
  mailcrab:
    image: marlonb/mailcrab:latest
    ports:
      - '1080:1080'
      - '1025:1025'
    networks: [default]

For Kubernetes there is a Helm chart in the charts directory of the repository, installed with helm install mailcrab ./charts/mailcrab -f values.yaml, and a reverse proxy guide covers putting the interface behind a proxy.

Where MailCrab differs from the tools it was inspired by

The project says it was inspired by MailHog and MailCatcher, and the differences are mostly in packaging and implementation rather than in the job done. Both of those are long standing tools in the same category, and the choice here is not about a better inbox but about what your stack already contains. MailCrab is written entirely in Rust on both ends, with the backend on Axum and the frontend built with Yew as functional components compiled to WebAssembly, and it ships as a single binary or as a small Alpine based container. A team already standardised on a different container base, or with a proxy that expects a specific upstream, will care more about that than about the feature list. The other difference is the API surface: MailCrab documents its endpoints and its websocket commands explicitly, which makes it straightforward to write assertions against in a test suite rather than scraping a web UI. A related project is worth knowing about, since it is the kind of integration that saves a developer a trip to the browser: CrabAlert is a macOS status bar application that notifies you of incoming messages in MailCrab.

Editorial conclusion

Adopt MailCrab when a backend under development needs to prove it sends mail without involving a real mailbox, since the accept-all SMTP server, the local ports and the API make that a one-command setup for anyone on the team. Do not adopt it as a shared team inbox, a long-lived staging relay or a load test rig, because messages live in memory until a restart, the websocket feed can drop messages above roughly 100 per second, and TLS authentication accepts any username and password combination. Verify first that the default SMTP port 1025 and HTTP port 1080 are free on the machine, that MAILCRAB_RETENTION_PERIOD is set if the process will run for hours, and that the app under test does not depend on a provider specific API that a local sink cannot emulate.

Frequently asked questions

How do I start MailCrab and see the emails it receives?

Run the container with the two ports published, docker run --rm -p 1080:1080 -p 1025:1025 marlonb/mailcrab:latest, then open a browser and navigate to http://localhost:1080. The SMTP port is 1025 and the HTTP port is 1080 by default, and both can be changed with the SMTP_PORT and HTTP_PORT environment variables.

Does MailCrab store received emails on disk?

No. All email is stored in memory while the application is running, and by default messages are stored until MailCrab is restarted, which the project warns might cause an OOM over time. Setting MAILCRAB_RETENTION_PERIOD to a number of seconds clears messages older than that duration.

How do I make MailCrab serve the web interface under a path prefix?

Set the MAILCRAB_PREFIX environment variable, for example to emails, and the web interface is then served at http://localhost:1080/emails/. The HTTP_HOST variable controls the address the HTTP listener binds to, which defaults to 0.0.0.0 in the docker image and 127.0.0.1 when running the binary directly.

Can MailCrab handle a burst of thousands of test emails?

The SMTP side accepts them, but the project notes a bottleneck in the websocket connection to the browser, where a client can lag behind and messages can get lost above more than 100 per second. The QUEUE_CAPACITY environment variable sets the internal queue size, which defaults to 32, and raising it helps prevent losing messages during a burst.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. Releases
  5. tweedegolf/mailcrab on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tweedegolf-mailcrab.svg)](https://hysenlabs.com/projects/tweedegolf-mailcrab)

Was this useful?