Tuesday, February 09, 2016

Setting up a mailinator-like test mail server with postfix and MailHog

The purpose of this exercise is to set up a mailinator-style mail server under our control. If you haven't used mailinator, it's a free service which provides an easy way to test sending email to random recipients. If you send mail to somebody189@mailinator.com and then go to
https://mailinator.com/inbox.jsp?to=somebody189, you will see a mailbox associated with that user, and any incoming email messages destined for that user. It's a handy way to also sign up for services that send confirmation emails.

I have been playing with MailHog, which is a mail server written in Golang for exactly the same purpose as mailinator. In fact, MailHog can happily intercept ANY recipient at ANY mail domain, provided it is set up properly. In my case, I didn't want to expose MailHog on port 25 externally, because that is a recipe for spam. Instead, I wanted to set up a regular postfix server for mydomain.com, then set up a catch-all user which will receive mail destined for anyuser@maildomain.com, and finally send all that mail to MailHog via procmail. Quite a few moving parts, but I got it to work and I am hastening to jot down my notes before I forget how I did it.

The nice thing about MailHog is that it provides a Web UI where you can eyeball the email messages you sent, including in raw format, and it also provides a JSON API which allows you to list messages and search for specific terms within messages. This last feature is very useful for end-to-end testing of your application's email sending capabilities.

I set up everything on a Google Cloud Engine instance running Ubuntu 14.04.
  • instance name: mailhog-mydomain-com
  • DNS/IP: mailhog.mydomain.com /
Install go 1.5.3 from source

First install the binaries for go 1.4, then compile go 1.5.

# apt-get update
# apt-get install build-essential git mercurial bazaar unzip
# cd /root
# wget https://storage.googleapis.com/golang/go1.4.3.linux-amd64.tar.gz
# tar xvfz go1.4.3.linux-amd64.tar.gz
# mv go go1.4
# git clone https://go.googlesource.com/go
# cd go
# git checkout go1.5.3
# cd src
# ./all.bash

# mkdir /opt/gocode
Edit /root/.bashrc and add:

export GOPATH=/opt/gocode
export PATH=$PATH:/root/go/bin:$GOPATH/bin

then source /root/.bashrc.

# go version
go version go1.5.3 linux/amd64

Set up postfix

Install postfix and mailutils

# apt-get install postfix mailutils

- specified System mail name as mydomain.com

Set up catch-all mail user (local Unix user)

# adduser catchall

Edit /etc/aliases and replace content with the lines below.

# cat /etc/aliases

# See man 5 aliases for format
mailer-daemon: postmaster
postmaster: root
nobody: root
hostmaster: root
usenet: root
news: root
webmaster: root
www: root
ftp: root
abuse: root
root: catchall


# newaliases
Edit /etc/postfix/main.cf and add lines:

luser_relay = catchall
local_recipient_maps =

Restart postfix:

# service postfix restart

Use Google Cloud Platform Web UI to add firewall rule called allow-smtp for the “default” network associated with the mailhog-mydomain-com instance. The rule allows incoming traffic from everywhere to port tcp:25.

Set up DNS

Add A record for mailhog.mydomain.com pointing to

Add MX record for catchallpayments.com pointing to mailhog.mydomain.com.

Test the incoming mail setup

Send mail to catchall@mydomain.com from gmail.

Run mail utility on GCE instance as user catchall:

catchall@mailhog-mydomain-com:~$ mail

"/var/mail/catchall": 1 message 1 new

>N 1 Some Body     Tue Feb 9 00:23 52/2595 test from gmail

? 1

Return-Path: <some.body@gmail.com>
X-Original-To: catchall@mydomain.com
Delivered-To: catchall@mydomain.com

Send mail to random user which doesn’t exist locally catchall333@mydomain.com and verify that user catchall receives it:

catchall@mailhog-mydomain-com:~$ mail

"/var/mail/catchall": 1 message 1 new

>N 1 Some Body     Tue Feb 9 18:32 52/2702 test 3

? 1

Return-Path: <some.body@gmail.com>
X-Original-To: catchall333@mydomain.com
Delivered-To: catchall333@mydomain.com

Install and configure MailHog

Get MailHog

