Frits Stegmann
Setup your own mail server with Postfix and Dovecot

Install services via apt

apt update && \
apt install -y postfix postfix-pgsql postfix-pcre dovecot-core dovecot-imapd dovecot-lmtpd dovecot-pgsql postgresql mailutils dovecot-sieve dovecot-managesieved rspamd redis-server clamav clamav-daemon swaks net-tools unbound

Enable Mailbox Location

Edit /etc/dovecot/conf.d/10-mail.conf, look for mail_location, update it to the following

mail_location = maildir:~/Maildir

Enable LMTP

Update /etc/dovecot/conf.d/10-master.conf, look for the service lmtp section and update it to the following

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

Update sieve to only listen to localhost

Update /etc/dovecot/conf.d/20-managesieve.conf, change the contents to the following

protocol sieve {
}
service managesieve-login {
  inet_listener sieve {
    address = localhost
    port = 4190
  }
}

Update /etc/postfix/main.cf, Add the following to the end of the file

smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
broken_sasl_auth_clients = yes
smtpd_sasl_security_options = noanonymous
smtpd_sasl_tls_security_options = noanonymous, noplaintext
smtpd_sasl_authenticated_header = yes
virtual_transport = lmtp:unix:private/dovecot-lmtp
mailbox_transport = lmtp:unix:private/dovecot-lmtp

