Ivan Tomica

Self hosted email

There are many reasons why would somebody want to host their own email server. There are many advantages for doing this but there are also many drawbacks.

Setting up your own mail server can certainly be a great learning experience filled with discoveries about different set of technologies, protocols and how stuff works in general.

Hosting your own email server can also be a huge pain in the ass, especially if you start to have problems with spamming but I won’t go too deep into pros or the cons since, if you’re already here, you probably read a thing or two and decided for yourself if you want to host your own email server or not. Since you’re here I suppose you have decided to do it. ;-)

Article index:


Some prerequisites for using this guide are:

  • You have domain name registered and DNS servers which you can use to point some records to the domain name.
  • You have server with root access and correct PTR (reverse DNS record) set up.
  • You have pointed domain name or mail.domain name to this server.
  • You have set up MX records correctly.

Example of good setup would be:

  • example.com has MX pointed to mail.example.com
  • mail.example.com points to IP
  • IP address has PTR record set up to point to mail.example.com

Also consider setting up SPF record for your domain name. It should look something like:

“v=spf1 a mx -all”

In this guide I will be using CentOS 7 system which uses SystemD and this guide assumes you are using the same, though with little bit of thinking you may adapt this guide for any Linux distribution.

This guide will primarily use following technologies:

  • Postfix – MTA (mail transfer agent)
  • Dovecot – POP and IMAP server

Besides those we will also set up SpamAssassin for spam filtering and issue SSL via LetsEncrypt to secure your connection to the server.

First we will start with enabling EPEL repository since we need some pieces of software that are not available in standard CentOS repositories.

To enable EPEL repository on your CentOS server use:

yum install -y epel-release.noarch

LetsEncrypt SSL

Next, we’ll want to prepare our SSL certificate so we don’t have to do it later. To do that we will use certbot utility. To install certbot use:

yum install -y certbot.noarch


Using it is quite simple and more options can be found on official documentation but we will use following command to issue SSL:

certbot certonly --standalone -d mail.example.com


You should be presented with following screen:


Type in your working email account address which can later be used to revoke certificate if needed. After that you will be presented with a screen to accept Terms of Service so hit Agree so we can carry on.

Hopefully everything went fine without any issues and you now have your SSL certificate. You should be greeted with message that goes like this:

 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/mail.example.com/fullchain.pem. Your cert will
   expire on 2016-12-25. To obtain a new or tweaked version of this
   certificate in the future, simply run certbot again. To
   non-interactively renew *all* of your certificates, run "certbot
 - If you lose your account credentials, you can recover through
   e-mails sent to youremail@example.com.
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

It is also wise to set up automatic certificate renewal. It is as easy as placing following line in your crontab:

0 12 * * * /bin/certbot renew --quiet

Install software

Now that we are done with that we can carry on with setting up our mail server. As I mentioned in the beginning of this post we will be using few components to achieve our goal so go ahead and install them all.

yum install -y postfix.x86_64 dovecot.x86_64 dovecot-pigeonhole.x86_64 spamassassin.x86_64 amavisd-new.noarch

Afet all of the software pieces are installed we can proceed with configuring them.


First, it is turn on Postfix. I strongly recommend that we first backup all of the files we plan to change so we can restore them if needed later, so that will be our first step:

cd /etc/postfix
cp main.cf main.cf.backup
cp master.cf master.cf.backup

Next we will create user and group which will be used for storing email account directories:

useradd vpostfix -s /usr/sbin/nologin -d /var/empty/postfix

To find out UID (User ID) and GID (Group ID) of newly created user and group you can use:

grep vpostfix /etc/passwd

which should print something like (your UID and GID may vary):


Next step is to edit /etc/postfix/main.cf file in your favorite text editor and ensure following values are in there:

myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
inet_interfaces = all
home_mailbox = Maildir/

You will also need to add following lines at the end of your main.cf file:

# Virtual domain config
virtual_mailbox_domains = /etc/postfix/virtual_domains
virtual_mailbox_base = /var/mail/vhosts
# Replace UID and GID numbers with real ones
virtual_minimum_uid = 1000
virtual_uid_maps = static:1000
virtual_gid_maps = static:1000
virtual_alias_maps = hash:/etc/postfix/virtual
smtpd_use_tls = yes
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_loglevel = 1
smtpd_tls_received_header = yes
smtpd_tls_session_cache_timeout = 3600s
tls_random_source = dev:/dev/urandom
# Harden TLS (disables sslv3 and lower, uses strong ciphers)
smtpd_tls_ciphers = high
smtpd_tls_mandatory_ciphers = high
smtpd_tls_mandatory_protocols = TLSv1.2,TLSv1.1,TLSv1,!SSLv3,!SSLv2
smtpd_tls_protocols = TLSv1.2,TLSv1.1,TLSv1,!SSLv3,!SSLv2
lmtp_tls_mandatory_protocols = TLSv1.2,TLSv1.1,TLSv1,!SSLv3,!SSLv2
lmtp_tls_protocols = TLSv1.2,TLSv1.1,TLSv1,!SSLv3,!SSLv2
smtp_tls_mandatory_protocols = TLSv1.2,TLSv1.1,TLSv1,!SSLv3,!SSLv2
smtp_tls_protocols = TLSv1.2,TLSv1.1,TLSv1,!SSLv3,!SSLv2
# Needs to be generated with:
# openssl dhparam -out /etc/postfix/dhparams.pem 2048
# Can be more than config directive suggests (1024, 2048, 4096..)
smtpd_tls_dh1024_param_file = /etc/postfix/dhparams.pem

# Encrypt transport between servers
smtp_tls_security_level = may
smtp_tls_note_starttls_offer = yes
smtp_tls_loglevel = 1
smtpd_sasl_type = dovecot
broken_sasl_auth_clients = yes
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination
smtpd_relay_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination
# This one is optional and it is used to change max message size limit (10MB by default)
message_size_limit = 51200000

Next, edit your /etc/postfix/master.cf and make sure following lines are not commented out:

submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING

If you want to enable legacy smtps on port 465 for whatever reason you can also enable following lines:

smtps     inet  n       -       n       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING

In next step we will list our domain names so Postfix knows for which domains will it accept email:

touch /etc/postfix/virtual_domains

List your domain names in there like:

# Each domain goes to a new line

Of course we need to save all of our messages somewhere so we will create email directory and ensure proper permissions are set on them:

mkdir -p /var/mail/vhosts
chgrp vpostfix /var/mail
chgrp -R vpostfix /var/mail

Next create folders for each of the domains you plan to use:

cd /var/mail/vhosts
mkdir example.com
mkdir example2.com
mkdir example3.com
cd ../
chown -R vpostfix:vpostfix vhosts/

Postfix will later create its file and directory structure automatically.

Every time you make changes to /etc/postfix/virtual which contains virtual aliases you need to issue:

postmap /etc/postfix/virtual

There is also another file which is used for setting up local system aliases and every time you make changes to your local aliases file you need to issue:

postalias /etc/aliases

So if you haven’t already do that now.


Now it is turn on Dovecot. First we will backup dovecot configuration file as we did with Postfix configuration files:

cd /etc/dovecot/
cp dovecot.conf dovecot.conf.backup

Next edit dovecot.conf file and ensure that following is enabled:

protocols = imap pop3 lmtp

Next switch to dovecot conf.d directory and backup files which will be edited next:

cd /etc/dovecot/conf.d/
cp 10-auth.conf 10-auth.conf.backup
cp 10-logging.conf 10-logging.conf.backup
cp 10-mail.conf 10-mail.conf.backup
cp 10-master.conf 10-master.conf.backup
cp 10-ssl.conf 10-ssl.conf.backup
cp 15-lda.conf 15-lda.conf.backup
cp 90-sieve.conf 90-sieve.conf.backup

After you have backed up those files edit them and ensure they have following contents:


disable_plaintext_auth = yes
#!include auth-system.conf.ext
!include auth-passwdfile.conf.ext


log_path = /var/log/dovecot.log
auth_debug = no
verbose_ssl = no


mail_home = /var/mail/vhosts/%d/%n
mail_location = maildir:~
mail_uid = 1000    # These are the GID and UID numbers for vpostfix
mail_gid = 1000    # Don't just put these numbers here
mail_privileged_group = vpostfix


