This guide is outdated and will not work without hours of debugging and modifications.
This guide is written for Ubuntu/Debian. If you run any other distribution of linux, this guide may not be for you.

Email on linux 2015-05-30 Outdated
A full guide on setting up an email server on Linux. Written by a human, for humans.

Let's face it, everyone knows email is an absolute pain to set up on linux-based servers. Hopefully this guide can help remedy that.
If you don't already have any SSL/TLS certificates, you can follow my Let's Encrypt Guide to get free and trusted certificates for both mail and web.

This guide will show you step by step how to set up a secure email server that supports virtual mailboxes, SPF, DKIM, DMARC and SSL/TLS.

Obtaining certificates for SSL/TLS

For your emails to be transferred encrypted over a network, you need SSL/TLS. For that you need a certificate and private key pair. This can be obtained several ways, and i go over one of many ways in another guide.

The most important thing is that you store this pair along with the certificate authority's certificate in a place readable by postfix and dovecot. You will need the path to your certificate, your private key and the CA's certificate (chain).


If you already have a MySQL server set up, you may continue reading. Otherwise please follow my guide on installing MySQL 5.7.


First of all we need to create a database to hold our tables and data. For security we'll create a specific user only used to read the database by the email servers. We also need to USE the database, so our tables will be created in the correct database.

Make sure you change 'mailpassword' to something more secure.
GRANT SELECT ON mail.* TO 'mail'@'localhost' IDENTIFIED BY 'mailpassword';
USE mail;


We'll be creating three tables for our virtual mailbox configuration. One for aliases, one for domains and one for users.