# go get github.com/mailhog/MailHog
- this will drop several binaries in /opt/gocode/bin, including mhsendmail and MailHog (for reference, the code for mhsendmail is here)

# which MailHog

# which mhsendmail

Configure HTTP basic authentication

MailHog supports HTTP basic authentication via a file similar to .htpasswd. It uses bcrypt for password (see more details here). The MailHog binary can also generate passwords with bcrypt.

I created a password with MailHog:

# MailHog bcrypt somepassword

Then I created a file called .mailhogrc in /root and specified a user called mailhogapi with the password generated above:

# cat /root/.mailhogrc

Create upstart init file for MailHog

I specified the port MailHog listens on (I chose the same port as its default which is 1025) and the filed used for HTTP basic auth.

# cat /etc/init/mailhog.conf
# MailHog Test SMTP Server (Upstart unit)
description "MailHog Test SMTP Server"
start on (local-filesystems and net-device-up IFACE!=lo)
stop on runlevel [06]

exec /opt/gocode/bin/MailHog -smtp-bind-addr -auth-file /root/.mailhogrc
respawn limit 10 10
kill timeout 10

See more command line options for MailHog in this doc.

Start mailhog service

# start mailhog
mailhog start/running, process 25458

# ps -efd|grep Mail
root 7782 1 0 22:04 ? 00:00:00 /opt/gocode/bin/MailHog -smtp-bind-addr -auth-file /root/.mailhogrc

At this point MailHog is listening for SMTP messages on port 1025. It also provides a Web UI on default UI port 8025 and a JSON API also on port 8025.

Install procmail and configure it for user catchall

This is so messages addressed to user catchall (which again is our catch-all user) can get processed by a script via procmail.

# apt-get install procmail

Add this line to /etc/postfix/main.cf:

mailbox_command = /usr/bin/procmail -a "$EXTENSION" DEFAULT=$HOME/Maildir/ MAILDIR=$HOME/Maildir

(this will send all messages to procmail instead of individual user mailboxes)

Then su as user catchall and create .procmailrc file in its home directory:

catchall@mailhog-mydomain-com:~$ cat .procmailrc
| /opt/gocode/bin/mhsendmail --smtp-addr="localhost:1025"

This tells procmail to pipe the incoming mail message to mhsendmail, which will format it properly and pass it to port 1025, where MailHog is listening.

Test end-to-end

Use Google Cloud Platform Web UI to add firewall rule called allow-mailhog-ui for the “default” network associated with the mailhog-mydomain-com instance. The rule allows incoming traffic from everywhere to tcp:8025 (where the MailHog UI server listens). It’s OK to allow traffic to port 8025 from everywhere because it is protected via HTTP basic auth.

The MailHog UI is at http://mailhog.mydomain.com:8025

Any email sent to xyz@mydomain.com should appear in the MailHog Inbox.

By default, MailHog stores incoming messages in memory. Restarting MailHog (via ‘restart mailhog’ at the cmdline) will remove all messages.

MailHog also supports MongoDB as a persistent storage backend for incoming messages (exercise left to the reader.)

Use the MailHog JSON API to verify messages

List all messages:

$ curl --user 'mailhogapi:somepassword' -H "Content-Type: application/json" "http://mailhog.mydomain.com:8025/api/v2/messages"

Search messages for specific terms (for example for the recipient’s email):

$ curl -i --user 'mailhogapi:somepassword' -H "Content-Type: application/json" "http://mailhog.mydomain.com:8025/api/v2/search?kind=containing&query=test1%40mydomain.com"

See the MailHog API v2 docs here.

That's it, hope it makes your email sending testing more fun!

1 comment:

Will Emmerson said...

Thanks for posting this article.

Just one thing I ran into...and this might only affect those of us on Redhat/Centos but I couldn't install MailHog from Go using the EPEL bazaar version of 2.1. I received the following error when running 'go get github.com/mailhog/MailHog':

*** Bazaar has encountered an internal error. This probably indicates a
bug in Bazaar. You can help us fix it by filing a bug report at
including this traceback and a description of the problem.
package labix.org/v2/mgo: exit status 4

Instead I had to compile Bazaar version 2.7 and then run the go get github.com/mailhog/MailHog command again.