unix_listener auth-userdb {
  mode = 0600
  user = postfix
  group =  postfix
# Postfix smtp-auth
unix_listener /var/spool/postfix/private/auth {
  mode = 0660
  user = postfix
  group = postfix


ssl = yes
ssl_cert = 

Remember that we changed something in 10-auth.conf? Basically we commented the line #!include auth-system.conf.ext and uncommented the !include auth-passwdfile.conf.ext. Take a look at this file (/etc/dovecot/conf.d/auth-passwdfile.conf.ext) and you will see:

passdb {
  driver = passwd-file
  args = scheme=CRYPT username_format=%u /etc/dovecot/users
userdb {
  driver = passwd-file
  args = username_format=%u /etc/dovecot/users

That means that our user name/password database will be in the file /etc/dovecot/users. To generate a password with SHA512-CRYPT password scheme you can use

doveadm pw -s SHA512-CRYPT

You’ll be prompted to enter a password twice and the output will be similar to this.

doveadm pw -s SHA512-CRYPT
Enter new password:
Retype new password:

If you want to use a different password scheme, take a look at official documentation.

So to define users and user password we need to generate hash of the password using above mentioned command and edit /etc/dovecot/users to insert the password after the user name.

To create user@example.com your entry needs to look something like:


Don’t forget to add 4 colons after the password “::::”.

With all set let’s test if email authentification works:

doveadm auth test -a /var/spool/postfix/private/auth user@example.com EmailTest

Hopefully it works, in that case you should get result similar to this:

passdb: user@example.com auth succeeded
extra fields:

You can also test IMAP or POP3 login via telnet. To install telnet you can use

yum install telnet.x86_64

Note that when logged in postfix will automatically create directory structure for the user so I definitely recommend this :-)

Usage is like:

telnet localhost 110
user user@example.com
pass EmailTest
telnet localhost 143
login user@example.com EmailTest
list “” “*”

With that we should have finished setting up dovecot.


Next, it is turn on Amavisd which will be used as bridge between Postfix, and SpamAssassin.

Edit /etc/amavisd/amavisd.conf and ensure it contains:

@bypass_virus_checks_maps = (1); # controls running of anti-virus code
# @bypass_spam_checks_maps = (1); # controls running of anti-spam code
# $bypass_decode_parts = 1; # controls running of decoders&dearchivers
$daemon_user = 'amavis'; # (no default; customary: vscan or amavis), -u
$daemon_group = 'amavis'; # (no default; customary: vscan or amavis), -g
$mydomain = 'example.com'; # a convenient default for other settings (change it)
$MYHOME = '/var/spool/amavis'; # a convenient default for other settings, -H
@local_domains_maps = ( [".$mydomain","example2.com"] ); # list of all domains
$myhostname = 'mail.example.com'; # must be a FQDN

I have commented out option “bypass_virus_checks_maps” because we’re not setting up ClamAV for virus scanning since it requires more RAM. It is recommended to use server with at least 2GB of RAM when running ClamAV so I skipped this step entirely but if you want to know how to enable it please state so and I’ll make sure to update this article or to write a follow up.

Now, let’s tie everything together with Postfix. Edit /etc/postfix/master.cf and add these lines at the end.

# Amavisd
amavisfeed unix - - n - 2 lmtp
-o lmtp_data_done_timeout=1200
-o lmtp_send_xforward_command=yes inet n - n - - smtpd
-o content_filter=
-o smtpd_delay_reject=no
-o smtpd_client_restrictions=permit_mynetworks,reject
-o smtpd_helo_restrictions=
-o smtpd_sender_restrictions=
-o smtpd_recipient_restrictions=permit_mynetworks,reject
-o smtpd_data_restrictions=reject_unauth_pipelining
-o smtpd_end_of_data_restrictions=
-o smtpd_restriction_classes=
-o mynetworks=
-o smtpd_error_sleep_time=0
-o smtpd_soft_error_limit=1001
-o smtpd_hard_error_limit=1000
-o smtpd_client_connection_count_limit=0
-o smtpd_client_connection_rate_limit=0
-o receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_milters,no_address_mappings
-o local_header_rewrite_clients=
-o smtpd_milters=
-o local_recipient_maps=
-o relay_recipient_maps=

Edit /etc/postfix/main.cf and add this to the end:

# Amavisd
content_filter = amavisfeed:[]:10024


Perform initial update of SpamAssassin:


To update SpamAssassin definitions you can set up Cron job to do it

crontab -e

and place following entry in it:

1 0 * * * /bin/sa-update && /bin/systemctl restart spamassassin.service

This will update SpamAssassin first minute after midnight (every day) and restart SpamAssassin service after that.

Now, let’s restart all of the services:

systemctl restart postfix.service dovecot.service amavisd.service spamassassin.service

We should also enable them at boot:

systemctl enable postfix.service dovecot.service amavisd.service spamassassin.service

Dovecot Pigeonhole

OK, we have set up majority of things, next, we’ll need to set up email filtering. To do that we will use pigeonhole

You should have following files in your /etc/dovecot/conf.d/ folder already:


Add this to the end of your /etc/postfix/main.cf file:

virtual_transport = lmtp:unix:private/dovecot-lmtp

Then edit the following files located in /etc/dovecot/conf.d/ and make sure that it contains options as noted below (though some of them should already since we did that in postfix configuration section):


mail_home = /var/mail/vhosts/%d/%n
mail_location = maildir:~
mail_privileged_group = vpostfix


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


recipient_delimiter = +
mail_plugins = $mail_plugins sieve


lmtp_save_to_detail_mailbox = yes

protocol lmtp {
# Space separated list of plugins to load (default is global mail_plugins).
postmaster_address = joe@whatsup.com
mail_plugins = $mail_plugins sieve


recipient_delimiter = +

If user directory still isn’t created, log in to that account first (with telnet via POP3 as explained in Dovecot section) so Postfix can set up directory structure for that account.

Now, go to the mail directory of one of the virtual users.

cd /var/mail/vhosts/example.com/user

and create file named .dovecot.sieve (hidden file) with the following Sieve commands inside.

require "fileinto";
if header :comparator "i;ascii-casemap" :contains "Subject" "***Spam***" {
fileinto "Junk";

That basically means that if an e-mail arrives flagged with “***Spam***” in the subject (recognized as SPAM by SpamAssassin) it will move it to the “Junk” folder.

For this to work make sure you have Junk folder created. Folder needs to be named .Junk in user email directory. For example like this:


ensure following permissions are set:

ls -ld /var/mail/vhosts/example.com/user/.Junk/
drwx------ 5 vpostfix vpostfix 4096 Sep 26 18:34 /var/mail/vhosts/example.com/user/.Junk/

Now onto the real tests. To test if messages are correctly filtered as Junk we will be sending test SPAM email message while monitoring the logs.

In one terminal window start following command:

tail -f /var/log/maillog

and then send plain-text email with following contents:


You should see something like this in your email logs:

Sep 26 20:53:38 digitalocean amavis[3626]: (03626-02) Blocked SPAM {DiscardedInbound,Quarantined}, [2a00:1450:400c:c09::22e]:36535 [2a00:1450:400c:c09::22e] <something@gmail.com> -> <user@example>, Queue-ID: 1D744418B2, Message-ID: <CAGnadjHX9wfu-icP5mNAH1ME6RRZuYYc16FU-W+D5N2W3ODJTg@mail.gmail.com>, mail_id: xC78jb4x_e9j, Hits: 999.901, size: 2362, dkim_sd=20120113:gmail.com, 583 ms

Which means message was detected as spam and hopefully it should be sorted in Junk folder ;-)

I suggest rebooting your server and seeing if all services are up and running after that and if they’re working correctly.

Thank you!

At last, I should mention that this article is an evolved version of following article: #63 FreeBSD 10: postfix, dovecot, Roundcube, amavisd-new, spamassassin, clamav, pigeonhole. Big thanks to author of original article for breaking down things in a simple way.

This article will hopefully evolve over time and things like different webmail clients, OpenDKIM setup and similar stuff may be added to it or I may post a follow up article.

Tagged in:
Sysadmin on the everlasting journey of learning. Always in search for an opportunity to prove myself and to learn something new. My addiction is learning and my main goal is to excel in every aspect of Linux/Unix system administration.


Leave a Reply

Your email address will not be published. Required fields are marked *