Personal email server

Linux

Given that GMail with cease to provide free hosting service for custom domains I've decided to deploy a fully featured personal email server and migrate everything to my Arch Linux box.

mail-tester.com tests reports on Jul 7, 2022 and Jul 29, 2023 look pretty good, 10 out of 10. All the details below, let's begin…

1. Dovecot

One of the most popular IMAP/POP3 server is Dovecot.

1.1 Installation

  pacman -S dovecot

1.2 Configuration

We are going to use doveconf tool or just cat the files when needed.

Default config
  mkdir /etc/dovecot
  cp -r /usr/share/doc/dovecot/example-config/ /etc/dovecot

Just copy the default provided config, very good as a starting point.

Mail location and type
  doveconf -n mail_location namespace inbox
mail_location = mbox:~/mail:INBOX=/var/mail/%u
namespace inbox {
  inbox = yes
  location =
  mailbox Drafts {
    special_use = \Drafts
  }
  mailbox Junk {
    special_use = \Junk
  }
  mailbox Sent {
    special_use = \Sent
  }
  mailbox "Sent Messages" {
    special_use = \Sent
  }
  mailbox Trash {
    special_use = \Trash
  }
  prefix =
}

mail_location is one of the most important config, it basically says that it expects MTA (Postfix) to deliver email in INBOX=/var/mail/USERNAME and store mailboxes in ~/mail

Add dovecot to mail group
  usermod -aG mail dovecot
  id dovecot
uid=76(dovecot) gid=76(dovecot) groups=76(dovecot),12(mail)

Adds mail group to dovecot user that will allow MTA to have access to /var/mail dir.

SSL certificates

Check Wildcard SSL certificate blog post to see how to aquire a Let's Encrypt SSL certificate; or generate a self-signed one below.

Self-signed certificate
config
  cat /etc/ssl/dovecot-openssl.cnf
[ req ]
default_bits = 2048
encrypt_key = yes
distinguished_name = req_dn
x509_extensions = cert_type
prompt = no

[ req_dn ]
# country (2 letter code)
C=RO

# State or Province Name (full name)
ST=Iasi

# Locality Name (eg. city)
L=Iasi

# Organization (eg. company)
O=Matrix

# Organizational Unit Name (eg. section)
OU=Email server

# Common Name (*.example.com is also possible)
CN=*.costan.ro

# E-mail contact
emailAddress=postmaster@costan.ro

[ cert_type ]
nsCertType = server
generate
  cat /usr/lib/dovecot/mkcert.sh
#!/bin/sh

# Generates a self-signed certificate.
# Edit dovecot-openssl.cnf before running this.

umask 077
OPENSSL=${OPENSSL-openssl}
SSLDIR=${SSLDIR-/etc/ssl}
OPENSSLCONFIG=${OPENSSLCONFIG- /etc/ssl/dovecot-openssl.cnf}

CERTDIR=$SSLDIR/certs
KEYDIR=$SSLDIR/private

CERTFILE=$CERTDIR/dovecot.pem
KEYFILE=$KEYDIR/dovecot.pem

if [ ! -d $CERTDIR ]; then
  echo "$SSLDIR/certs directory doesn't exist"
  exit 1
fi

if [ ! -d $KEYDIR ]; then
  echo "$SSLDIR/private directory doesn't exist"
  exit 1
fi

if [ -f $CERTFILE ]; then
  echo "$CERTFILE already exists, won't overwrite"
  exit 1
fi

if [ -f $KEYFILE ]; then
  echo "$KEYFILE already exists, won't overwrite"
  exit 1
fi

$OPENSSL req -new -x509 -nodes -config $OPENSSLCONFIG -out $CERTFILE -keyout $KEYFILE -days 365 || exit 2
chmod 0600 $KEYFILE
echo
$OPENSSL x509 -subject -fingerprint -noout -in $CERTFILE || exit 2

Generate /etc/ssl/{certs,private}/dovecot.pem cert files.

  openssl dhparam -out /etc/dovecot/dh.pem 4096

Generate /etc/dovecot/dh.pem file.

SSL
  doveconf ssl ssl_cert ssl_key ssl_dh ssl_require_crl