Update /etc/dovecot/conf.d/10-master.conf, Update service auth with

    service auth {
        unix_listener auth-userdb {
        }

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

Setup PostgreSQL database

Update /etc/postgresql/12/main/pg_ident.conf, Replace the file contents with the following

mailmap         dovecot                 mailreader
mailmap         postfix                 mailreader
mailmap         root                    mailreader

Update /etc/postgresql/12/main/pg_hba.conf, Replace the file contents with the following

local   all             postgres                                peer
local   mail            all                                     peer map=mailmap
host    roundcube       roundcube       127.0.0.1/32            md5

Restart PostgreSQL

systemctl restart postgresql

Create the mail database with the following schema

# sudo -u postgres psql

CREATE USER mailreader;
CREATE DATABASE mail WITH OWNER mailreader;

# psql -U mailreader -d mail

CREATE TABLE aliases (
    alias text NOT NULL,
    email text NOT NULL
);
CREATE TABLE users (
    email text NOT NULL,
    password text NOT NULL,
    maildir text NOT NULL,
    created timestamp with time zone DEFAULT now()
);
ALTER TABLE aliases OWNER TO mailreader;
ALTER TABLE users OWNER TO mailreader;

Add virtual user

# doveadm pw -s BLF-CRYPT -r 15
Enter new password: ...
Retype new password: ...
{BLF-CRYPT}.............................................................==
# psql -U mailreader -d mail
INSERT INTO users (
    email,
    password,
    maildir
) VALUES (
    'foo@localhost',
    '{BLF-CRYPT}.............................................................==',
    'foo/'
);

Setup Dovecot with virtual users, storing them in PostgreSQL

Create user and directories

adduser --system --no-create-home --uid 500 --group --disabled-password --disabled-login --gecos 'dovecot virtual mail user' vmail
mkdir /home/mailboxes
chown vmail:vmail /home/mailboxes
chmod 700 /home/mailboxes

Update /etc/dovecot/dovecot-sql.conf.ext, Replace the file contents with the following

driver = pgsql
connect = host=/var/run/postgresql/ dbname=mail user=mailreader
default_pass_scheme = BLF-CRYPT
password_query = SELECT email as user, password FROM users WHERE email = '%u'
user_query = SELECT email as user, 'maildir:/home/mailboxes/maildir/'||maildir as mail, '/home/mailboxes/home/'||maildir as home, 500 as uid, 500 as gid FROM users WHERE email = '%u'

Update /etc/dovecot/conf.d/10-auth.conf, replace the file contents with the following

auth_mechanisms = plain login
!include auth-sql.conf.ext

Setup Postfix for virtual users in PostgreSQL

Create /etc/postfix/pgsql-aliases.cf, and populate it with the following

user=mailreader
dbname=mail
table=aliases
select_field=alias
where_field=email
hosts=unix:/var/run/postgresql

Create /etc/postfix/pgsql-boxes.cf, and populate it with the following

user=mailreader
dbname=mail
table=users
select_field=email
where_field=email
hosts=unix:/var/run/postgresql

Update /etc/postfix/main.cf, add the following to the end of the file

alias_maps = hash:/etc/aliases proxy:pgsql:/etc/postfix/pgsql-aliases.cf
local_recipient_maps = proxy:pgsql:/etc/postfix/pgsql-boxes.cf $alias_maps

and removing the existing alias_maps

Setup mail folders

Edit /etc/dovecot/conf.d/10-mail.conf, replace the inbox namespace with the following

namespace inbox {
  inbox = yes
  separator = /

  mailbox "Drafts" {
    auto = subscribe
    special_use = \Drafts
  }
  mailbox "Sent" {
    auto = subscribe
    special_use = \Sent
  }
  mailbox "Trash" {
    auto = subscribe
    special_use = \Trash
  }
  mailbox "Junk" {
    auto = subscribe
    special_use = \Junk
  }
  mailbox "Archive" {
    auto = subscribe
    special_use = \Archive
  }
}

Rspamd

Update Postfix

Update /etc/postfix/main.cf, add the following to the end of the file

smtpd_milters = inet:localhost:11332
milter_default_action = accept
non_smtpd_milters = inet:localhost:11332

Update dovecot

Update /etc/dovecot/conf.d/20-lmtp.conf, replace the file contents with the following

protocol lmtp {
  postmaster_address = postmaster@localhost # replace localhost with your domain
  mail_plugins = $mail_plugins sieve
}

Update /etc/dovecot/conf.d/20-imap.conf, replace the file contents with the following

protocol imap {
  mail_plugins = $mail_plugins imap_sieve
}

Update /etc/dovecot/conf.d/90-plugin.conf, replace the file contents with the following

plugin {
  sieve_plugins = sieve_imapsieve sieve_extprograms
  sieve_implicit_extensions = +vnd.dovecot.report

  sieve_before = /home/mailboxes/sieve/global-spam.sieve

  # From elsewhere to Spam folder or flag changed in Spam folder
  imapsieve_mailbox1_name = Junk
  imapsieve_mailbox1_causes = COPY FLAG
  imapsieve_mailbox1_before = file:/home/mailboxes/sieve/report-spam.sieve

  # From Spam folder to elsewhere
  imapsieve_mailbox2_name = *
  imapsieve_mailbox2_from = Junk
  imapsieve_mailbox2_causes = COPY
  imapsieve_mailbox2_before = file:/home/mailboxes/sieve/report-ham.sieve

  sieve_pipe_bin_dir = /home/mailboxes/sieve

  sieve_global_extensions = +vnd.dovecot.pipe
}

Create the sieve directory and files

mkdir /home/mailboxes/sieve

Create /home/mailboxes/sieve/global-spam.sieve, and populate it with the following

require ["fileinto", "imap4flags"];

if header :contains "X-Spam-Flag" "YES" {
    addflag "\\Seen";
    fileinto "Junk";
}

if header :is "X-Spam" "Yes" {
    addflag "\\Seen";
    fileinto "Junk";
}

Create /home/mailboxes/sieve/learn-ham.sh, and populate it with the following

#!/bin/sh
exec /usr/bin/rspamc -h localhost:11334 learn_ham

Create /home/mailboxes/sieve/learn-spam.sh, and populate it with the following

#!/bin/sh
exec /usr/bin/rspamc -h localhost:11334 learn_spam

Create /home/mailboxes/sieve/report-ham.sieve, and populate it with the following

require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables", "vnd.dovecot.report"];

if environment :matches "imap.mailbox" "*" {
  set "mailbox" "${1}";
}

if string "${mailbox}" "Trash" {
  stop;
}

if string "${mailbox}" "Junk" {
  stop;
}

if environment :matches "imap.user" "*" {
  set "username" "${1}";
}

pipe :copy "learn-ham.sh" [ "${username}" ];

Create /home/mailboxes/sieve/report-spam.sieve, and populate it with the following

require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables", "imap4flags"];

if environment :matches "imap.user" "*" {
  set "username" "${1}";
}

setflag "\\Seen";

pipe :copy "learn-spam.sh" [ "${username}" ];

Update the permissions for the /home/mailboxes/sieve/ folder

chown -R vmail:vmail /home/mailboxes/sieve/ && \
chmod ug+x /home/mailboxes/sieve/learn-spam.sh && \
chmod ug+x /home/mailboxes/sieve/learn-ham.sh

Create files in /etc/rspamd/local.d/ to override configs

Update /etc/rspamd/options.inc, add the following to the dns section

nameserver = ["127.0.0.1:53:10"];

Create /etc/rspamd/local.d/actions.conf, and populate it with the following

reject = 150;
add_header = 6;
greylist = 4;

Create /etc/rspamd/local.d/dmarc.conf, and populate it with the following

servers = "127.0.0.1:6379";
actions = {
  quarantine = "add_header";
  reject = "reject";
}

reporting = true;

Create /etc/rspamd/local.d/spf.conf, and populate it with the following

spf_cache_size = 1k;
spf_cache_expire = 1d;
max_dns_nesting = 10;
max_dns_requests = 30;
min_cache_ttl = 5m;
disable_ipv6 = false;

Create /etc/rspamd/local.d/classifier-bayes.conf, and populate it with the following

servers = "127.0.0.1";
backend = "redis";
new_schema = true;
expire = 31536000;

Create /etc/rspamd/local.d/milter_headers.conf, and populate it with the following

use = ["x-spamd-bar", "authentication-results"];

extended_spam_headers = true;
skip_local = false;

routines {
  x-spamd-bar {
    negative = "";
  }
}

Install ClamAV

Rspamd

Create /etc/rspamd/local.d/antivirus.conf, and populate it with the following

clamav {
  attachments_only = false;
  action = "reject";
  symbol = "CLAM_VIRUS";
  type = "clamav";
  log_clean = true;
  servers = "127.0.0.1:3310";
}

Update /etc/clamav/clamd.conf, add the following to the end of the file

TCPSocket 3310
TCPAddr localhost

And remove the following configuration

LocalSocket

I’ve tried using the clamav socket but I think permissions issues is stopping it from running clamav and it’s failing silently so we are using the TCP socket for now, running netstat we can confirm the listening address is only localhost. I found restarting the server can break clamav’s unix socket. so I’ve removed it

Unofficial ClamAV signatures

The official signatures are a bit lacking so we download the unofficial ones

cd ~ && \
wget https://raw.githubusercontent.com/extremeshok/clamav-unofficial-sigs/master/clamav-unofficial-sigs.sh -O /usr/local/sbin/clamav-unofficial-sigs.sh  && \
chmod 755 /usr/local/sbin/clamav-unofficial-sigs.sh  && \
mkdir -p /etc/clamav-unofficial-sigs/  && \
wget https://raw.githubusercontent.com/extremeshok/clamav-unofficial-sigs/master/config/master.conf -O /etc/clamav-unofficial-sigs/master.conf  && \
wget https://raw.githubusercontent.com/extremeshok/clamav-unofficial-sigs/master/config/user.conf -O /etc/clamav-unofficial-sigs/user.conf  && \
os_conf="os.ubuntu.conf" && \
wget "https://raw.githubusercontent.com/extremeshok/clamav-unofficial-sigs/master/config/os/${os_conf}" -O /etc/clamav-unofficial-sigs/os.conf  && \
/usr/local/sbin/clamav-unofficial-sigs.sh --force  && \
/usr/local/sbin/clamav-unofficial-sigs.sh --install-logrotate  && \
/usr/local/sbin/clamav-unofficial-sigs.sh --install-man  && \
wget https://raw.githubusercontent.com/extremeshok/clamav-unofficial-sigs/master/systemd/clamav-unofficial-sigs.service -O /etc/systemd/system/clamav-unofficial-sigs.service  && \
wget https://raw.githubusercontent.com/extremeshok/clamav-unofficial-sigs/master/systemd/clamav-unofficial-sigs.timer -O /etc/systemd/system/clamav-unofficial-sigs.timer  && \
systemctl enable clamav-unofficial-sigs.service  && \
systemctl enable clamav-unofficial-sigs.timer && \
systemctl start clamav-unofficial-sigs.timer

Install Roundcube

apt install -y roundcube php7.4-pgsql composer

Follow the prompts and choose pgsql(PostgreSQL) as the database server

Update /etc/roundcube/config.inc.php by changing the value of $config['default_host'] to 'localhost'

Apache2 virtual host config

Create /etc/apache2/sites-available/mail.<domain>.conf, and populate it with the following

<VirtualHost *:80>
  ServerName mail.<domain>
  DocumentRoot /usr/share/roundcube/
  ServerAdmin postmaster@<domain>

  ErrorLog ${APACHE_LOG_DIR}/roundcube-error.log
  CustomLog ${APACHE_LOG_DIR}/roundcube-access.log combined

  <Directory /usr/share/roundcube/>
      Options -Indexes
      AllowOverride All
      Order allow,deny
      allow from all
  </Directory>
</VirtualHost>
cd /etc/apache2/sites-available
cd ../sites-enabled/
ln -s ../sites-available/mail.<domain>.conf

Check the configuration

apachectl -S

Add Certbot

We open up port 80 for Certbot HTTP validation

ufw allow 80 && \
apt install -y certbot python3-certbot-apache && \
certbot --apache

Add Certificates to Postfix and Dovecot

Apache2 configs after running Certbot

SSLCertificateFile /etc/letsencrypt/live/mail.<domain>/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/mail.<domain>/privkey.pem

Postfix

Update /etc/postfix/main.cf, update the following by replacing the lines

smtpd_tls_cert_file=/etc/letsencrypt/live/mail.<domain>/fullchain.pem
smtpd_tls_key_file=/etc/letsencrypt/live/mail.<domain>/privkey.pem

Dovecot

Update /etc/dovecot/conf.d/10-ssl.conf, update the following by replacing the lines

ssl_cert = </etc/letsencrypt/live/mail.<domain>/fullchain.pem # make sure to keep the <
ssl_key = </etc/letsencrypt/live/mail.<domain>/privkey.pem # make sure to keep the <

Getting Setup ready for opening up to the world

Rspamd DKIM

Run the following to create the DKIM

cd ~  && \
mkdir /var/lib/rspamd/dkim/  && \
rspamadm dkim_keygen -b 2048 -s mail -k /var/lib/rspamd/dkim/mail.key | tee -a  /var/lib/rspamd/dkim/mail.pub  && \
chown -R _rspamd: /var/lib/rspamd/dkim  && \
chmod 440 /var/lib/rspamd/dkim/*

Create /etc/rspamd/local.d/dkim_signing.conf, and populate it with the following

selector = "mail";
path = "/var/lib/rspamd/dkim/$selector.key";
allow_username_mismatch = true;

Copy the content for ARC

cp /etc/rspamd/local.d/dkim_signing.conf /etc/rspamd/local.d/arc.conf

Postfix

Update /etc/postfix/master.cf, add the following to the end of the file

submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_wrappermode=no
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject

Update /etc/postfix/main.cf by - changing the value of mydestination to $myhostname, localhost.$mydomain, $mydomain, localhost - add the following lines at the end of the file

masquerade_domains = $myorigin

IMAP

Update /etc/dovecot/conf.d/10-master.conf, update the imap-login to the below

service imap-login {
  inet_listener imap {
    address = 127.0.0.1
  }
  inet_listener imaps {
    ssl = yes
  }

  service_count = 1024
}

Update /etc/dovecot/conf.d/10-ssl.conf - update ssl to required - update #ssl_prefer_server_ciphers to yes and uncomment it

Firewall

ufw allow 443
ufw allow 25
ufw allow 587
ufw allow 993

Restart services

Restart postfix, rspamd and dovecot

systemctl restart apache2
systemctl restart clamav-daemon
systemctl restart postfix
systemctl restart rspamd
systemctl restart dovecot

DNS

DKIM & DMARC

Here is a good guide to setting up the DNS

https://linuxize.com/series/setting-up-and-configuring-a-mail-server/

SPF

v=spf1 mx ~all

Test settings

Double check services listening port

netstat -ltp

Spam mail test string

cd ~  && \
echo 'XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X' > body  && \
swaks --to foo@localhost --body body

Virus mail test string

cd ~  && \
wget https://secure.eicar.org/eicar.com  && \
swaks --to foo@localhost --attach - --server localhost < eicar.com

Ligitimate emails

swaks --to foo@localhost  && \
cat /home/mailboxes/maildir/foo/new/*

Check logs files

Check that rspamd marks the files either contains spam or a virus

tail -F /var/log/rspamd/rspamd.log -n 25

References