Index
• Introduction
• Step #1: Create databases
• Step #2: Configure DNS
• Step #3: Add user and create directory
• Step #4: Configure PostfixAdmin
• Step #5: Install and configure Postfix
• Step #6: Install and configure Dovecot
• Step #7: Configure Roundcube
• Step #8: Start the services
• Step #9: Initial settings with PostfixAdmin
• Postscripts: Debugging
• References
Introduction
What services do we want:
▪ IMAP(S)
▪ SMTP(S)
▪ POP3(S)
What features do we want:
▪ Let’s Encrypt SSL
▪ Dovecot SASL
▪ Quota
▪ Mail sender address restrictions
▪ Web Mail (Roundcube)
▪ Web Administratiron (PostfixAdmin)
What do we need:
▪ Postfix (3.1.0-3)
▪ Dovecot (2.2.22-1ubuntu2)
▪ MySQL (5.7.15-1ubuntu16.04)
▪ php7 (7.0+44+deb.sury.org~xenial+1)
▪ nginx (1.11.3-0+xenial0)
▪ Roundcube (1.2.1)
▪ PostfixAdmin (2.93)
You don’t have to download the vary listed versions, they are only examples.
Step #1: Create databases
Here we need two databases: one for PostfixAdmin and the other for roundcube.
Database User Password mail_auth mail_auth mail_auth_example roundcube roundcube roundcube_example
You’d better choose utf8_unicode_ci in order to avoid problems.
It’s a good choice to use phpMyAdmin if you’re not familiar to MySQL commands. It’s one of my favorite tools which is of great convenience.
Step #2: Configure DNS
(Sub) Domain Type Data @ MX 10 smtp.example.com. mail A 111.111.111.111 smtp A 111.111.111.111 imap A 111.111.111.111 pop3 A 111.111.111.111
Please edit the records based on your own environment.
Step #3: Add user and create directory
Run:
useradd -r -u 150 -g mail -d /var/vmail -s /sbin/nologin -c "Virtual mailbox" vmail mkdir /var/vmail chmod 770 /var/vmail/ chown vmail:mail /var/vmail/
Step #4: Configure PostfixAdmin
Download PostfixAdmin and extract it to /var/www/PostfixAdmin
Edit /var/www/PostfixAdmin/config.inc.php
// Only lines to edit listed here // DO NOT copy the text directly to your configuration document $CONF['configured'] = true; $CONF['default_language'] = 'en'; $CONF['database_type'] = 'mysqli'; $CONF['database_host'] = 'localhost'; $CONF['database_user'] = 'mail_auth'; $CONF['database_password'] = 'mail_auth_example'; $CONF['database_name'] = 'mail_auth'; $CONF['encrypt'] = 'md5crypt'; $CONF['domain_path'] = 'YES'; $CONF['domain_in_mailbox'] = 'NO'; $CONF['maildir_name_hook'] = 'NO'; $CONF['aliases'] = '0'; $CONF['mailboxes'] = '0'; $CONF['maxquota'] = '0'; $CONF['domain_quota_default'] = '2048'; $CONF['quota'] = 'YES'; $CONF['domain_quota'] = 'YES'; $CONF['quota_multiplier'] = '1048576'; $CONF['used_quotas'] = 'YES'; $CONF['new_quota_table'] = 'YES'; $CONF['mailbox_postdeletion_script'] = 'sudo -u vmail -g mail /var/vmail/Bin/postfixadmin-mailbox-postdeletion.sh'; $CONF['domain_postdeletion_script'] = 'sudo -u vmail -g mail /var/vmail/Bin/postfixadmin-domain-pos deletion.sh';
And then, we should create the Post-deletion scripts.
#!/bin/sh
# Example script for removing a Maildir domain top-level folder
# from a Courier-IMAP virtual mail hierarchy.
# The script only looks at argument 1, assuming that it
# indicates the relative name of a domain, such as
# "somedomain.com". If $basedir/somedomain.com exists, it will
# be removed.
# The script will not actually delete the directory. I moves it
# to a special directory which may once in a while be cleaned up
# by the system administrator.
# This script should be run as the user which owns the maildirs. If
# the script is actually run by the apache user (e.g. through PHP),
# then you could use "sudo" to grant apache the rights to run
# this script as the relevant user.
# Assume this script has been saved as
# /usr/local/bin/postfixadmin-domain-postdeletion.sh and has been
# made executable. Now, an example /etc/sudoers line:
# apache ALL=(courier) NOPASSWD: /usr/local/bin/postfixadmin-domain-postdeletion.sh
# The line states that the apache user may run the script as the
# user "courier" without providing a password.
# Change this to where you keep your virtual mail users' maildirs.
basedir=/var/vmail
# Change this to where you would like deleted maildirs to reside.
trashbase=/var/vmail/Trash
if [ `echo $1 | fgrep '..'` ]; then
echo "First argument contained a double-dot sequence; bailing out."
exit 1
fi
if [ ! -e "$trashbase" ]; then
echo "trashbase '$trashbase' does not exist; bailing out."
exit 1
fi
trashdir="${trashbase}/`date +%F_%T`_$1"
domaindir="${basedir}/$1"
if [ ! -e "$domaindir" ]; then
echo "Directory '$domaindir' does not exits; nothing to do."
exit 0;
fi
if [ ! -d "$domaindir" ]; then
echo "'$domaindir' is not a directory; bailing out."
exit 1
fi
if [ -e "$trashdir" ]; then
echo "Directory '$trashdir' already exits; bailing out."
exit 1;
fi
mv $domaindir $trashdir
exit $?
#!/bin/sh
# Example script for removing a Maildir from a Courier-IMAP virtual mail
# hierarchy.
# The script looks at arguments 1 and 2, assuming that they
# indicate username and domain, respectively.
# The script will not actually delete the maildir. I moves it
# to a special directory which may once in a while be cleaned up
# by the system administrator.
# This script should be run as the user which owns the maildirs. If
# the script is actually run by the apache user (e.g. through PHP),
# then you could use "sudo" to grant apache the rights to run
# this script as the relevant user.
# Assume this script has been saved as
# /usr/local/bin/postfixadmin-mailbox-postdeletion.sh and has been
# made executable. Now, an example /etc/sudoers line:
# apache ALL=(courier) NOPASSWD: /usr/local/bin/postfixadmin-mailbox-postdeletion.sh
# The line states that the apache user may run the script as the
# user "courier" without providing a password.
# Change this to where you keep your virtual mail users' maildirs.
basedir=/var/vmail
# Change this to where you would like deleted maildirs to reside.
trashbase=/var/vmail/Trash
if [ ! -e "$trashbase" ]; then
echo "trashbase '$trashbase' does not exist; bailing out."
exit 1
fi
if [ `echo $1 | fgrep '..'` ]; then
echo "First argument contained a double-dot sequence; bailing out."
exit 1
fi
if [ `echo $2 | fgrep '..'` ]; then
echo "First argument contained a double-dot sequence; bailing out."
exit 1
fi
subdir=`echo "$1" | sed 's/@.*//'`
maildir="${basedir}/$2/${subdir}"
trashdir="${trashbase}/$2/`date +%F_%T`_${subdir}"
parent=`dirname "$trashdir"`
if [ ! -d "$parent" ]; then
if [ -e "$parent" ]; then
echo "Strainge - directory '$parent' exists, but is not a directory."
echo "Bailing out."
exit 1
else
mkdir -p "$parent"
if [ $? -ne 0 ]; then
echo "mkdir -p '$parent' returned non-zero; bailing out."
exit 1
fi
fi
fi
if [ ! -e "$maildir" ]; then
echo "maildir '$maildir' does not exist; nothing to do."
exit 0
fi
if [ -e "$trashdir" ]; then
echo "trashdir '$trashdir' already exists; bailing out."
exit 1
fi
mv $maildir $trashdir
exit $?
Of course, we should also grant the permissions to the web server to run the scripts as vmail.
yourname:~# sudo su root:/home/yourname# cd /etc Defaults requiretty #Comment the line out ↓ notice that if you're running Ubuntu this may be not necessary #Defaults requiretty root:/etc# cd sudoers.d root:/etc/sudoers.d# touch nginx root:/etc/sudoers.d# vim nginx www-data ALL=(vmail:mail) NOPASSWD: ALL #this will grant www-data to run anything as vmail in mail group without passwords #you can also change the NOPASSWD: ALL to NOPASSWD: /absolute/path/to/script to make it safer root:/etc/sudoers.d# service nginx restart root:/etc/sudoers.d# exit
Next, let’s go to http://mail.example.com/PostfixAdmin/
Set a password and copy the return hashed value to /var/www/PostfixAdmin/config.inc.php
$CONF['setup_password'] = 'Your Hash Here';
Finally, follow the web guidance to add an administrator account. Here we use:
Account: admin@example.com
Password: admin_example
Step #5: Install and configure Postfix
apt-get install postfix postfix-mysql postfix-pcre
When prompted, choose Internet Site and then type example.com for your domain.
After the installation, navigate to /etc/postfix and edit /etc/postfix/main.cf
# See /usr/share/postfix/main.cf.dist for a commented, more complete version
# Debian specific: Specifying a file name will cause the first
# line of that file to be used as the name. The Debian default
# is /etc/mailname.
#myorigin = /etc/mailname
smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
biff = no
# appending .domain is the MUA's job.
append_dot_mydomain = no
# Uncomment the next line to generate "delayed mail" warnings
#delay_warning_time = 4h
readme_directory = no
# TLS parameters
tls_medium_cipherlist = AES128+EECDH:AES128+EDH
smtpd_tls_cert_file = /etc/letsencrypt/live/example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/example.com/privkey.pem
smtpd_use_tls = yes
smtpd_tls_security_level = may
smtpd_tls_auth_only = no
smtpd_tls_mandatory_protocols = !SSLv3,!TLSv1,!TLSv1.1
smtpd_tls_protocols = !SSLv3,!TLSv1,!TLSv1.1
smtpd_tls_mandatory_ciphers = medium
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_use_tls = yes
smtp_tls_security_level = may
smtp_tls_mandatory_protocols = !SSLv3,!TLSv1,!TLSv1.1
smtp_tls_protocols = !SSLv3,!TLSv1,!TLSv1.1
smtp_tls_mandatory_ciphers = medium
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
# information on enabling SSL in the smtp client.
myhostname = smtp.example.com
alias_maps = hash:/etc/postfix/aliases
alias_database = hash:/etc/postfix/aliases
myorigin = smtp.example.com
mydestination = localhost
relayhost =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
# User Define
virtual_uid_maps = static:150
virtual_gid_maps = static:8
virtual_mailbox_base = /var/vmail
virtual_gid_maps = static:8
virtual_mailbox_base = /var/vmail
virtual_mailbox_domains = mysql:/etc/postfix/mysql_virtual_mailbox_domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql_virtual_mailbox_maps.cf
virtual_alias_maps = mysql:/etc/postfix/mysql_virtual_alias_maps.cf
#relay_domains = mysql:/etc/postfix/mysql_relay_domains.cf
virtual_transport = lmtp:unix:private/dovecot-lmtp
smtpd_recipient_limit = 10
smtpd_delay_reject = yes
smtpd_helo_required = yes
smtpd_sender_login_maps = pcre:/etc/postfix/login_maps.pcre
smtpd_helo_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_non_fqdn_helo_hostname,
reject_invalid_helo_hostname,
permit
smtpd_sender_restrictions =
reject_sender_login_mismatch,
reject_non_fqdn_sender,
reject_unknown_sender_domain,
permit_mynetworks,
permit_sasl_authenticated,
permit
smtpd_recipient_restrictions =
reject_non_fqdn_recipient,
reject_unknown_recipient_domain,
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination,
reject_unlisted_recipient,
permit
smtpd_relay_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
defer_unauth_destination
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
#milter_default_action = accept
# Quota
virtual_create_maildirsize = yes
virtual_mailbox_extended = yes
virtual_mailbox_limit_maps = mysql:/etc/postfix/mysql_virtual_mailbox_limit_maps.cf
virtual_mailbox_limit_override = yes
virtual_maildir_limit_message = Sorry, the user's maildir has overdrawn his diskspace quota, please try again later.
virtual_overquota_bounce = yes
Here because the configuration is not that long so I pasted a full configuration file here. You can just replace example.com with your domain and then copy it to your server.
Nowadays, you can easily generate a certificate with the help of Let’s Encrypt. They have got a convenient online guide: Certbot. Open the link and select Nginx & Ubuntu, then add the following block to your Nginx configuration if needed,
location ~ ^/\.well-known\/.* {
allow all;
log_not_found off;
}
Finally just follow the guide to get your certificates. Note that appending --rsa-key-size 4096 to your command is highly recommended.
Next, we have to edit the /etc/postfix/master.cf
submission inet 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_client_restrictions=permit_sasl_authenticated,reject # -o smtpd_helo_restrictions=$mua_helo_restrictions # -o smtpd_sender_restrictions=$mua_sender_restrictions # -o smtpd_recipient_restrictions= # -o smtpd_relay_restrictions=permit_sasl_authenticated,reject # -o milter_macro_daemon_name=ORIGINATING smtps inet 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_client_restrictions=permit_sasl_authenticated,reject # -o smtpd_helo_restrictions=$mua_helo_restrictions # -o smtpd_sender_restrictions=$mua_sender_restrictions # -o smtpd_recipient_restrictions= # -o smtpd_relay_restrictions=permit_sasl_authenticated,reject # -o milter_macro_daemon_name=ORIGINATING #### Add the following line to the end of the file dovecot unix - n n - - pipe flags=DRhu user=vmail:mail argv=/usr/lib/dovecot/deliver-lda -d ($recipient)
Finally, create other files under /etc/postfix
root@mail.example.com:/etc/postfix# cat login_maps.pcre
/^(.*)@example.com$/ ${1}@example.com
root@mail.example.com:/etc/postfix# cat mysql_virtual_alias_maps.cf
hosts = 127.0.0.1
user = mail_auth
password = mail_auth_example
dbname = mail_auth
query = SELECT goto FROM alias WHERE address='%s' AND active = 1
root@mail.example.com:/etc/postfix# cat mysql_virtual_mailbox_domains.cf
hosts = 127.0.0.1
user = mail_auth
password = mail_auth_example
dbname = mail_auth
query = SELECT domain FROM domain WHERE domain='%s' and backupmx = 0 and active = 1
root@mail.example.com:/etc/postfix# cat mysql_virtual_mailbox_limit_maps.cf
hosts = 127.0.0.1
user = mail_auth
password = mail_auth_example
dbname = mail_auth
query = SELECT quota FROM mailbox WHERE username = '%s' AND active = 1
root@mail.example.com:/etc/postfix# cat mysql_virtual_mailbox_maps.cf
hosts = 127.0.0.1
user = mail_auth
password = mail_auth_example
dbname = mail_auth
query = SELECT maildir FROM mailbox WHERE username='%s' AND active = 1
Step #6: Install and configure Dovecot
apt-get install dovecot-core dovecot-imapd dovecot-lmtpd dovecot-mysql dovecot-pop3d
After the installation, navigate to /etc/dovecot/conf.d and modify the files. Take care of commenting and uncommenting.
# 10-auth.conf
disable_plaintext_auth = no
#!include auth-system.conf.ext
!include auth-sql.conf.ext
# 10-mail.conf
mail_location = maildir:/var/vmail/%d/%n:INDEX=/var/vmail/%d/%n/indexes
first_valid_uid = 150
last_valid_uid = 150
mail_plugins = quota
# 10-master.conf
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0660
user = postfix
group = postfix
}
}
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0666
}
}
service dict {
unix_listener dict {
mode = 0600
user = vmail
group = mail
}
}
# 10-ssl.conf
ssl = yes
ssl_cert = </etc/letsencrypt/live/example.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/example.com/privkey.pem
# 15-lda.conf
postmaster_address =postmaster@%d
protocol lda {
mail_plugins = $mail_plugins
}
# 15-mailboxes.conf
namespace inbox {
mailbox Drafts {
special_use = \Drafts
}
mailbox Junk {
auto = subscribe
special_use = \Junk
}
mailbox Trash {
auto = subscribe
special_use = \Trash
}
mailbox Sent {
special_use = \Sent
}
mailbox "Sent Messages" {
special_use = \Sent
}
}
# 20-imap.conf
imap_max_line_length = 64k
protocol imap {
mail_plugins = $mail_plugins imap_quota
mail_max_userip_connections = 10
}
# 20-pop3.conf
protocol pop3 {
mail_plugins = $mail_plugins
}
# 90-quota.conf
plugin {
#quota = dict:user::proxy::quota
#quota2 = dict:domain:%d:proxy::quota_domain
#quota_rule = *:storage=102400
#quota2_rule = *:storage=1048576
quota = dict:User quota::proxy::sqlquota
quota2 = dict:Domain quota:%d:proxy::sqlquota
}
dict {
sqlquota = mysql:/etc/dovecot/dovecot-dict-sql.conf.ext
}
Finally, create the other required files under /etc/dovecot
driver = mysql
connect = host=localhost dbname=mail_auth user=mail_auth password=mail_auth_example
default_pass_scheme = MD5-CRYPT
password_query = SELECT password FROM mailbox WHERE username = '%u'
user_query = SELECT '/var/vmail/%d/%n' as home, CONCAT('*:storage=', quota , 'B') AS quota_rule, 150 AS uid, 8 AS gid FROM mailbox WHERE username = '%u'
connect = host=localhost dbname=mail_auth user=mail_auth password=mail_auth_example
map {
pattern = priv/quota/storage
table = quota2
username_field = username
value_field = bytes
}
map {
pattern = priv/quota/messages
table = quota2
username_field = username
value_field = messages
}
#map {
# pattern = shared/expire/$user/$mailbox
# table = expires
# value_field = expire_stamp
#
# fields {
# username = $user
# mailbox = $mailbox
# }
#}
Step #7: Configure Roundcube
Download Roundcube complete version and extract it to /var/www/mail
Then visit http://mail.example.com/mail/installer/
Follow the guidance, check your dependencies and you’ll get a configuration file.
All of the hosts including MySQL, IMAP, SMTP are localhost, and just use the default ports as you do not need SSL for local communication. DO NOT forget to check the password authentication for SMTP.
Write the configuration you get to /var/www/mail/config/config.inc.php and add this line into the file to avoid abusing of the installer:
$config['enable_installer'] = false;
Finally, edit the /var/www/mail/config/defaults.inc.php
// Only lines to edit listed here // DO NOT copy the text directly to your configuration document $config['db_dsnw'] = 'mysql://roundcube:roundcube_example@localhost/roundcube'; $config['identities_level'] = 3; $config['quota_zero_as_unlimited'] = true;
The file is quite long but with lots of useful configurations in it, you can read it if you’re interested.
Step #8: Start the services
service postfix restart service dovecot restart service mysql restart service php5-fpm restart service nginx restart
Many warning will be printed while starting Postfix, just ignore them.
Step #9: Initial settings with PostfixAdmin
Visit http://mail.example.com/PostfixAdmin
Click Domain List – New Domain (You can change the numbers according to your requirements)
Domain example.com Description Example site Aliases 200 Mailboxes 100 Max Mailbox Quota 2048 Domain Quota 819200 Mail server is backup MX Unchecked Active Checked Add default mail aliases Checked
Then you can click Virtual List – Add Mailbox to add your users.
Postscripts: Debugging
Check the log files: /var/log/syslog, /var/log/mail.log
References
“Debian Wheezy Mail Server – Postfix Dovecot Sasl MySQL PostfixAdmin RoundCube SpamAssassin Clamav Greylist Nginx PHP5”. XenLens. Last modified August 24, 2014. Accessed August 29, 2014. http://www.xenlens.com/debian-wheezy-mail-server-postfix-dovecot-sasl-mysql-postfixadmin-roundcube-spamassassin-clamav-greylist-nginx-php5/.
“MAIL SERVER HOWTO – POSTFIX AND DOVECOT WITH MYSQL AND TLS/SSL, POSTGREY AND DSPAM”. Johnny Chadda. Last modified April 15, 2007. Accessed August 29, 2014. http://johnny.chadda.se/article/mail-server-howto-postfix-and-dovecot-with-mysql-and-tlsssl-postgrey-and-dspam/.
Updates
2014/09/02 Commented reject_unauth_destination from smtpd_recipient_restrictions out, this parameter should not be added here. It may cause problems on sending emails to external destinations.
2014/11/21 Support Post-deletion scripts for PostfixAdmin
2015/01/24 Add post master setting in 15-lda.conf
2015/05/11 Add permit_sasl_authenticated to smtpd_helo_restrictions to permit Outlook to log in.
2015/08/08 Fixed a mistake in “90-quota.conf” which may give clients a wrong quota; Trash directory is auto-subscribed in “15-mailboxes.conf” as some clients won’t create it; Minor changes in the bash commands where sudoers files are edited.
2015/08/14 Auto-subscribe Junk folder as the preparation of writing the tutorial for anti-spam and anti-virus.
2016/09/10 Use Let’s Encrypt instead of StartSSL.
2019/01/23 Use TLS to transfer emails to other servers if possible.
2019/03/13 Update smtpd_recipient_restrictions.

How about using LetsEncrypt SSL rather than StartSSL ? 😀
Yes it seems to be a better plan for free SSL now. StartSSL has started to cooperate with a Chinese company which I don’t trust very much.
I’ll try to use LetsEncrypt later, not only for the mail server but the web as well.
Now using Let’s Encrypt instead of StartSSL, cheers! 🙂