Virtual user mail system

Related articles

This article describes how to set up a complete virtual user mail system on an Arch Linux system in the simplest manner possible. However, since a mail system consists of many complex components, quite a bit of configuration will still be necessary.

Roughly, the components used in this article are Postfix as the mail server, Dovecot as the IMAP server, Roundcube as the webmail interface and PostfixAdmin as the administration interface to manage it all.

In the end, the provided solution will allow you to use the best currently available security mechanisms, you will be able to send mails using SMTP and SMTPS and receive mails using POP3, POP3S, IMAP and IMAPS. Additionally, configuration will be easy thanks to PostfixAdmin and users will be able to login using Roundcube. What a deal!

Installation

Before you start, you must have both a working MySQL server as described in MySQL and a working Postfix server as described in Postfix.

Install the dovecot and roundcubemail packages from the official repositories.

Configuration

User

For security reasons, a new user should be created to store the mails:

# groupadd -g 5000 vmail
# useradd -u 5000 -g vmail -s /usr/bin/nologin -d /home/vmail -m vmail

A gid and uid of 5000 is used in both cases so that we do not run into conflicts with regular users. All your mail will then be stored in /home/vmail. You could change the home directory to something like /var/mail/vmail but be careful to change this in any configuration below as well.

Database

You will need to create an empty database and corresponding user. In this article, the user postfix_user will have read/write access to the database postfix_db using hunter2 as password. You are expected to create the database and user yourself, and give the user permission to use the database, as shown in the following code.

$ mysql -u root -p
CREATE DATABASE postfix_db;
USE postfix_db;
CREATE USER postfix_user@localhost IDENTIFIED BY 'hunter2';
GRANT ALL ON postfix_db.* TO postfix_user@localhost;
FLUSH PRIVILEGES;

Now you can go to the PostfixAdmin's setup page, let PostfixAdmin create the needed tables and create the users in there.

PostfixAdmin

See Postfix#PostfixAdmin.

SSL certificate

You will need a SSL certificate for SMTPS. If you do not have one, create one:

# cd /etc/ssl/private/
# openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out server.key
# chmod 400 server.key
# openssl req -new -key server.key -out server.csr
# openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
# chmod 444 server.crt

Postfix

Enable secure SMTP as described in Postfix#Secure SMTP.

To /etc/postfix/main.cf append:

relay_domains = *
virtual_alias_maps = proxy:mysql:/etc/postfix/virtual_alias_maps.cf
virtual_mailbox_domains = proxy:mysql:/etc/postfix/virtual_mailbox_domains.cf
virtual_mailbox_maps = proxy:mysql:/etc/postfix/virtual_mailbox_maps.cf
virtual_mailbox_base = /home/vmail
virtual_mailbox_limit = 512000000
virtual_minimum_uid = 5000
virtual_transport = virtual
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
local_transport = virtual
local_recipient_maps = $virtual_mailbox_maps
transport_maps = hash:/etc/postfix/transport

smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = /var/run/dovecot/auth-client
smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
smtpd_sasl_security_options = noanonymous
smtpd_sasl_tls_security_options = $smtpd_sasl_security_options
smtpd_tls_auth_only = yes
smtpd_tls_cert_file = /etc/ssl/private/server.crt
smtpd_tls_key_file = /etc/ssl/private/server.key
smtpd_sasl_local_domain = $mydomain
broken_sasl_auth_clients = yes
smtpd_tls_loglevel = 1
Warning: relay_domains = * might be a bad idea (see http://www.postfix.org/BASIC_CONFIGURATION_README.html#relay_to). You usually do not want postfix to forward mail from strangers.

virtual_mailbox_domains is a list of the domains that you want to receive mail for. This CANNOT contain the domain that is set in mydestination. That is why we left mydestination to be localhost only.

virtual_mailbox_maps will contain the info about the virtual users and their mailbox locations. We are using a hash file to store the more permanent maps, and these will override the forwards in the MySQL database.

virtual_mailbox_base is the base directory where the virtual mailboxes will be stored.

The gid and uid maps are the real system user account that the virtual mail will be owned by. This is for storage purposes. Since we will be using a web interface, and do not want people accessing this by any other means, we will be creating this account later with no login access.

This references a lot of files that do not even exist yet. Create them with the following contents:

If setting up without PostfixAdmin

/etc/postfix/virtual_alias_maps.cf
user = postfix_user
password = hunter2
hosts = localhost
dbname = postfix_db
table = domains
select_field = virtual
where_field = domain
/etc/postfix/virtual_mailbox_domains.cf
user = postfix_user
password = hunter2
hosts = localhost
dbname = postfix_db
table = forwardings
select_field = destination
where_field = source
/etc/postfix/virtual_mailbox_maps.cf
user = postfix_user
password = hunter2
hosts = localhost
dbname = postfix_db
table = users
select_field = concat(domain,'/',email,'/')
where_field = email
Note: Instead of having a directory structure something like /home/vmail/example.com/user@example.com you can have cleaner subdirectories (without the additional domain name) by replacing select_field and where_field with:
query = SELECT CONCAT(SUBSTRING_INDEX(email,'@',-1),'/',SUBSTRING_INDEX(email,'@',1),'/') FROM users WHERE email='%s'

If setting up with PostfixAdmin and created schema through PostfixAdmin

/etc/postfix/virtual_alias_maps.cf
user = postfix_user
password = hunter2
hosts = localhost
dbname = postfix_db
table = alias
select_field = goto
where_field = address
/etc/postfix/virtual_mailbox_domains.cf
user = postfix_user
password = hunter2
hosts = localhost
dbname = postfix_db
table = domain
select_field = domain
where_field = domain
/etc/postfix/virtual_mailbox_maps.cf
user = postfix_user
password = hunter2
hosts = localhost
dbname = postfix_db
table = mailbox
select_field = maildir
where_field = username

Run postmap on transport to generate its db:

# postmap /etc/postfix/transport

Dovecot

Start by creating a new /etc/dovecot/dovecot.conf with the following contents:

protocols = imap
auth_mechanisms = plain
passdb {
    driver = sql
    args = /etc/dovecot/dovecot-sql.conf
}
userdb {
    driver = sql
    args = /etc/dovecot/dovecot-sql.conf
}

service auth {
    unix_listener auth-client {
        group = postfix
        mode = 0660
        user = postfix
    }
    user = root
}

mail_home = /home/vmail/%d/%u
mail_location = maildir:~

ssl_cert = </etc/ssl/private/server.crt
ssl_key = </etc/ssl/private/server.key

If you instead want to modify dovecot.conf.sample, beware that the default configuration file imports the content of conf.d/*.conf. Those files call other files that aren't present in our configuration.

See http://wiki2.dovecot.org/Variables for the docevot variables %d and %u.

Now obviously we also need the /etc/dovecot/dovecot-sql.conf we just referenced in the config above. Go ahead and create a /etc/dovecot/dovecot-sql.conf with these contents:

driver = mysql
connect = host=localhost dbname=postfix_db user=postfix_user password=hunter2
# The new name for MD5 is MD5-CRYPT so you might need to change this depending on version
default_pass_scheme = MD5-CRYPT
# Get the mailbox
user_query = SELECT '/home/vmail/%d/%u' as home, 'maildir:/home/vmail/%d/%u' as mail, 5000 AS uid, 5000 AS gid, concat('dirsize:storage=',  quota) AS quota FROM mailbox WHERE username = '%u' AND active = '1'
# Get the password
password_query = SELECT username as user, password, '/home/vmail/%d/%u' as userdb_home, 'maildir:/home/vmail/%d/%u' as userdb_mail, 5000 as  userdb_uid, 5000 as userdb_gid FROM mailbox WHERE username = '%u' AND active = '1'
# If using client certificates for authentication, comment the above and uncomment the following
#password_query = SELECT null AS password, ‘%u’ AS user

And without PostfixAdmin you can use:

driver = mysql
connect = host=localhost dbname=postfix_db user=postfix_user password=hunter2
# The new name for MD5 is MD5-CRYPT so you might need to change this depending on version
default_pass_scheme = MD5-CRYPT
# Get the mailbox
user_query = SELECT '/home/vmail/%d/%u' as home, 'maildir:/home/vmail/%d/%u' as mail, 5000 AS uid, 5000 AS gid, concat('dirsize:storage=',  quota) AS quota FROM users WHERE email = '%u'
# Get the password
password_query = SELECT email as user, password, '/home/vmail/%d/%u' as userdb_home, 'maildir:/home/vmail/%d/%u' as userdb_mail, 5000 as  userdb_uid, 5000 as userdb_gid FROM users WHERE email = '%u'
# If using client certificates for authentication, comment the above and uncomment the following
#password_query = SELECT null AS password, ‘%u’ AS user

PostfixAdmin

See Postfix#PostfixAdmin.

Roundcube

Roundcube needs a separate database to work. You'll need to provide information about the database during installation. Make sure that the pdo_mysql.so extension is uncommented in your php.ini file. Also check the .htaccess for access restrictions. Assuming that localhost is your current host, navigate a browser to http://localhost/roundcube/installer/ and follow the instructions. You should not use the same database for Roundcube that you already used for PostfixAdmin: create a second database "roundcube_db" and a "roundcube_user" for use with Roundcube.

While running the installer, make sure to address the IMAP host with ssl://localhost/ or tls://localhost/ instead of just localhost. Use port 993. Likewise with SMTP, make sure to provide ssl://localhost/ on port 465 if you used the wrapper mode and tls://localhost/ on port 587 if you used the proper TLS mode. See here for an explanation on that.

The post install process is similar to any other webapps like PhpMyAdmin or PostFixAdmin. The configuration file is /etc/webapps/roundcubemail/config/config.inc.php which works as an override over default.inc.php.

If you are using Apache, copy the example configuration file to your webserver configuration directory.

# cp /etc/webapps/roundcubemail/apache.conf /etc/httpd/conf/extra/httpd-roundcubemail.conf

Add the include line in /etc/httpd/conf/httpd.conf

Include conf/extra/httpd-roundcubemail.conf

To let users change their passwords from within Roundcube, do the following:

Enable password plugin by adding this line to /etc/webapps/roundcubemail/config/config.inc.php:

$rcmail_config['plugins'] = array('password');

Configure password plugin in /usr/share/webapps/roundcubemail/plugins/password/config.inc.php:

$config['password_driver'] = 'sql';
$config['password_db_dsn'] = 'mysql://postfix_database_user:password@localhost/postfix_database_name';
$config['password_query'] = 'UPDATE mailbox SET password=%c WHERE username=%u';

Fire it up

Since now hopefully everything is set up correctly, all necessary daemons should be started for a test run:

# systemctl start postfix dovecot

Now for testing purposes, create a domain and mail account in PostfixAdmin. Try to login to this account using Roundcube. Now send yourself a mail.

Optional Items

Although these items are not required, they definitely add more completeness to your setup

Quota

To enable mailbox quota support by dovecot, do the following:

  • First add the following lines to /etc/dovecot/dovecot.conf
dict {
	quotadict = mysql:/etc/dovecot/dovecot-dict-sql.conf.ext
}
service dict {
	unix_listener dict {
		group = vmail
		mode = 0660
		user = vmail
	}
	user = root
}
service quota-warning {
	executable = script /usr/local/bin/quota-warning.sh
	user = vmail
	unix_listener quota-warning {
		group = vmail
		mode = 0660
		user = vmail
	}
}	
mail_plugins=quota
protocol pop3 {
	 mail_plugins = quota
	 pop3_client_workarounds = outlook-no-nuls oe-ns-eoh
	 pop3_uidl_format = %08Xu%08Xv
}
protocol lda {
	mail_plugins = quota
	postmaster_address = postmaster@yourdomain.com
}
protocol imap {
	mail_plugins = $mail_plugins imap_quota
	mail_plugin_dir = /usr/lib/dovecot/modules
}
plugin {
	quota = dict:User quota::proxy::quotadict
	quota_rule2 = Trash:storage=+10%%
	quota_warning = storage=95%% quota-warning 95 %u
	quota_warning2 = storage=80%% quota-warning 80 %u
	quota_warning3 = -storage=100%% quota-warning below 100 %u # user is no longer over quota
}
  • Create a new file /etc/dovecot/dovecot-dict-sql.conf.ext with the following code:
connect = host=localhost dbname=yourdb user=youruser password=yourpassword
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
}
  • Create a warning script /usr/local/bin/quota-warning.sh and make sure it is executable.
#!/bin/sh
PERCENT=$1
USER=$2
cat << EOF | /usr/lib/dovecot/dovecot-lda -d $USER -o "plugin/quota=maildir:User quota:noenforcing"
From: postmaster@yourdomain.com
Subject: quota warning

THIS MESSAGE IS AUTOMATICALLY GENERATED BY THE MAIL SYSTEM. DO NOT REPLY TO IT.

Dear User,
Your mailbox is now $PERCENT% full. Please remove some mails from the server.
Regards,
Postmaster
EOF
  • Edit the user_query line and add iterat_query in dovecot-sql.conf as following:
 user_query = SELECT '/home/vmail/%d/%u' as home, 'maildir:/home/vmail/%d/%u' as mail, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active = '1'
 iterate_query = SELECT username AS user FROM mailbox
  • Set up LDA as described above under SpamAssassin. If you're not using SpamAssassin, the pipe should look like this in /etc/postfix/master.cf :
 dovecot    unix  -       n       n       -       -       pipe
 flags=DRhu user=vmail:vmail argv=/usr/lib/dovecot/deliver -f ${sender} -d ${recipient}

As above activate it in Postfix main.cf

 virtual_transport = dovecot
  • You can set up quota per each mailbox in postfixadmin. Make sure the relevant lines in config.inc.php look like this:
$CONF['quota'] = 'YES';
$CONF['quota_multiplier'] = '1024000';

Restart postfix and dovecot services. If things go well, you should be able to list all users' quota and usage by the this command:

doveadm quota get -A

You should be able to see the quota in roundcube too.

Troubleshooting

If you get errors like your imap/pop3 client failing to receive mails, take a look into your /var/log/mail.log file. It turned out that the maildir /home/vmail/mail@domain.tld is just being created if there is at least one email waiting. Otherwise there wouldn't be any need for the directory.