ssl = yes
ssl_cert = </etc/letsencrypt/live/costan.ro/fullchain.pem
ssl_key = </etc/letsencrypt/live/costan.ro/privkey.pem
ssl_dh = </etc/dovecot/dh.pem
ssl_require_crl = yes

Path to SSL certificate files.

Authentication
  doveconf -n userdb passdb
userdb {
  driver = passwd
}
passdb {
  driver = pam
}

Where/how user/pass is looked up, users in /etc/passwd and passwords in PAM.

  cat /etc/pam.d/dovecot
#%PAM-1.0
auth include system-auth
account include system-auth
session include system-auth
password include system-auth

PAM configuration is complex and out of the scope of this blog post, take it for granted.

Create system user
  useradd iulian -m
  passwd iulian

Create system user that need to send/receive email.

Final conf
  doveconf -n
# 2.3.18 (9dd8408c18): /etc/dovecot/dovecot.conf
# OS: Linux 5.17.4-arch1-1 x86_64
# Hostname: rig
mail_location = mbox:~/mail:INBOX=/var/mail/%u
namespace inbox {
  inbox = yes
  location =
  mailbox Drafts {
    special_use = \Drafts
  }
  mailbox Junk {
    special_use = \Junk
  }
  mailbox Sent {
    special_use = \Sent
  }
  mailbox "Sent Messages" {
    special_use = \Sent
  }
  mailbox Trash {
    special_use = \Trash
  }
  prefix =
}
passdb {
  driver = pam
}
service auth {
  unix_listener /var/spool/postfix/private/auth {
    group = postfix
    mode = 0660
    user = postfix
  }
}
ssl_cert = </etc/ssl/certs/dovecot.pem
ssl_key = # hidden, use -P to show it
userdb {
  driver = passwd
}

The whole Dovecot config is long / complex, these are only the non-defaults values.

1.3 Service

  systemctl start dovecot.service
  ufw limit "IMAPS"
  ufw limit "Mail"

Start/enable dovecot.service and open the ports in UFW firewall.

1.4 Testing tools

Just basic connectivity/speed IMAPS testing, we'll run more advanced tests later on.

2. Postfix

I know Sendmail is the classic, widely used mail transfer agent but it is a bit old-fashion to me and I'll use Postfix instead.

2.1 Installation

  pacman -S postfix

2.2 Configuration

Again, we will use postconf to show/manage configuration.

Directories
  postconf -n | grep -E "directory\s"
command_directory = /usr/bin
daemon_directory = /usr/lib/postfix/bin
data_directory = /var/lib/postfix
html_directory = no
manpage_directory = /usr/share/man
meta_directory = /etc/postfix
queue_directory = /var/spool/postfix
readme_directory = /usr/share/doc/postfix
sample_directory = /etc/postfix
shlib_directory = /usr/lib/postfix

This is mostly Arch Linux specific but is worth seeing where things are installed/stored.

Domain
  postconf -n | grep ^my
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
mydomain = costan.ro
myhostname = smtp.$mydomain
myorigin = $mydomain

mydomain, mydestination specify what email recipients should be accepted by my server.

Aliases
  postconf -n | grep -E "^alias|newaliases"
alias_database = $alias_maps
alias_maps = hash:/etc/postfix/aliases
newaliases_path = /usr/bin/newaliases

Email aliases if any; dont forget to run newaliases command to rebuild aliases db.

Catch-all email
  postconf -n luser_relay local_recipient_maps
luser_relay = iulian
local_recipient_maps =

Redirect all (mind spam) unknown email recipients to given username.

Secure email with TLS (Transport Layer Security)
receiving
  postconf -n | grep -E "smtpd_tls|smtpd_use_tls"
smtpd_tls_auth_only = yes
smtpd_tls_cert_file = /etc/letsencrypt/live/costan.ro/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/costan.ro/privkey.pem
smtpd_tls_loglevel = 1
smtpd_tls_security_level = may
smtpd_use_tls = yes

smtpd_tls_auth_only to reject plain auth over unsecured connections.

sending
  postconf -n | grep smtp_
smtp_tls_loglevel = 1
smtp_tls_security_level = may

smtp_tls_security_level optional TLS when sending, since TLS is not enabled in all MTAs.

