Self-hosting email is not rocket science

Inspired by a recent Hacker News comment, I’m publishing the notes I took when I set up my email server two years ago. This covers all the steps to get from a fresh Debian install to a working email setup. My setup features a spam filter and SPF, DKIM and DMARC for reliable delivery but is otherwise pretty minimal. I’ve been using this with Thunderbird and K-9 Mail for the past two years and it’s been great.

I’ve left my notes largely unedited, only adding a few explanations here and there. If you want to understand everything in detail you will have to read up on the topics yourself. The building blocks are Postfix, Dovecot and Rspamd.

DNS

Start by configuring DNS as shown in the zone file below. You’ll be generating a new DKIM key later, so leave that record blank for now. The imap and smtp subdomains are not strictly necessary but many email clients try them first when adding a new account.

@               IN  A     157.90.113.29
@               IN  AAAA  2a01:4f8:c2c:ef29::1
@               IN  MX    10 mail.tschumacher.net.
@               IN  TXT   "v=spf1 mx -all"
dkim._domainkey IN  TXT   "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQClpscaeydgCSiEdDTCU4YJxXGX7wK+xKCEMAvIGiq8XUo0pOA7K0rC02u15H97JV/JNpcAMUsHo5a4JePK2rufpL0o1/5X1bFhziyQkDHOPr4ZEYiE7EOp8Wo+nbyuR+0sdh+wLtyrVBlC6KRj1n2fqEM/58bN9dtnw6aY8cIY2QIDAQAB"
_dmarc          IN  TXT   "v=DMARC1; p=reject"
mail            IN  CNAME tschumacher.net.
imap            IN  CNAME tschumacher.net.
smtp            IN  CNAME tschumacher.net.
rspamd          IN  CNAME tschumacher.net.

Make sure reverse DNS is set up and resolves to tschumacher.net.

Dependencies

Install the Debian packages listed below. Nginx is required as a reverse proxy for the Rspamd web interface. Read here for why Rspamd recommends setting up Unbound as your own recursive DNS resolver. The resolvconf package is used by Unbound to set itself as the system DNS server in /etc/resolv.conf. Redis is used by Rspamd to cache ephemeral data.

nginx apache2-utils certbot python3-certbot-nginx postfix dovecot-imapd dovecot-lmtpd dovecot-sieve unbound resolvconf redis-server gpg

On the Rspamd downloads page the developers recommend against using the package from the official Debian repository and provide links to their own repository. First download their repository key:

curl https://rspamd.com/apt-stable/gpg.key | gpg --dearmor > /usr/share/keyrings/rspamd.gpg

Then add their repository to /etc/apt/sources.list. Change bookworm to the current Debian version. Check at http://rspamd.com/apt-stable/dists/ if it’s available first.

deb [signed-by=/usr/share/keyrings/rspamd.gpg] http://rspamd.com/apt-stable/ bookworm main

Finally run apt update and install rspamd.

Dovecot

Start by running Certbot to get an SSL certificate—this will automatically set up a cron job to renew the certificate regularly.

certbot certonly --nginx -d tschumacher.net,mail.tschumacher.net,imap.tschumacher.net,smtp.tschumacher.net,rspamd.tschumacher.net

We will begin by configuring Dovecot. In /etc/dovecot/conf.d/10-ssl.conf set the Let’s Encrypt certificate.

ssl_cert = </etc/letsencrypt/live/tschumacher.net/fullchain.pem
ssl_key = </etc/letsencrypt/live/tschumacher.net/privkey.pem

In /etc/dovecot/conf.d/10-master.conf enable the LMTP socket which will be used by Postfix to deliver incoming email to Dovecot.

service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0666
    user = postfix
    group = postfix
  }
}

In the same file enable the SASL socket which is used by Postfix to authenticate users for sending outgoing email.

service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0666
    user = postfix
    group = postfix
  }
}

In /etc/dovecot/conf.d/10-auth.conf configure the domain name to be stripped from usernames before they are looked up in /etc/passwd.

auth_username_format = %Ln

In /etc/dovecot/conf.d/10-mail.conf switch from mbox to Maildir.

mail_location = maildir:~/Maildir

In /etc/dovecot/conf.d/20-lmtp.conf add sieve to the list of loaded plugins.

mail_plugins = $mail_plugins sieve

Rspamd

We will move on to the configuration of Rspamd. Create /etc/rspamd/local.d/redis.conf to instruct Rspamd to use the local Redis server:

servers = "127.0.0.1";

Create /etc/rspamd/local.d/dkim_signing.conf to change the DKIM signing policy so that usernames don’t need to contain the domain name:

allow_username_mismatch = true;

Next, generate a DKIM key or copy the key from a previous installation. Create the directory /var/lib/rspamd/dkim if it doesn’t exist. Make sure the resulting TXT record is correctly set in the DNS zone.

rspamadm dkim_keygen -s dkim -d tschumacher.net -k /var/lib/rspamd/dkim/tschumacher.net.dkim.key

Add the Nginx site /etc/nginx/sites-available/rspamd as a reverse proxy for the Rspamd web interface and enable it:

server {
    listen 80;
    listen [::]:80;
    server_name rspamd.tschumacher.net;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name rspamd.tschumacher.net;
    ssl_certificate /etc/letsencrypt/live/tschumacher.net/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/tschumacher.net/privkey.pem;
    add_header Strict-Transport-Security "max-age=31536000" always;

    auth_basic "closed site";
    auth_basic_user_file htpasswd;

    location / {
        root /usr/share/rspamd/www/;
        try_files $uri @proxy;
    }
    location @proxy {
        proxy_pass http://127.0.0.1:11334;
    }
}

Then create the password file that is used to protect the Rspamd web interface.

htpasswd -c /etc/nginx/htpasswd tim

Postfix

Finally we will tend to the configuration of Postfix. In /etc/postfix/main.cf set the Let’s Encrypt certificate and enable opportunistic TLS.

smtpd_tls_cert_file=/etc/letsencrypt/live/tschumacher.net/fullchain.pem
smtpd_tls_key_file=/etc/letsencrypt/live/tschumacher.net/privkey.pem
smtpd_tls_security_level = may
smtp_tls_security_level = may

In the same file configure Postfix to talk to Dovecot via the previously configured LMTP and SASL sockets.

mailbox_transport = lmtp:unix:private/dovecot-lmtp
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth

And in the same file configure Rspamd as a milter for Postfix.

smtpd_milters = inet:localhost:11332
milter_default_action = accept
milter_protocol = 6

In /etc/postfix/master.cf uncomment the following lines to start a separate Postfix worker on port 465. This worker will act as a Message Submission Agent (MTA). SASL is only enabled for this worker.

submissions     inet  n       -       y       -       -       smtpd
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes

Final steps

Now that Dovecot, Rspamd and Postfix are configured we can move on. Next create the user tim and set a password. This is the password that will be used by the email client for both sending mail via SMTPS and fetching mail via IMAPS.

In /etc/aliases redirect mail destined for postmaster to tim instead of root. Run newaliases after making changes to /etc/aliases.

The file /home/tim/.dovecot.sieve is a script in the Sieve language for filtering and sorting incoming mail similar in capabilities to procmail. At the very least mail classified as spam by Rspamd should be placed in the Spam folder. Note that when adding new fileinto rules new folders have to be created by hand e.g. mkdir ~/Maildir/.Spam or Dovecot will produce an error.

require ["fileinto"];

if header :is "X-Spam" "Yes" {
    fileinto "Spam";
}

This completes the email setup. Proper functioning of opportunistic TLS and also SPF, DKIM and DMARC can be tested with mail-tester.com or Gmail.

Go to the front page