MailCatcher: the missing piece of every local development setup
Catches mail and serves it through a dream.
At a glance
- What is it?
- A Ruby SMTP sink with a web interface, a JSON API and explicit instructions for not putting it in your Gemfile. 6,780 stars later, the design is still the same one-liner it started as.
- Who is it for?
- MailCatcher solves a problem so small that most teams solve it badly with a shared production inbox or by not looking at the mail their app sends. It catches everything on one port, shows it on another, and exposes the same data over HTTP, which means it can sit in CI as a test fixture rather than only on a developer's laptop.
- Can I use it commercially?
- Yes. MIT 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 26 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two ports and one command
The entire premise is one sentence: MailCatcher runs a simple SMTP server which catches any message sent to it and displays it in a web interface. You run it, point your application at smtp on port 1025 instead of a real server, and look at port 1080 in a browser.
Installation is a gem install and a bare command name.
gem install mailcatcherUpgrading is the same command, which tells you something about how stable the interface has been.
The feature list explains what the web interface actually does, and more than one item is about debugging rather than reading. It shows HTML, plain text and the raw source of each message as applicable. It rewrites the HTML so embedded inline images display and links open in a new window, which is the feature that turns an unreadable wall of markup into something you can actually read. It lists attachments and lets you download individual parts, and it can hand you the original email so you can open it in your native mail client. There is keyboard navigation between messages, which sounds minor until you have gone through forty notification emails looking for one with the wrong content.
Mail appears instantly if your browser supports WebSockets, and otherwise refreshes every thirty seconds. That fallback is the sort of detail most projects would omit.
The command line options, and a version number that has aged
The README reproduces the help output rather than describing the options in prose, which is the right call for a tool like this.
Usage: mailcatcher [options]
MailCatcher v0.8.0
--ip IP Set the ip address of both servers
--smtp-ip IP Set the ip address of the smtp server
--smtp-port PORT Set the port of the smtp server
--http-ip IP Set the ip address of the http server
--http-port PORT Set the port address of the http server
--messages-limit COUNT Only keep up to COUNT most recent messages
--http-path PATH Add a prefix to all HTTP paths
--no-quit Don't allow quitting the process
-f, --foreground Run in the foreground
-b, --browse Open web browser
-v, --verbose Be more verbose
-h, --help Display this help information
--version Display the current version`--messages-limit` is the one most teams should consider, because a long-running development instance otherwise accumulates every test email you have ever generated. `--http-path` matters when MailCatcher shares a machine with something else on port 1080. `--no-quit` exists for the reverse case, where you do not want a stray request to shut the process down. `-f` for foreground is what you want under a process manager or in a container.
Note the version in that sample output: it says v0.8.0. The Docker example further down prints v0.9.0, the newest published release is v0.10.0, and the Dockerfile defaults to building 0.11.0. The README's embedded samples were written once and never regenerated, so treat them as a shape rather than a specification, and use `--version` for the truth.
Wiring it to four frameworks
The README's most practically useful section is four short framework configurations, and it is worth having all four in one place.
For Rails, the recommendation is a block in `environments/development.rb`.
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = { :address => '127.0.0.1', :port => 1025 }
config.action_mailer.raise_delivery_errors = falseTurning off delivery errors is the line that stops a development environment from falling over when mail fails to send.
For PHP, which has no SMTP client, the route is a sendmail-compatible command.
sendmail_path = /usr/bin/env catchmail -f [email protected]That is what the separate `catchmail` binary is for: a sendmail analogue, which the README describes as making MailCatcher much easier from PHP. The same directive works in Apache configuration, and the README warns that if you installed under RVM you will need the literal path from `which catchmail` rather than `/usr/bin/env`, plus the same `--smtp-ip` and `--smtp-port` flags if you moved the SMTP listener off the defaults.
For Django, it is settings under a DEBUG check.
if DEBUG:
EMAIL_HOST = '127.0.0.1'
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
EMAIL_PORT = 1025
EMAIL_USE_TLS = FalseGating on DEBUG is what keeps the interception from happening in production, where sending to 127.0.0.1 would silently discard real mail.
The HTTP API is the part people miss
Under a heading called API, the README describes a fairly RESTful URL scheme that is more than a convenience for scripting.
`/messages` returns a list of messages in JSON. `/messages/:id.json` returns that message's metadata. Then the payload itself comes in pieces: `/messages/:id.html` and `/messages/:id.plain` for the default HTML and plain text versions, `/messages/:id/parts/:cid` for an individual attachment by content id, and `/messages/:id.source` for the whole raw message.
That set of endpoints turns MailCatcher from a viewer into an assertion target. A test can send a message, poll `/messages`, find the one it just sent by subject or recipient, and assert on the HTML or the raw source. The `.source` endpoint in particular is the way to test a mail template's headers, which is where a lot of mail bugs hide and where the web interface will not help you.
The catchmail-style structure also means you can build the same assertions in shell without any HTTP client beyond curl.
If you are wiring MailCatcher into CI rather than a laptop, this is the API you want, and `--messages-limit` becomes important because a shared test job should not accumulate state between runs.
The gem, the container, and a rule about Gemfiles
There are two ways to run it and one strong prohibition.
The prohibition comes first because it is the one most people get wrong. The README says plainly: please do not put mailcatcher into your Gemfile, because it will conflict with your application's gems at some point. The advice instead is to note in your project's own README that the team uses MailCatcher, and to run gem install and mailcatcher directly.
The reason is dependency pinning. MailCatcher brings its own HTTP server and its own native extension requirements, and those versions will eventually conflict with what your application pinned. If you use RVM, the README gives you a dedicated gemset and a wrapper script so the command is available regardless of which Ruby you are in.
rvm default@mailcatcher --create do gem install mailcatcherRuby 3.3 or newer is required, and the README is honest about the friction that causes: you may need build tools for the gem dependencies, `apt install build-essential` on Debian or Ubuntu, `xcode-select --install` on macOS, and a specific workaround for installing `thin` if it fails.
The container is the escape hatch from all of that.
$ docker run -p 1080:1080 -p 1025:1025 sj26/mailcatcherBoth ports need publishing, which is easy to forget. The README also notes that how those ports appear depends on your Docker configuration, and that MailCatcher listens on all the IPs available inside the container.
What the Dockerfile reveals, and where the project stops
The Dockerfile is more informative than the README about how this is built.
It starts from `ruby:4.0-alpine`, which is ahead of the Ruby 3.3 minimum the README states, so the container runs a newer interpreter than the gem requires. It takes a `VERSION` build argument that defaults to 0.11.0, and it accepts a mounted secret containing a locally built `.gem` file, installing that instead of fetching from RubyGems when present. That is a release-engineering convenience: the same Dockerfile builds the published image from the exact artifact you just built.
The dependency handling is tidy. Build tools, `libstdc++`, the SQLite libraries and SQLite headers are installed in one layer, then removed with `apk del --rdepends --purge build-base sqlite-dev` in the same layer, so they never reach the final image. SQLite is a runtime dependency, which tells you messages are stored on disk in a database rather than in memory. The entrypoint is `mailcatcher --foreground` with `--ip 0.0.0.0` as the default command, and both ports are exposed.
Then there are the caveats, and they are short. Mail processing is described as fairly basic but easily modified, with the instruction to fork and fix it or file an issue with the whole message attached. Encoding is described as difficult, and the README says MailCatcher does not completely support utf-8 straight over the wire, pointing you at a mail library instead.
The `examples/` directory backs that up: fourteen fixtures covering plain mail, multipart, attachments, inline images, dot-mail, quoted-printable HTML, XHTML and a case literally named breaking. That last one suggests the author ships the failing case alongside the working ones, which is a good sign about how the project handles the messy edges of SMTP.
Editorial conclusion
MailCatcher solves a problem so small that most teams solve it badly with a shared production inbox or by not looking at the mail their app sends. It catches everything on one port, shows it on another, and exposes the same data over HTTP, which means it can sit in CI as a test fixture rather than only on a developer's laptop. What it will not do is process mail properly, and the README says so. Encoding is the hard part of SMTP and this project explicitly declines to solve utf-8 straight over the wire, telling you to use a mail library instead. Everything else about the project is unusually candid: do not add it to your Gemfile, use a dedicated gemset, and expect to fork it if your mail is unusual. Read the caveats before you adopt it, decide between the gem and the container, and remember the version numbers in the README's own sample output are older than the releases.
Frequently asked questions
What is a mail catcher?
A mail catcher, also called a mail sink or a fake SMTP server, is a service that accepts outbound email on the SMTP port instead of delivering it anywhere, then shows you what arrived. Developers run one locally so they can read the notifications their application sends without sending real mail to real people. MailCatcher is one of these, paired with a web interface on a second port so you can read the captured messages as HTML.
Is SMTP still used today?
Yes, for a specific and shrinking role. Transactional email from applications, such as password resets and receipts, still overwhelmingly runs over SMTP because that is what mail providers accept. New product-to-product messaging has largely moved to HTTP APIs and message queues. For local development, SMTP remains the common interface because frameworks expose mail sending as SMTP settings, which is why tools that intercept it still exist.
Is there a free email server available?
If you mean something that receives mail from your application during development, that is exactly the free case, and MailCatcher is one option. If you mean a real inbox that receives mail from the internet on a permanent domain, that is a different problem with real costs around deliverability, spam filtering and reputation. The first is a local process; the second is a service.
Should mailcatcher go in my Gemfile?
No, and the README is unusually explicit about it. MailCatcher brings its own HTTP server and native dependencies, and those version requirements will eventually collide with what your application pinned. Install it as a separate gem, and if you use RVM put it in a dedicated gemset so the command is always available without entering the application's Ruby.
How do I assert on sent mail in a test?
Use the HTTP API rather than the web interface. POST to the SMTP port, poll `/messages` for JSON, locate the message you just sent, and fetch `/messages/:id.html`, `/messages/:id.plain` or `/messages/:id.source` for the content. The source endpoint is the one for checking headers. Setting a messages limit keeps a shared test job from accumulating state between runs.
Official sources
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.
[](https://hysenlabs.com/projects/sj26-mailcatcher)