Authentication/authorization
Postfix auth config
  postconf -n | grep ^smtpd_sasl
smtpd_sasl_auth_enable = yes
smtpd_sasl_local_domain = $mydomain
smtpd_sasl_path = private/auth
smtpd_sasl_security_options = noanonymous, noplaintext
smtpd_sasl_tls_security_options = noanonymous
smtpd_sasl_type = dovecot

smtpd_sasl_type, smtpd_sasl_path - backend and unix socket for SASL smtpd_sasl_tls_security_options - allow plain text auth over TLS, but no anonymous

Dovecot auth integration
  doveconf -n service auth
service auth {
  unix_listener /var/spool/postfix/private/auth {
    group = postfix
    mode = 0660
    user = postfix
  }
}

The other side of the Unix socket configured in Dovecot.

Relay and restrictions
  postconf -n | grep -E "helo|relay"
smtpd_helo_required = yes
smtpd_helo_restrictions = reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination

smtpd_relay_restrictions - no open relay ever OK?

Mail submission
  postconf -M submission
submission inet  n       -       n       -       -       smtpd -o syslog_name=postfix/submission -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= -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject -o milter_macro_daemon_name=ORIGINATING

The MSA service that listen on 587/tcp port for mail submission from a MUA (email client).

DNS TXT record
  drill -Q costan.ro TXT
  drill -Q smtp.costan.ro TXT
"v=spf1 a mx ip4:86.124.145.184 ~all"
"v=spf1 a mx ip4:86.124.145.184 ~all"

Sender Policy Framework (SPF) is required to detect some forged sender addreses.

Reverse DNS record
  drill -Q 86.124.145.184 -x
smtp.costan.ro.

And last, one of the most important configuration, get in touch with your ISP to setup the Reverse DNS (rDNS); otherwise your emails will, most probably, be marked as spam.

Final conf
  postconf -n
alias_database = $alias_maps
alias_maps = hash:/etc/postfix/aliases
command_directory = /usr/bin
compatibility_level = 3.7
daemon_directory = /usr/lib/postfix/bin
data_directory = /var/lib/postfix
debug_peer_level = 2
debugger_command = PATH=/bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin ddd $daemon_directory/$process_name $process_id & sleep 5
html_directory = no
inet_protocols = ipv4
local_recipient_maps =
luser_relay = iulian
mail_owner = postfix
mailq_path = /usr/bin/mailq
manpage_directory = /usr/share/man
meta_directory = /etc/postfix
milter_default_action = accept
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
mydomain = costan.ro
myhostname = smtp.$mydomain
myorigin = $mydomain
newaliases_path = /usr/bin/newaliases
non_smtpd_milters = $smtpd_milters
queue_directory = /var/spool/postfix
readme_directory = /usr/share/doc/postfix
sample_directory = /etc/postfix
sendmail_path = /usr/bin/sendmail
setgid_group = postdrop
shlib_directory = /usr/lib/postfix
smtp_tls_loglevel = 1
smtp_tls_security_level = may
smtpd_helo_required = yes
smtpd_helo_restrictions = reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname
smtpd_milters = inet:localhost:8891, inet:localhost:8893
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
smtpd_sasl_auth_enable = yes
smtpd_sasl_local_domain = $mydomain
smtpd_sasl_path = private/auth
smtpd_sasl_security_options = noanonymous, noplaintext
smtpd_sasl_tls_security_options = noanonymous
smtpd_sasl_type = dovecot
smtpd_tls_auth_only = yes
smtpd_tls_cert_file = /etc/letsencrypt/live/costan.ro/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/costan.ro/privkey.pem
smtpd_tls_loglevel = 1
smtpd_tls_security_level = may
smtpd_use_tls = yes
unknown_local_recipient_reject_code = 550

Again, these are only the non-default config values.

2.3 Service

  systemctl start postfix.service
  ufw limit "SMTP"

2.4 Testing tools

For all geeks out there you can use openssl to do basic SMTP testing.

  openssl s_client -connect smtp.costan.ro:25 -starttls smtp

3. DomainKeys Identified Mail - DKIM

DKIM is an email authentication method used to detect forged sender addresses.

