Setup a production-ready exim/dovecot server
In this tutorial, I will introduce step by step how to set up a full-fledged email server.
At the end of the tutorial, the email server will support
- SMTP server to receive email
- SMTP to send email
- Receiving SMTP server supports TLS
- Sending SMTP server support DKIM
- Configure DMARC, SPF records
- IMAP/POP3 server with dovecot
- (TODO) Automatically renew TLS certificate with letsencrypt
- (TODO) Periodically backup email data to S3
- (TODO) Setup a simple web-based client
Refer to this post to know how to test sending SMTP/receiving SMTP/POP3/IMAP with or without TLS servers.
My server information:
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04 LTS
Release: 20.04
Codename: focal
$ uname -a
Linux ip-10-0-20-69 5.4.0-1018-aws #18-Ubuntu SMP Wed Jun 24 01:15:00 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
Brief introduction of basic terms and concepts
Alice wants to send Bob an email. To accomplish this, Alice needs the following information:
- A
from
email address. For example:alice@phanthuha.me
. - A
to
email address. For example:bob@transang.me
. - The email payload such as body, title, reply-to, subject, attachment, etc. For example:
lorem ipsum
. - An SMTP agent server, for example:
smtp.emailagent.com
. - The credential (usually are username and password) to access and authorize the email
alice@phanthuha.me
at the SMTP agent server.
On the other hand, when sending the email request (from: alice@phanthuha.me
, to: bob@transang.me
, payload: lorem ipsum
), the SMTP agent server prepares the following information:
- A
reply-to
email address. For example:mailer@emailagentmail.com
. Note: this can be any domain value regardless of thefrom
email's domain. Of course, this domain and the associated email agent server must be correctly configured in conjunction with thefrom
email's domain. And this is what we are going to set up in this tutorial. - A random string value called
domainkey
. For example:a_random_domainkey
. - A pair of pre-configured private/public key with the public key propagated in a specific DNS record called DKIM record at the
from
email address's domain.
Step 1: In an email application (Gmail, outlook, thunderbird, etc.), Alice use her credential to access the email agent server at smtp.emailagent.com
with a request to send an email with the content.
{
from: 'alice@phanthuha.me',
to: 'bob@transang.me',
payload: 'lorem ipsum'
}
Step 2: The email application does a DNS reverse of smtp.emailagent.com
to obtain the associated IP address 1.2.3.4
(at server X) and send the email sending a request to this ip address.
Step 3: Server X authorizes the request with the provided credential. X sees the receiver email bob@transang.me
, it looks for the MX record of transang.me
and gets a value. For example: mail.transang.me
. X does a DNS reverse of mail.transang.me
to get an IP address 4.3.2.1
of the receiver server Y.
Step 4: X add reply-to
(mailer@emailagentmail.com
) and domainkey
(a_random_domainkey
) to the request. Now the email payload becomes:
{
from: 'alice@phanthuha.me',
to: 'bob@transang.me',
payload: 'lorem ipsum',
replyTo: 'mailer@emailagentmail.com',
domainkey: 'a_random_domainkey'
}
Step 5: X uses the pre-configured private key associated with the DKIM record to sign the email (i.e., it attaches a signature along with the email payload).
Step 6: X sends the encoded payload to Y, at the Network Layer, this request is sent from 1.2.3.4
to 4.3.2.1
.
Step 7: Y decodes the email payload, and reveals the email's payload in Step 4 along with the signature. At the Network Layer, Y also knows that this email is sent from the IP address 1.2.3.4
.
If the to
address is somewhat another email that is not controlled by Y. Y can just reject or ignore the email.
Step 8: Y checks a TXT DNS record of emailagentmail.com
to get the list of allowed-sender IP addresses. This record is called SPF record. If 1.2.3.4
is not on the list, go to step 10, otherwise, go to step 9.
Step 9: Y gets the public key configured at the TXT DNS record at a_random_domainkey._domainkey.phathuha.me
and verifies the email's signature. If the signature is correct, accept the email and put it in the associated mailbox. Notify Bob to check his new email from Alice and finish the process. Otherwise, go to step 10.
Step 10: In this step, Y has already rejected the email. However, there is one more step to improve email server management. Y checks the value of the TXT DNS record at _dmarc.phanthuha.me
and gets an email address to report the incident (to the admin of phanthuha.me domain). This DNS record is called DMARC record. For example, Y gets tr@nsang.me
as the report target.
Step 11: Because nsang.me
is different from phanthuha.me
, Y needs to make sure that the admin of nsang.me
allow to receive SPF failure or DKIM failure reports for phanthuha.me
. Y checks for the TXT DNS record at phanthuha.me._report._dmarc.nsang.me
. If there is an expected value (of v=DMARC1
), Y sends a DMARC report to tr@nsang.me
.
Email forwarding
There is another scenario that is worth looking at.
A sends email mail@a.com
to B's email mail@b.com
. However, B setups a forwarding server to forward all incoming emails to mail@b.com
to b@gmail.com
.
A owns mail@a.com
, and B owns both mail@b.com
and b@gmail.com
.
Something will happen in this case.
- The forwarding server sends the email to
b@gmail.com
with the from address beingmail@a.com
from a server with IP address different from whatmail@a.com
was sent from. Basically, and mostly SPF checks will fail. - In this case, DKIM comes and plays the main role in preventing the email from being spoofed.
- Because the forwarding server sends the email at the end, it keeps the responsibility for the success of the sending. So it should rewrite the
Return-Path
header in the email. Also, to prevent the DKIM integrity, it shouldn't modify the email body. - If the original email (from
mail@a.com
) is not signed with DKIM and the forwarding server keeps forwarding it without any modification. The email will not pass the DMARC check (because none of SPF or DKIM checks passes). It is common that the email agent (such as ofb@gmail.com
) has its own heuristic to overwrite the behaviour defined in DMARC by the sender provider (ofmail@a.com
). - To avoid being marked as spam when forwarding an email without DKIM, the forwarding server might choose to rewrite the FROM field in the email to be forwarded.
- The forwarding server might choose to implement Sender Rewriting Scheme (SRS) or Authenticated Received Chain (ARC) to improve the forward email reliability.
Some useful resources:
- https://github.com/forwardemail/free-email-forwarding
- https://serverfault.com/a/1105357/318072
- https://pieterhollander.nl/post/mailserver/
- https://postmarkapp.com/blog/what-is-arc-or-authenticated-received-chain
- https://www.namecheap.com/support/knowledgebase/article.aspx/10204/2214/email-forwarding-why-its-important-to-have-srs-configured/
Part 1: Install exim4
sudo apt install -y exim4 certbot
- Allow port 80 (HTTP for letsencrypt), 25 (SMTP) in iptables/security group/network ACL, etc.
- Generate a dkim key
To check the username who is running the exim4 process: ps aux | grep exim
. In ubuntu, the username is Debian-exim
sudo openssl genrsa -out /etc/exim4/dkim.pem 2048
openssl rsa -in /etc/exim4/dkim.pem -pubout -out dkim.pub -outform PEM
sudo chown Debian-exim:Debian-exim /etc/exim4/dkim.pem
cat dkim.pub
copy the result to set up the DNS txt dkim record in the next subsection "Setup DNS records".
- Setup DNS records
From the administrator panel of the domain, add the following records.
===========annotation explanation starts===========
From now on, I will this format for DNS records <x> / <y> / <z> / <t>
where
-<x>
is the record type ( mx
, A
, TXT
, CNAME
)
- <y>
is target domain. For example:
+ mail
is for mail.example.com
with example.com
is the top-level domain
+ mail.transang.me.
is for mail.transang.me
(notice the ending dot).
+ @
is for the root level domain, for example example.com
.
The true annotation depends on your DNS provider. The provider I am using is Namecheap.
- <z>
is the value of the record, it can be
+ an IP address for A
type
+ a domain for CNAME
type
+ a text value for TXT
type
- <t>
only exists in MX
record, and is the priority value.
===========annotation explanation ends============
+ For receiver server: (replace the ip address 54.199.209.24
with your value of the ip of the receiver server).mx / @ / mail / 10
A / mail / 54.199.209.24
+ Sender server: A / smtp / 54.199.209.24
. (replace the ip address 54.199.209.24
with your value of the ip of the sender server). Here I am use the same server for sending and receiving.
+ SPF record: TXT / @ / "v=spf1 ip4:54.199.209.24 -all"'
(replace the ip address 54.199.209.24
with your value of the sender server).
+ DMARC record: TXT / _dmarc / v=DMARC1\;p=reject\;rua=mailto:tr@nsang.me\;ruf=mailto:tr@nsang.me\;rf=afrf\;pct=100
. Note that the rua/ruf email target is to a nsang.me domain. Because this target domain (nsang.me) is different from the being setup domain (phanthuha.me), it requires an additional record in the nsang.me domain (see next).
+ DKIM record.
Note 1: mail
is a key selector and can be an arbitrary value.
Note 2: DNS records are limited by 255 characters. Some DNS providers support long DNS records (with namecheap, up to 2500 characters) in the panel. In fact, they (the providers) separate the long value to multiple less-than-256 characters entries under the hood.
TXT / mail._domainkey / k=rsa; t=s; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo+/XQCEHaV/3Yp9ZWQ0r1i9Eyb8l8ZPOdFvyZIFFRI2ZLQo86fyDu8HSs2MZXC0wtr1Ec5STsyG1kAmai+OOgN9JS1g050NCJFXAjd0fhxymWSsG+bHvP70BirwU7oaiUCsJ8H9cKUdZ4BkiHQfJ1k6+D4jMTra62hEMgkg8cillG9reW69PfwF4hhiYg+qNcUm1lxYFPAqLbxD5n2wG9f6bhSuntVomwGPAnUMivfi0lpb+rLM9XW1RUZI/aDjI5imfMhhTefRPW4pPnnhtOwyMMs78jccmEZroCcWVD3wMTgZbxQyB2nZRm/80sLrP+HDZMMZUlpK8pZCC6pTPywIDAQAB
The long value MIIBI...
is the value copied in the DKIM generating step.
+ In nsang.me, add DMARC report record: TXT / phanthuha.me._report._dmarc / v=DMARC1
- Add user group. This group will be used for the dovecot user and exim user to read cert keys generated by letsencrypt.
sudo groupadd mailers
sudo usermod -aG mailers Debian-exim
- Get TLS certificates with certbot
sudo certbot certonly -d smtp.phanthuha.me --standalone --agree-tos -m tr@nsang.me -n
The cert key files are at:
+ Public cert and chain: /etc/letsencrypt/live/smtp.phanthuha.me/fullchain.pem
+ Key file (private): /etc/letsencrypt/live/smtp.phanthuha.me/privkey.pem
- Allow exim user to access the private key
sudo chgrp mailers /etc/letsencrypt/{live,archive}{,/smtp.phanthuha.me} /etc/letsencrypt/archive/smtp.phanthuha.me/privkey1.pem
sudo chmod g+x /etc/letsencrypt/{live,archive}
sudo chmod g+r /etc/letsencrypt/archive/smtp.phanthuha.me/privkey1.pem
- (optional) Configure exim4 from interactively:
sudo dpkg-reconfigure exim4
This script merely updates the configuration file at /etc/exim4/update-exim4.conf.conf
.
The default content of this file is as follow:
# /etc/exim4/update-exim4.conf.conf
#
# Edit this file and /etc/mailname by hand and execute update-exim4.conf
# yourself or use 'dpkg-reconfigure exim4-config'
#
# Please note that this is _not_ a dpkg-conffile and that automatic changes
# to this file might happen. The code handling this will honor your local
# changes, so this is usually fine, but will break local schemes that mess
# around with multiple versions of the file.
#
# update-exim4.conf uses this file to determine variable values to generate
# exim configuration macros for the configuration file.
#
# Most settings found in here do have corresponding questions in the
# Debconf configuration, but not all of them.
#
# This is a Debian specific file
dc_eximconfig_configtype='local'
dc_other_hostnames='ip-10-0-20-69.ap-northeast-1.compute.internal'
dc_local_interfaces='127.0.0.1 ; ::1'
dc_readhost=''
dc_relay_domains=''
dc_minimaldns='false'
dc_relay_nets=''
dc_smarthost=''
CFILEMODE='644'
dc_use_split_config='false'
dc_hide_mailname=''
dc_mailname_in_oh='true'
dc_localdelivery='mail_spool'
My config (comment stripped)
dc_eximconfig_configtype='internet'
dc_other_hostnames='phanthuha.me'
dc_local_interfaces=''
dc_readhost=''
dc_relay_domains=''
dc_minimaldns='false'
dc_relay_nets=''
dc_smarthost=''
CFILEMODE='644'
dc_use_split_config='false'
dc_hide_mailname=''
dc_mailname_in_oh='true'
dc_localdelivery='maildir_home'
If you change the content of this file or the configuration in /etc/exim4/exim4.conf.template
or /etc/exim4/conf.d
, you need to execute sudo update-exim4.conf
to generate and update the updated configuration.
From exim4 manpage
The script update-exim4.conf generates the main configuration files /var/lib/exim4/config.autogenerated for Exim v4 by merging the data in the template file /etc/exim4/exim4.conf.template or the ones in the /etc/exim4/conf.d directory tree respectively and /etc/exim4/update-exim4.conf.conf to the output file /var/lib/exim4/config.autogenerated.
If dc_use_split_config in /etc/exim4/update-exim4.conf.conf specifies a split configuration, update-exim4.conf processes the /etc/exim4/conf.d subdirectories in the order main, acl, router, transport, retry, rewrite and auth. Within each directory it takes files in lexical sort order by file name. It concatenates all these files and makes the debconf replacement described below.
If you are not using split configuration update-exim4.conf concatenates /etc/exim4/exim4.conf.localmacros (if this file exists) and /etc/exim4/exim4.conf.template (in this order) and makes the debconf replacement described below.
For some configuration, it requires restarting the exim4 service with sudo systemctl restart exim4
.
- Config the exim4 server:
sudo vim /etc/exim4/exim4.conf.template
+ TLS
MAIN_TLS_ENABLE = true
MAIN_TLS_CERTIFICATE = /etc/letsencrypt/live/smtp.phanthuha.me/fullchain.pem
MAIN_TLS_PRIVATEKEY = /etc/letsencrypt/live/smtp.phanthuha.me/privkey.pem
+ Enable authentication by uncommenting these lines:
login_server:
driver = plaintext
public_name = LOGIN
server_prompts = "Username:: : Password::"
server_condition = "${if crypteq{$auth2}{${extract{1}{:}{${lookup{$auth1}lsearch{CONFDIR/passwd}{$value}{*:*}}}}}{1}{0}}"
server_set_id = $auth1
.ifndef AUTH_SERVER_ALLOW_NOTLS_PASSWORDS
server_advertise_condition = ${if eq{$tls_in_cipher}{}{}{*}}
.endif
+ Enable DKIM by adding:
DKIM_DOMAIN=phanthuha.me
DKIM_SELECTOR=mail
DKIM_PRIVATE_KEY=/etc/exim4/dkim.pem
Update the configuration with sudo update-exim4.conf && sudo systemctl restart exim4
Note: changing MAIN_TLS_CERTKEY
and MAIN_TLS_PRIVATEKEY
requires exim to be restarted: sudo systemctl restart exim4
.
- Check configuration:
+exim4 -bV
+ps aux | grep exim
Debian-+ 32196 0.0 0.1 16376 4516 ? Ss 21:21 0:00 /usr/sbin/exim4 -bd -q30m
- Alias for email receiving:
Suppose that the OS username is ubuntu
, we have an associated email ubuntu@phanthuha.me
at /home/ubuntu/Maildir
. To accept all emails sent to me@phanthuha.me
and store in the mailbox for the ubuntu
user, we need to config alias.sudo vim /etc/aliases
, add me: ubuntu
.
- Email addresses for sending
By default, when the ubuntu
user calls exim to send an email from me@phanthuha.me
, exim will use ubuntu@phanthuha.me
in the envelope from
and return path
fields. To overwrite this config,sudo vim /etc/email-addresses
, add ubuntu: me@phanthuha.me
.
Now: me@phanthu.me
is in used instead.
- Configure the remote SMTP connection (username/password).
+ Install package to get password hash sudo apt install -yq whois
+ mkpasswd
to get a password. For example real password mypassword
is hashed as tQ1xW9c3f5OP.
+ Add credential username = me
, password = mypassword
by sudo vim /etc/exim4/passwd
, add me:
tQ1xW9c3f5OP.
Note: adding/removing/modifying the user database does not require restarting the server or running update-exim4.conf
- Use mxtoolbox's deliverability tool to test:
exim -v ping@tools.mxtoolbox.com
. Enter the following content
From: me@phanthuha.me
To: ping@tools.mxtoolbox.com
Subject: Test email from exim
hello. this is the test email's body
Sang
Ctrl-D
to finish.
- Setup Gmail to sending using the setup SMTP server
+ name: any
+ email:me@phanthuha.me
+ smtp server:smtp.phanthuha.me
+ username:me
+ password:mypassword
+ secured connection using TLS
Setup dovecot
- Install dovecot
sudo apt install -yq dovecot-imapd dovecot-pop3d
- Open ports: 110 (POP3), 995 (POP3S), 143 (IMAP), 993 (IMAPS) in iptables/security group/network ACL/etc.
- In DNS panel, add
A / imap / 54.199.209.24
. Note: we will share the IMAP and pop servers atimap.phanthuha.me
. - Generate TLS cert
sudo certbot certonly -d imap.phanthuha.me --standalone --agree-tos -m tr@nsang.me -n
- Add dovecot user to the
mailers
group:sudo usermod -aG mailers dovecot
- Allow the
mailers
group to read the TLS private key
sudo chgrp mailers /etc/letsencrypt/{live,archive}{,/imap.phanthuha.me} /etc/letsencrypt/archive/imap.phanthuha.me/privkey1.pem
sudo chmod g+r /etc/letsencrypt/archive/imap.phanthuha.me/privkey1.pem
- Config dovecot
+ Setup keychain. In/etc/dovecot/conf.d/10-ssl.conf
,
change lines
ssl_cert = </etc/dovecot/private/dovecot.pem
ssl_key = </etc/dovecot/private/dovecot.key
to
ssl_cert = </etc/letsencrypt/live/imap.phanthuha.me/fullchain.pem
ssl_key = </etc/letsencrypt/live/imap.phanthuha.me/privkey.pem
+ In the same dir, in file 10-auth.conf
,
change line auth_mechanism = plain
to auth_mechanism = dovecot_plain dovecot_login
And, uncomment !include auth-passwdfile.conf.ext
.
And, comment out #!includeauth-system.conf.ext
.
+ In the same dir, in file 10-master.conf
, in the block service auth { unix_listener ...
, uncomment and change to the following
service auth {
unix_listener auth-userdb {
mode = 0660
user = mail
}
}
+ In the same dir, in file 10-mail.conf
change line mail_location = mbox:~/mail:INBOX=/var/mail/%u
to mail_location = maildir:~/Maildir
+ In the same dir, in the file 10-logging.conf
, uncomment log_path = syslog
- Add dovecot credentials. Create file
sudo touch /etc/dovecot/users
- Add user. For example with username =
me
, password =mypassword
.
+ Usesudo doveadm pw
to generate the password. For e.g.:{CRYPT}$2y$05$pFZ8zDO.o.FtcTIWNOTqdeTgRj0OmoxzK2HineVAKEv91DEP4DXY6
+ Add following entry in/etc/dovecot/users
me:{CRYPT}$2y$05$pFZ8zDO.o.FtcTIWNOTqdeTgRj0OmoxzK2HineVAKEv91DEP4DXY6:1000:1000::/home/ubuntu:/bin/bash:userdb_mail=maildir:/home/ubuntu/Maildir
Check this dovecot document for the meanings of these fields.
- Restart dovecot:
sudo systemctl restart dovecot
- Test the created dovecot credential:
sudo doveadm auth login me
- Setup POP3 in Gmail
+ Email address:me@phanthuha.me
+ Gmailify: no support
+ Username:me
+ Password:mypassword
+ POP server:imap.phanthuha.me
+ Port: 995
+ Check "Always use a secure connection (SSL) when receiving mail.
+ All other 3 checkboxes are optional and up to your preferences.
Helpful commands and information
- To test an SMTP connection:
swaks -a -tls -q HELO -s smtp.phanthuha.me -au test -ap '<>'
- Exim log dir:
tail -f /var/log/exim4/mainlog
. Also, checkrejectlog
, andpaniclog
. - View exim stat:
eximstats /var/log/exim4/mainlog
- List all listening ports/running PID/running user:
sudo lsof -i -P
- Get dovecot user information:
sudo doveadm user me
- To debug userdb/passwd in dovecot. Add
auth_debug_passwords = yes
auth_debug = yes
- Watch dovecot log with
tail -f /var/log/syslog
TODO
- There are two ways of storing emails in the Linux OS: mbox and maildir. They are both used widely and are not compatible with the other.
Personally, I would recommend maildir over mbox (in short, maildir is more structure and safer). In this post, I have introduced to change dovecot config from using mbox to maildir. And, in my installation, Exim uses maildir by default. However, according to @Solaris (see his comment at the end), in some distributions, mbox is the default for Exim. I will investigate the detection and configuration of exim for this latter. - Test if exim/dovecot requires a restart if TLS cert keys change. Hint: use openssl s_client to reveal the current certificates. (refer Test SMTP/IMAP(S)/POP3(S) configuration from the command line)
- Support multiple domains
- Periodically backup to AWS s3.