CREATE TABLE IF NOT EXISTS `virtual_domains` (
	`name` varchar(50) NOT NULL,

CREATE TABLE IF NOT EXISTS `virtual_aliases` (
	`domain_id` int(11) NOT NULL,
	`source` varchar(254) NOT NULL,
	`destination` varchar(254) NOT NULL,
	PRIMARY KEY (`id`),
	KEY `domain_id` (`domain_id`),
	CONSTRAINT `virtual_aliases_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `virtual_domains` (`id`) ON DELETE CASCADE

CREATE TABLE IF NOT EXISTS `virtual_users` (
	`domain_id` int(11) NOT NULL,
	`password` varchar(106) NOT NULL,
	`email` varchar(254) NOT NULL,
	PRIMARY KEY (`id`),
	UNIQUE KEY `email` (`email`),
	KEY `domain_id` (`domain_id`),
	CONSTRAINT `virtual_users_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `virtual_domains` (`id`) ON DELETE CASCADE

Virtual domains

Now we'll begin inserting data into the tables, starting with virtual domains. You should obviously change and remove or change as neccesary. You may also add new values, depending on what domains you wish to receive emails for.

INSERT INTO `virtual_domains`
(`id`, `name`)
('1', ''),
('2', '');

Virtual mailboxes

Next we'll create email accounts or mailboxes. Again, modify the query to your preferences.

INSERT INTO `virtual_users`
(`id`, `domain_id`, `password` , `email`)
('1', '1', ENCRYPT('firstpassword', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), '[email protected]'),
('2', '2', ENCRYPT('secondpassword', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), '[email protected]');

Virtual aliases

We can also create aliases for the mailboxes. This basically routes mails from the source address to the destination address, no matter if there's already an account on that source address. However, the destination address must be an existing mailbox.

INSERT INTO `virtual_aliases`
(`id`, `domain_id`, `source`, `destination`)
('1', '1', '[email protected]', '[email protected]');

The application we'll use to receive emails (IMAP server).


apt-get install -y dovecot-core dovecot-imapd dovecot-lmtpd dovecot-mysql

Main configuration

First we'll make sure dovecot is set to include extra config files and enable some protocols.

nano /etc/dovecot/dovecot.conf

Now we'll enable the protocols we need. These protocols won't be enabled if they're not defined first though, that's why we have to enable them after the !include_try /usr/share/dovecot/protocols.d/*.protocol line. We want to enable imap and lmtp, so we are going to write it like this:

!include_try /usr/share/dovecot/protocols.d/*.protocol
protocols = imap lmtp

Finally, to make sure dovecot includes all config files in /etc/dovecot/conf.d/ we have to make sure the following line is uncommented:

!include conf.d/*.conf

Mail configuration

Next up we're going to create a separate folder for every virtual domain and set the correct permissions, then tell that to dovecot.

The two first commands below might fail because the user/group mail already exists. This is completely fine and can be ignored. Move on to the next command.
groupadd -g 5000 mail
useradd -g mail -u 5000 mail -d /var/mail
mkdir -p /var/mail/vhosts/{,}
chown -R mail:mail /var/mail
chown -R dovecot:mail /etc/dovecot
chmod -R o-rwx /etc/dovecot
nano /etc/dovecot/conf.d/10-mail.conf

Now we need to edit mail_location and mail_privileged_group in order for dovecot to understand where mail is located, and what user has access to the mail.

Find the mail_location line, uncomment it and change it to the following:

mail_location = maildir:/var/mail/vhosts/%d/%n

Find the mail_privileged_group line, uncomment it and change it to the following:

mail_privileged_group = mail

Finally, find the first_valid_uid line, uncomment it and change it to the following:

first_valid_uid = 1

Auth configuration

Now we need to tell dovecot that we're using MySQL to authenticate our users.

nano /etc/dovecot/conf.d/10-auth.conf

Uncomment the following line:

disable_plaintext_auth = yes

Find the line containing auth_mechanisms = plain and change it to the following:

auth_mechanisms = plain login

Comment out this line:

#!include auth-system.conf.ext

Uncomment this line in order to enable MySQL authentication:

!include auth-sql.conf.ext

MySQL configuration

To allow dovecot to connect to our MySQL database, we need to give it our MySQL credentials using a driver.

nano /etc/dovecot/conf.d/auth-sql.conf.ext

Enter the following in the file before saving it:

passdb {
	driver = sql
	args = /etc/dovecot/dovecot-sql.conf.ext
userdb {
	driver = static
	args = uid=mail gid=mail home=/var/mail/vhosts/%d/%n

Now to actually provide dovecot our credentials, we have to modify another file.

nano /etc/dovecot/dovecot-sql.conf.ext

Find the #driver = line, uncomment it and change it to the following:

driver = mysql

Find the #connect = line, uncomment it and change it to the following, replacing the highlighted parts with your own MySQL credentials we created here.

connect = host= dbname=mail user=mail password=mailpassword

Find the #default_pass_scheme = line, uncomment it and change it to the following:

default_pass_scheme = SHA512-CRYPT

Finally, find the #password_query = \ line, uncomment it and change it to the following:

password_query = SELECT email as user, password FROM virtual_users WHERE email='%u';

Master configuration

Now we're going to define the services that dovecot will provide.

nano /etc/dovecot/conf.d/10-master.conf

Find service imap-login and change it to the following:

service imap-login {
		inet_listener imap {
				port = 143

		inet_listener imaps {
				port = 993
				ssl = yes

Find service lmtp and change it to the following:

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

Find service auth and change it to the following:

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

		unix_listener auth-userdb {
				mode = 0600
				user = mail

		user = dovecot

Finally, find service auth-worker and change it to the following:

service auth-worker {
		user = mail

SSL configuration

This will require dovecot to encrypt all network traffic with the ssl certificate we obtained here.

nano /etc/dovecot/conf.d/10-ssl.conf

Change the ssl parameter to required:

ssl = required

Modify the path for ssl_cert to your full certificate chain minus your CA's certificate and ssl_key to the path to your certificate's private key.

ssl_cert = </etc/letsencrypt/live/

ssl_key = </etc/letsencrypt/live/
This will take a very long time to complete, and dovecot will not be functional until it is completed. Alternatively you can use "2048" although that is not as future-proof.

Use stronger ssl_dh_parameters_length

ssl_dh_parameters_length = 4096

Disable insecure ssl_protocols

ssl_protocols = !SSLv2 !SSLv3 !TLSv1

Use a stronger ssl_cipher_list

ssl_cipher_list = ALL:HIGH:!SSLv2:!MEDIUM:!LOW:!EXP:!RC4:!MD5:!aNULL:@STRENGTH

Prefer server ciphers

ssl_prefer_server_ciphers = yes

The application we'll use to send emails (SMTP server).


apt-get install -y postfix postfix-mysql

Shortly after entering this command, you'll be prompted with the following. In this guide i will be showing how to set up a normal "Internet Site", so choose that option.

After selecting "Internet Site" you'll be asked to enter your "System mail name". This is your FQDN (Fully Qualified Domain Name) for the email server. I usually pick "" here.

Main configuration

We'll start with moving the default posfix configuration and making our own.

mv /etc/postfix/ /etc/postfix/
nano /etc/postfix/

Add the following lines replacing with your FQDN and everything marked in red with the appropriate paths from here and save the file.

Copy to clipboard
# SMTPD banner
smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu/Debian)

# Disable local mail notifications
biff = no

# Appending .domain is the MUA's job.
append_dot_mydomain = no

# Enable SASL authentication
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes

smtpd_use_tls = yes
smtpd_tls_security_level = may
smtpd_tls_loglevel = 1
smtpd_tls_CAfile = /etc/ssl/certs/lets-encrypt-x1-cross-signed.pem
smtpd_tls_cert_file = /etc/letsencrypt/live/
smtpd_tls_key_file = /etc/letsencrypt/live/
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache

smtp_use_tls = yes
smtp_tls_security_level = may
smtp_tls_loglevel = 1
smtp_tls_CAfile = /etc/ssl/certs/lets-encrypt-x1-cross-signed.pem
smtp_tls_cert_file = /etc/letsencrypt/live/
smtp_tls_key_file = /etc/letsencrypt/live/
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache

# Block clients that speak too early.
smtpd_data_restrictions = reject_unauth_pipelining

# Don't accept mail from domains that don't exist.
smtpd_sender_restrictions = reject_unknown_sender_domain

# Block senders with invalid or blacklisted hostname, unknown domain, non-fqdn domain, unauthorized destination.
# Also ensures sender SPF is valid.
smtpd_recipient_restrictions =
		check_policy_service unix:private/policy-spf,

# Network/Database config
myhostname =
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
mydestination = localhost
relayhost =
mynetworks = [::ffff:]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = all
local_recipient_maps = proxy:unix:passwd.byname $alias_maps

# Virtual mailboxes and database mappings
virtual_transport = lmtp:unix:private/dovecot-lmtp
virtual_mailbox_domains = mysql:/etc/postfix/
virtual_mailbox_maps = mysql:/etc/postfix/
virtual_alias_maps = mysql:/etc/postfix/

# Default milter options
milter_protocol = 2
milter_default_action = accept

# DKIM, DMARC, Spamassassin
smtpd_milters = unix:/var/run/opendkim/opendkim.sock, unix:/var/run/opendmarc/opendmarc.sock, unix:/spamass/spamass.sock
non_smtpd_milters = unix:/var/run/opendkim/opendkim.sock, unix:/var/run/opendmarc/opendmarc.sock, unix:/spamass/spamass.sock

# SPF time limit
policy-spf_time_limit = 3600s
Copy to clipboard

Master configuration

Now we'll configure the master process of postfix, and any daemons we want to run under postfix.
We'll be moving the default configuration because i provide a full configuration file.

mv /etc/postfix/ /etc/postfix/
nano /etc/postfix/

Insert the following lines and save the file.

Copy to clipboard
# Postfix master process configuration file.  For details on the format
# of the file, see the master(5) manual page (command: "man 5 master").
# Do not forget to execute "postfix reload" after editing this file.
# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (yes)   (never) (100)
# ==========================================================================
smtp      inet  n       -       -       -       -       smtpd
submission inet n       -       -       -       -       smtpd
	-o syslog_name=postfix/submission
	-o smtpd_client_restrictions=permit_sasl_authenticated,reject
	-o smtpd_tls_wrappermode=no
smtps     inet  n       -       -       -       -       smtpd
	-o syslog_name=postfix/smtps
	-o smtpd_client_restrictions=permit_sasl_authenticated,reject
	-o smtpd_tls_wrappermode=yes
#628       inet  n       -       -       -       -       qmqpd
pickup    unix  n       -       -       60      1       pickup
cleanup   unix  n       -       -       -       0       cleanup
qmgr      unix  n       -       n       300     1       qmgr
#qmgr     unix  n       -       n       300     1       oqmgr
tlsmgr    unix  -       -       -       1000?   1       tlsmgr
rewrite   unix  -       -       -       -       -       trivial-rewrite
bounce    unix  -       -       -       -       0       bounce
defer     unix  -       -       -       -       0       bounce
trace     unix  -       -       -       -       0       bounce
verify    unix  -       -       -       -       1       verify
flush     unix  n       -       -       1000?   0       flush
proxymap  unix  -       -       n       -       -       proxymap
proxywrite unix -       -       n       -       1       proxymap
smtp      unix  -       -       -       -       -       smtp
relay     unix  -       -       -       -       -       smtp
#       -o smtp_helo_timeout=5 -o smtp_connect_timeout=5
showq     unix  n       -       -       -       -       showq
error     unix  -       -       -       -       -       error
retry     unix  -       -       -       -       -       error
discard   unix  -       -       -       -       -       discard
local     unix  -       n       n       -       -       local
virtual   unix  -       n       n       -       -       virtual
lmtp      unix  -       -       -       -       -       lmtp
anvil     unix  -       -       -       -       1       anvil
scache    unix  -       -       -       -       1       scache
# ====================================================================
# Interfaces to non-Postfix software. Be sure to examine the manual
# pages of the non-Postfix software to find out what options it wants.
# Many of the following services use the Postfix pipe(8) delivery
# agent.  See the pipe(8) man page for information about ${recipient}
# and other message envelope options.
# ====================================================================
# maildrop. See the Postfix MAILDROP_README file for details.
# Also specify in maildrop_destination_recipient_limit=1
maildrop  unix  -       n       n       -       -       pipe
	flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
# ====================================================================
# Recent Cyrus versions can use the existing "lmtp" entry.
# Specify in cyrus.conf:
#   lmtp    cmd="lmtpd -a" listen="localhost:lmtp" proto=tcp4
# Specify in one or more of the following:
#  mailbox_transport = lmtp:inet:localhost
#  virtual_transport = lmtp:inet:localhost
# ====================================================================
# Cyrus 2.1.5 (Amos Gouaux)
# Also specify in cyrus_destination_recipient_limit=1
#cyrus     unix  -       n       n       -       -       pipe
#  user=cyrus argv=/cyrus/bin/deliver -e -r ${sender} -m ${extension} ${user}
# ====================================================================
# Old example of delivery via Cyrus.
#old-cyrus unix  -       n       n       -       -       pipe
#  flags=R user=cyrus argv=/cyrus/bin/deliver -e -m ${extension} ${user}
# ====================================================================
# See the Postfix UUCP_README file for configuration details.
uucp      unix  -       n       n       -       -       pipe
	flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient)
# Other external delivery methods.
ifmail    unix  -       n       n       -       -       pipe
	flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient)
bsmtp     unix  -       n       n       -       -       pipe
	flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t$nexthop -f$sender $recipient
scalemail-backend unix  -       n       n       -       2       pipe
	flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store ${nexthop} ${user} ${extension}
mailman   unix  -       n       n       -       -       pipe
	flags=FR user=list argv=/usr/lib/mailman/bin/
	${nexthop} ${user}
policy-spf  unix  -       n       n       -       -       spawn
	user=nobody argv=/usr/bin/policyd-spf
Copy to clipboard

MySQL integration

Create the following files replacing mailpassword with the password you set up in the MySQL step.

user = mail
password = mailpassword
hosts =
dbname = mail
query = SELECT 1 FROM virtual_domains WHERE name='%s'
user = mail
password = mailpassword
hosts =
dbname = mail
query = SELECT 1 FROM virtual_users WHERE email='%s'
user = mail
password = mailpassword
hosts =
dbname = mail
query = SELECT destination FROM virtual_aliases WHERE source='%s'

DomainKeys Identified Mail

Installation and configuration

apt-get install -y opendkim opendkim-tools
mkdir -p /etc/opendkim/keys
adduser postfix opendkim
nano /etc/opendkim.conf

Insert the following at the end of the file.

AutoRestart             yes
AutoRestartRate         10/1h

Syslog                  yes
SyslogSuccess           yes
LogWhy                  yes

Canonicalization        relaxed/simple

ExternalIgnoreList      refile:/etc/opendkim/TrustedHosts
InternalHosts           refile:/etc/opendkim/TrustedHosts
KeyTable                refile:/etc/opendkim/KeyTable
SigningTable            refile:/etc/opendkim/SigningTable

Mode                    sv
SignatureAlgorithm      rsa-sha256

UserID                  opendkim:opendkim

If you want to learn about these options and what they do, look at the documentation.

mkdir -p /var/spool/postfix/var/run/opendkim
chown -R opendkim:opendkim /var/spool/postfix/var/run/opendkim
nano /etc/default/opendkim

Add the following line to the end of the file


Trusted hosts

Now we'll define the hosts to sign all mails for.

nano /etc/opendkim/TrustedHosts

Insert the following lines and save the file.

Key table

We also need to create a key table that will contain each selector/domain pair and the path to their private key. The most common selector to use is "mail", so i would reccomend using that.

nano /etc/opendkim/KeyTable

Insert the following lines replacing with your root domain and save the file.
You should add a new line with the same structure for every new domain you wish to send mails for.

Signing table

Now create a signing table that's used for declaring the domains/email addresses to sign messages for and their selectors.

nano /etc/opendkim/SigningTable

Insert the following lines replacing with your root domain and save the file.
You should add a new line with the same structure for every new domain you wish to send mails for.


In this case, we will sign all emails for any email ending with with the private key for

Generating keys

The final step in setting up opendkim is generating the keys that will be signing the emails. You should repeat this process for every domain you've added in the key table and signing table.

Make sure you input the correct subject and domain for every key.
cd /etc/opendkim/keys
opendkim-genkey -s mail -d -b 4096
chown opendkim:opendkim mail.private

Executing these commands will make a folder for your domain and generate two files inside it. The one called mail.private is your private key used for signing emails. The one called mail.txt will contain a DNS TXT record you must put on your domain.

The record put in a zone file should look somewhat like this (along with an Author Domain Signing Practices record).

DNS Zone File
mail._domainkey  IN TXT "v=DKIM1; k=rsa; p=LONG/STRING/OF/CHARACTERS"
_adsp._domainkey IN TXT "dkim=all"

Sender Policy Framework


apt-get install -y  postfix-policyd-spf-python

SPF is a method of fighting spam, ensuring that whoever sends you an email is sending it from a whitelisted email server. This works both ways as long as both parties have SPF enabled, meaning other email servers will also verify that emails you send come from your server.

DNS Zone File
@ IN TXT "v=spf1 a mx ip4: -all"

The SPF record above will only allow servers with an IP address equal to the one of your A or MX records or the IP address to send emails from your domain.

Domain-based Message Authentication, Reporting and Conformance


apt-get install -y opendmarc
mkdir -p /var/spool/postfix/var/run/opendmarc
chown -R opendmarc:postfix /var/spool/postfix/var/run/opendmarc
adduser postfix opendmarc
nano /etc/default/opendmarc

Add the following line to the end of the file


DMARC is really just a DNS TXT record enforcing SPF and DKIM validation on emails coming from your domain.
Here's an example record in zone file format. The options in this record are explained here.

DNS Zone File
_dmarc IN TXT "v=DMARC1; p=quarantine; rua=mailto:[email protected]; fo=1; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400"

To help prevent spam, we will install spamassassin as an incoming mail filter.

Installation and configuration

apt-get install -y spamassassin spamass-milter
groupadd spamd
useradd -g spamd -s /bin/false -d /var/lib/spamassassin spamd
adduser spamd spamass-milter
adduser postfix spamd
mkdir /var/lib/spamassassin
chown -R spamd:spamd /var/lib/spamassassin
nano /etc/default/spamassassin

Replace everything with the following:

OPTIONS="--username spamd --nouser-config --max-children 2 --helper-home-dir ${SAHOME} --socketpath=/var/run/spamassassin.sock --socketowner=spamd --socketgroup=spamd --socketmode=0660"

Edit the spamass-milter config

nano /etc/default/spamass-milter

Replace everything with the following:

OPTIONS="-u spamass-milter -m -I -i -- --socket=/var/run/spamassassin.sock"

Spamassassin rules

To get the most out of spamassassin you have to create rules.

nano /etc/spamassassin/

Uncomment the following lines:

# rewrite_header Subject *****SPAM*****
# required_score 5.0
# use_bayes 1
# bayes_auto_learn 1

Then change this:

rewrite_header Subject *****SPAM*****
required_score 5.0

To this:

rewrite_header Subject [***** SPAM _SCORE_ *****]
required_score 3.0

Restart everything

Now that everything is configured correctly, we need to restart everything to apply our changes.

service spamassassin restart
service opendkim restart
service opendmarc restart
service dovecot restart
service postfix restart

Now you should be ready to test!


To ensure everything is working correctly, open up your email client and send an email to [email protected]. If everything is working as it should be, you should receive a response like this:

Summary of Results
SPF check:          pass
DomainKeys check:   neutral
DKIM check:         pass
Sender-ID check:    pass
SpamAssassin check: ham

If your results don't match the above, you should re-read the steps of the guide where it failed. If that doesn't give you any answers, remember Google is your friend.

Thanks for reading!

I hope you were able to follow the guide without any problems, if you have any questions please feel free to contact me!


Written by
Alexander Sagen