Docker: Running a Postfix container for testing mail during development

Update Jan 2022: If you would rather have a mail server running in Kubernetes, see my article here.

There are many reasons you might want your own Postfix server: maybe you need to test various relay settings, or validate the ability of your mail script to use TLS, or maybe you are a developer that needs to test multiple accounts without involving the infrastructure group.

In this article, I will show how to use the docker-mailserver image created by Thomas Vial to stand up a quick SMTP relay that can be used to test emails coming from an application during development.

Prerequisites

First, install Docker and Docker compose on your Ubuntu host as described in my previous article.

Configure docker-mailserver

Thomas Vial created the docker-mailserver image we will use in this article.  It includes a whole stack of services (antivirus, spam, security, DKIM, LetsEncrypt), but we will only use the most basic here.

Get the source
VERSION=11.3.1
git clone https://github.com/tomav/docker-mailserver.git -b v${VERSION}
cd docker-mailserver
Inspect configuration file

The mailserver.env contains the configuration values, take a quick look at the settings (full docs).

# is POP3 enabled?
grep ENABLE_POP3 mailserver.env

# is SSL enabled?
grep SSL_TYPE mailserver.env

# avoid errors where clients are rejected for coming from unknown network
# errors would look like 'NOQUEUE: reject: RCPT from unknown'
sed -i 's/PERMIT_DOCKER=.*/PERMIT_DOCKER=network/' mailserver.env

Start docker-mailserver

Now use Docker Compose to instantiate the container.

# use pinned version
sed -i "s/:latest/:$VERSION/" docker-compose.yml

# use set domain
sed -i 's/domainname:.*/domainname: domain.com/' docker-compose.yml

# bring up mail server in background
docker-compose up -d mailserver

And if you want to see the logs tailed as you perform other operations:

docker-compose logs -f mailserver

Create first users

Once the mail server image starts, you have two minutes to add a valid user account before the container stops.

# create mail account user/password
./setup.sh -i mailserver/docker-mailserver:$VERSION email add admin@domain.com admin123
./setup.sh -i mailserver/docker-mailserver:$VERSION email add flee@domain.com flee123

# list all email accounts
./setup.sh -i mailserver/docker-mailserver:$VERSION email list

Verify ports mapped to host

Make sure that the postfix container has mapped the postfix ports to local ports.  Per the official docs and docker-compose.yml, here are the exposed ports we should see:

  • 25 receiving email from other mailservers
  • 587 TLS Client email submission
  • 143 StartTLS IMAP client
  • 993 TLS/SSL IMAP client

Verify this with netstat and netcat:

# check listening ports on host running docker
sudo netstat -tulnp | grep -E "25|143|587|993"

# use host IP address
ip a
hostIP=192.168.1.xx

# check port access
for port in 25 143 587; do nc -w 1 -vz $hostIP $port; done

The hostIP above is going to be the IP address of your Docker host.  As we go through subsequent steps, we will continue to use this as the address of the Postfix services.

Verify basic email functionality

Send a test email using the swaks CLI utility:

sudo apt install -y swaks

# send test email to flee@domain.com
swaks --from no-reply@domain.com --to flee@domain.com --server $hostIP:25 -au flee@domain.com -ap flee123 --header "Subject: test" --body "testing 123"

# send test email to admin@domain.com 
swaks --from no-reply@domain.com --to admin@domain.com --server $hostIP:25 -au admin@domain.com -ap admin123 --header "Subject: test" --body "testing 456"

You should see output similar to below when sending these emails.

<- 220 mail.example.com ESMTP
-> EHLO bigboy
<- 250-mail.example.com
<- 250-PIPELINING
<- 250-SIZE 10240000
<- 250-ETRN
<- 250-AUTH PLAIN LOGIN
<- 250-AUTH=PLAIN LOGIN
<- 250-ENHANCEDSTATUSCODES
<- 250-8BITMIME
<- 250-DSN
<- 250 CHUNKING
-> AUTH LOGIN
<- 334 VXNlcm5hbWU6
-> YWRtaW5AZG9tYWluLmNvbQ==
<- 334 UGFzc3dvcmQ6
-> YWRtaW4xMjM=
<- 235 2.7.0 Authentication successful
-> MAIL FROM:<flee@domain.com>
<- 250 2.1.0 Ok
-> RCPT TO:<admin@domain.com>
<- 250 2.1.5 Ok
-> DATA
<- 354 End data with <CR><LF>.<CR><LF>
-> Date: Thu, 02 Feb 2023 13:17:21 -0500
-> To: admin@domain.com
-> From: flee@domain.com
-> Subject: test from flee
-> Message-Id: <20230202131721.1503248@bigboy>
-> X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
-> 
-> testing 456
-> 
-> 
-> .
<- 250 2.0.0 Ok: queued as 369DB5217D9
-> QUIT
<- 221 2.0.0 Bye
=== Connection closed with remote host.

Validating with desktop mail client

Any modern desktop mail client installed on the host should be able to connect to this Postfix server, in this example we will use Mozilla Thunderbird.

From Account Settings > Add Mail Account, you will get a name, email, and password.  Use the information from setup.sh earlier:

  • name = flee
  • email = flee@domain.com
  • password = flee123

Or for the admin user:

  • name = admin
  • email = admin@domain.com
  • password = admin123


And then press “Configure manually” to get advanced options.  Use the $hostIP value from earlier as the incoming IMAP server on port 143, and for the outgoing hostname on port 25.

Press “Re-test” and then “Done”.  Click on the email tab and you should see the email sent from swaks earlier.

If you followed my previous article on sending rich HTML email from Python, then invoking this script:

./send_html_email.py admin@domain.com flee@domain.com thesubject John $hostIP --port=25 --user=flee@domain.com --password=flee123 --attach attachments/test.txt attachments/testdocument.pdf

Should send a message like below to your flee@domain.com Inbox in Thunderbird.

Stopping mail server

docker-compose stop
docker-compose rm

 

REFERENCES

Thomas Vial, postfix image page

github tomav, postfix docker image source

github tomav, wiki page

github tomav, generating self-signed certificate

Postfix page

swaks man page

sefnet, sending mail using swaks

NOTES

View all docker container id and names

docker ps -a --format "{{.ID}} {{.Names}}"
Generate the self-signed certs
docker run -ti --rm -v "$(pwd)"/config/ssl:/tmp/docker-mailserver/ssl -h mail.domain.com -t tvial/docker-mailserver generate-ssl-certificate

This will have you first create a CA certificate, then issue a certificate based on that CA.  The CA common name MUST be “my-domain.com” and the leaf certificate common name MUST be “mail.my-domain.com”.

I used “passphrase” when asked for the passphrase.  On the “extras” sections, simply press <ENTER>.

Per the instructions in the docs:

Press enter
Enter a password when needed
Fill information like Country, Organisation name
Fill "my-domain.com" as FQDN for CA, and "mail.my-domain.com" for the certificate.
They HAVE to be different, otherwise you'll get a `TXT_DB error number 2`
Don't fill extras
Enter same password when needed

Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y

The certificates will be placed into the “config/ssl” directory.  I have pasted the output I got when filling these values out at the very bottom of this article in case you need to compare.