3.1 Installation

  pacman -S opendkim

3.2 Configuration

Minimal config
  grep -v -e '^#' -e '^[[:space:]]*$' /etc/opendkim/opendkim.conf
Canonicalization	  relaxed/simple
Domain			  costan.ro
KeyFile			  /etc/opendkim/rig.private
Selector		  rig
Socket                    inet:8891@localhost
Syslog			  Yes
UserID                    opendkim:postfix

Nothing too complex, domain, private key location and the socket.

Generate key file
  opendkim-genkey --restrict --selector rig --domain costan.ro --directory /etc/opendkim

Generate rig.private and rig.txt files.

Postfix integration
  postconf -n | grep milter
milter_default_action = accept
non_smtpd_milters = $smtpd_milters
smtpd_milters = inet:localhost:8891, inet:localhost:8893

Socket communication via inet:localhost:8891.

DNS TXT record
  cat /etc/opendkim/rig.txt
rig._domainkey	IN	TXT	( "v=DKIM1; k=rsa; s=email; "
	  "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQFlti46dceD5rk3+RGnoYStK6np+cIucrOrkMHbjoRLcOxNikOfi0ABgG2CxK/0X+VNmiL5PsaWWnXhYGOJWz82LM0zhDzoD1bQ0OIb/PWyPMz22udwnPa6FRypEEnjAdC6c8g7tX8fMovqX/09PHKKjLq4zX0X3CMT+t3QhXlQIDAQAB" )  ; ----- DKIM key rig for costan.ro
  drill -Q rig._domainkey.costan.ro TXT
"v=DKIM1; k=rsa; s=email;  	 p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQFlti46dceD5rk3+RGnoYStK6np+cIucrOrkMHbjoRLcOxNikOfi0ABgG2CxK/0X+VNmiL5PsaWWnXhYGOJWz82LM0zhDzoD1bQ0OIb/PWyPMz22udwnPa6FRypEEnjAdC6c8g7tX8fMovqX/09PHKKjLq4zX0X3CMT+t3QhXlQIDAQAB"

Public key published as TXT record.

3.3 Service

  systemctl start opendkim.service

4. Domain-based Message Authentication, Reporting and Conformance - DMARC

DMARC is an email authentication protocol that extends SPF and DKIM to protect domain from email spoofing.

4.1 Installation

  pacman -S opendmarc

4.2 Configuration

Minimal config
  grep -v -e '^#' -e '^[[:space:]]*$' /etc/opendmarc/opendmarc.conf
AuthservID HOSTNAME
IgnoreAuthenticatedClients true
Socket inet:8893@localhost
SPFSelfValidate true
UMask 002

Socket and some other basic stuff.

Postfix integration
  postconf -n | grep milter
milter_default_action = accept
non_smtpd_milters = $smtpd_milters
smtpd_milters = inet:localhost:8891, inet:localhost:8893

Socket communication via inet:localhost:8893

DNS TXT record
  drill -Q _dmarc.costan.ro TXT
"v=DMARC1; p=quarantine; rua=mailto:postmaster@costan.ro; ruf=mailto:forensic@costan.ro; adkim=s; aspf=s; fo=1; pct=25"

Enable p=quarantine policy for pct=25 percent of the emails that fail the validation.

4.3 Service

  systemctl start opendmarc.service

5. Imapsync

And finally, migrate all emails from Gmail to my personal email server with imapsync tool.

5.1 Installation

  pacman -S imapsync

5.2 Migration

  imapsync --gmail1 --user1 <SRC_USER> --password1 <SRC_PASS> \
           --host2 localhost --user2 <DST_USER> --password2 <DST_PASS> \
           --exclude "INBOX|Drafts|Important|Spam|Trash" \
           --f1f2 "[Gmail]/All Mail"="Archive" \
           --folderlast "[Gmail]All Mail" \
           --dry

Imapsync tool has a lots of params but the default automap works just fine, I only need to map Gmail's All Mail to Archive folder (to be synced last) and exclude the folders that I do not want.

Mind the –dry at the end, to play safe and test out the whole migration first.

Updates

  • [2023-02-21] Use costan.ro certs instead of self-generated Dovecot certs