Virtual Mailboxes¶
Postfixadmin¶
Install Postfixadmin¶
Postfixadmin is a web module that allows you to easily manipulate virtual domains and users in a database.
Futher Reading: http://postfixadmin.sourceforge.net/
First, we'll install the required packages:
apt update && apt install php7.4-fpm php7.4-imap php7.4-mbstring php7.4-mysql php7.4-json php7.4-curl php7.4-zip php7.4-xml php7.4-bz2 php7.4-intl php7.4-gmp
Next, we'll download the actual latest version 3.2.4 from GitHub and save it in the /opt folder:
wget -P /opt https://github.com/postfixadmin/postfixadmin/archive/postfixadmin-3.2.4.tar.gz
Wget is a free software package for retrieving files using HTTP, HTTPS, FTP and FTPS. More information here: https://www.lifewire.com/uses-of-command-wget-2201085 and here: https://www.gnu.org/software/wget/manual/html_node/index.html
Now go to that folder and uncompress it:
cd /opt && tar xvf postfixadmin-3.2.4.tar.gz
And then move and rename it:
mv postfixadmin-postfixadmin-3.2.4/ /usr/share/postfixadmin
Next, we connect to MariaDB database...
mysql -u root -p
And create a database and a user:
CREATE DATABASE postfix;
CREATE USER 'postfix'@'localhost' IDENTIFIED BY 'postfix-db-password';
GRANT ALL PRIVILEGES ON `postfix` . * TO 'postfix'@'localhost';
FLUSH PRIVILEGES;
exit
Now we need to amend the Postfixadmin config file so that it knows about the database. Replace the examples with your own values:
nano /usr/share/postfixadmin/config.local.php
<?php
$CONF['database_type'] = 'mysqli';
$CONF['database_user'] = 'postfix';
$CONF['database_password'] = 'postfix-db-password';
$CONF['database_name'] = 'postfix';
$CONF['default_aliases'] = array (
'abuse' => 'abuse@example.com',
'hostmaster' => 'hostmaster@example.com',
'postmaster' => 'postmaster@example.com',
'webmaster' => 'webmaster@example.com'
);
$CONF['configured'] = true;
$CONF['fetchmail'] = 'NO';
$CONF['show_footer_text'] = 'NO';
$CONF['quota'] = 'YES';
$CONF['domain_quota'] = 'YES';
$CONF['quota_multiplier'] = '1024000';
$CONF['used_quotas'] = 'YES';
$CONF['new_quota_table'] = 'YES';
$CONF['aliases'] = '0';
$CONF['mailboxes'] = '0';
$CONF['maxquota'] = '0';
$CONF['domain_quota_default'] = '0';
?>
Postfixadmin requires permission to a sub-folder named templates_c that doesn't exist. So to avoid some errors during the installation, we need to create it manually and give the www-data user permission:
mkdir /usr/share/postfixadmin/templates_c
setfacl -R -m u:www-data:rwx /usr/share/postfixadmin/templates_c/
Now we'll change the default password scheme to something stronger:
nano /usr/share/postfixadmin/config.local.php
<?php
$CONF['encrypt'] = 'dovecot:ARGON2I';
$CONF['dovecotpw'] = "/usr/bin/doveadm pw -r 5";
if(@file_exists('/usr/bin/doveadm')) { // @ to silence openbase_dir stuff; see https://github.com/postfixadmin/postfixadmin/issues/171
$CONF['dovecotpw'] = "/usr/bin/doveadm pw -r 5"; # debian
}
Issuing SSL Certificates¶
Now we need to create the site on Nginx reverse proxy but before we do that, we need to create the SSL certificates using Acme.sh (which I assume is already installed and configured). Make sure you've entered the A and AAAA records for the new postfixadmin domain in your DNS management service (eg pfa2.example.com)
Change to root user:
su -
First issue an RSA certificate
~/.acme.sh/acme.sh --issue --dns dns_cloudns -d pfa2.example.com --keylength 4096 --key-file /etc/letsencrypt/rsa-certs/pfa2.example.com/privkey.pem --ca-file /etc/letsencrypt/rsa-certs/pfa2.example.com/chain.pem --cert-file /etc/letsencrypt/rsa-certs/pfa2.example.com/cert.pem --fullchain-file /etc/letsencrypt/rsa-certs/pfa2.example.com/fullchain.pem --pre-hook "mkdir -p /etc/letsencrypt/rsa-certs/pfa2.example.com" --post-hook "find /etc/letsencrypt/rsa-certs/pfa2.example.com/ -name '*.pem' -type f -exec chmod 600 {} \;" --renew-hook "find /etc/letsencrypt/rsa-certs/pfa2.example.com/ -name '*.pem' -type f -exec chmod 600 {} \; -exec service nginx reload \;"
~/.acme.sh/acme.sh --issue --dns dns_cloudns -d pfa2.example.com --keylength ec-384 --key-file /etc/letsencrypt/ecc-certs/pfa2.example.com/privkey.pem --ca-file /etc/letsencrypt/ecc-certs/pfa2.example.com/chain.pem --cert-file /etc/letsencrypt/ecc-certs/pfa2.example.com/cert.pem --fullchain-file /etc/letsencrypt/ecc-certs/pfa2.example.com/fullchain.pem --pre-hook "mkdir -p /etc/letsencrypt/ecc-certs/pfa2.example.com" --post-hook "find /etc/letsencrypt/ecc-certs/pfa2.example.com/ -name '*.pem' -type f -exec chmod 600 {} \;" --renew-hook "find /etc/letsencrypt/ecc-certs/pfa2.example.com/ -name '*.pem' -type f -exec chmod 600 {} \; -exec service nginx reload \;"
Exit back to the sudo shell:
exit
Configuring Nginx¶
Now we'll create the site:
nano /etc/nginx/sites-available/pfa2.example.com
server {
listen 99 ssl http2;
listen [::]:99 ssl http2;
server_name pfa2.example.com;
ssl_certificate /etc/letsencrypt/rsa-certs/pfa2.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/rsa-certs/pfa2.example.com/privkey.pem;
ssl_certificate /etc/letsencrypt/ecc-certs/pfa2.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/ecc-certs/pfa2.example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/ecc-certs/pfa2.example.com/chain.pem;
include /etc/nginx/custom-config/ssl.conf;
include /etc/nginx/custom-config/header.conf;
location / {
include /etc/nginx/custom-config/proxy.conf;
proxy_pass http://127.0.0.1:8080;
}
}
The contents of the conf files are explained in my Nginx article
Enable the site with:
ln -s /etc/nginx/sites-available/pfa2.example.com /etc/nginx/sites-enabled/
Configuring Apache¶
We then create an Apache2 virtualhost:
nano /etc/apache2/sites-available/pfa2.example.com.conf
<VirtualHost 127.0.0.1:8080>
Protocols http/1.1
ServerAdmin admin@example.com
DocumentRoot /usr/share/postfixadmin/public
ServerName pfa2.example.com
<Directory /usr/share/postfixadmin/public>
Options -Indexes +FollowSymLinks +MultiViews
AllowOverride All
Require all granted
<FilesMatch \.php$>
SetHandler "proxy:unix:/var/run/php/php7.4-fpm-www-postfixadmin.sock|fcgi://localhost/"
</FilesMatch>
<IfModule mod_apparmor.c>
AAHatName postfixadmin-a2
</IfModule>
</Directory>
SetEnvIf HTTPS on HTTPS=on
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 127.0.0.1
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
As you can see, we've allocated this site to the www-postfixadmin fpm pool (please see the PHP-FPM article for further information) and added an apparmor changehat which we'll configure shortly.
Now enable the site with:
a2ensite pfa2.example.com.conf
Creating the PHP-FPM Pool¶
First we'll create the user:
groupadd www-postfixadmin
useradd -g www-postfixadmin www-postfixadmin
We'll also create a tmp directory for PHP:
mkdir /usr/share/postfixadmin/tmp
And change permissions across the structure
chown -R www-postfixadmin:www-data /usr/share/postfixadmin
find /usr/share/postfixadmin -type d -exec chmod 550 {} \;
find /usr/share/postfixadmin -type f -exec chmod 440 {} \;
chmod g+s /usr/share/postfixadmin
chown -R www-postfixadmin:www-postfixadmin /usr/share/postfixadmin/tmp
chmod g+s /usr/share/postfixadmin/tmp
chmod 770 /usr/share/postfixadmin/templates_c
chmod 770 /usr/share/postfixadmin/templates
chmod g+s /usr/share/postfixadmin/templates_c
chmod g+s /usr/share/postfixadmin/templates
find /usr/share/postfixadmin -type f -name '*.php' -exec chmod 600 {} \;
Next we add www-postfixadmin to the dovecot group so that it has the required permissions to perform certain tasks:
usermod -aG dovecot www-postfixadmin
Now we create our PHP-FPM pool:
nano /etc/php/7.4/fpm/pool.d/www-postfixadmin.conf
; pool name
[www-postfixadmin]
; Unix user/group of processes
; will be used.
user = www-postfixadmin
group = www-postfixadmin
listen.owner = www-data
listen.group = www-data
; Socket to which the Apache will connect
listen = /run/php/php7.4-fpm-www-postfixadmin.sock
apparmor_hat = postfixadmin
pm = ondemand
pm.max_children = 5
pm.process_idle_timeout = 10s
;pm.start_servers = 2
;pm.min_spare_servers = 1
;pm.max_spare_servers = 3
pm.max_requests = 500
php_admin_value[allow_url_fopen] = On
php_admin_value[allow_url_include] = On
php_admin_value[memory_limit] = 512M
php_admin_flag[output_buffering] = Off
php_admin_value[max_execution_time] = 1800
php_admin_value[max_input_time] = 3600
php_admin_value[post_max_size] = 10M
php_admin_value[upload_max_filesize] = 10M
php_admin_value[upload_tmp_dir] = /usr/share/postfixadmin/tmp
php_admin_value[max_file_uploads] = 100
php_admin_flag[session.cookie_secure] = True
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
php_admin_value[open_basedir] = /usr/share/postfixadmin/
After running the site for a while, feel free to come back and tune the settings according to the instructions in the earlier PHP-FPM guide
Now check the Apache2 and Nginx configs:
apache2ctl configtest
nginx -t
And restart the services:
service php7.4-fpm restart && service apache2 restart && service nginx reload
Creating the Apparmor Changehat¶
edit the php-fpm apparmor profile:
nano /etc/apparmor.d/usr.sbin.php-fpm7.4
Add the following changehat:
^postfixadmin flags=(attach_disconnected, complain) {
#include <abstractions/base>
#include <abstractions/dovecot-common>
#include <abstractions/mysql>
#include <abstractions/nameservice>
#include <abstractions/openssl>
#include <abstractions/php>
#include <abstractions/ssl_certs>
signal receive peer=php-fpm7.4,
deny /var/www/** r,
/etc/dovecot/conf.d/ r,
/etc/dovecot/conf.d/10-auth.conf r,
/etc/dovecot/conf.d/10-director.conf r,
/etc/dovecot/conf.d/10-logging.conf r,
/etc/dovecot/conf.d/10-mail.conf r,
/etc/dovecot/conf.d/10-master.conf r,
/etc/dovecot/conf.d/10-ssl.conf r,
/etc/dovecot/conf.d/10-tcpwrapper.conf r,
/etc/dovecot/conf.d/15-lda.conf r,
/etc/dovecot/conf.d/15-mailboxes.conf r,
/etc/dovecot/conf.d/20-imap.conf r,
/etc/dovecot/conf.d/20-lmtp.conf r,
/etc/dovecot/conf.d/20-managesieve.conf r,
/etc/dovecot/conf.d/90-acl.conf r,
/etc/dovecot/conf.d/90-plugin.conf r,
/etc/dovecot/conf.d/90-quota.conf r,
/etc/dovecot/conf.d/90-sieve-extprograms.conf r,
/etc/dovecot/conf.d/90-sieve.conf r,
/etc/dovecot/conf.d/auth-sql.conf.ext r,
/etc/dovecot/conf.d/auth-system.conf.ext r,
/etc/dovecot/dovecot.conf r,
/run/php/php7.4-fpm-www-postfixadmin.sock rw,
/usr/bin/dash mrix,
/usr/bin/doveadm mrix,
/usr/bin/doveconf mrix,
/usr/share/dovecot/protocols.d/ r,
/usr/share/dovecot/protocols.d/imapd.protocol r,
/usr/share/dovecot/protocols.d/lmtpd.protocol r,
/usr/share/dovecot/protocols.d/managesieved.protocol r,
owner /usr/share/postfixadmin/** rw,
}
Create an apache2 changehat for postfixadmin:
nano /etc/apparmor.d/apache2.d/postfixadmin-a2
^postfixadmin-a2 flags=(attach_disconnected) {
#include <abstractions/apache2-common>
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/php>
capability setuid,
capability setgid,
# for log writing (could be abstracted)
/var/log/apache2/access.log w,
/var/log/apache2/error.log w,
# Socket access
/run/php/php7.4-fpm-www-postfixadmin.sock wr,
# Access to standard PostfixAdmin files
/usr/share/postfixadmin/public/ r,
/usr/share/postfixadmin/public/** r,
# Deny access to these locations
deny /var/www/** r,
# Deny access to Bash
deny /bin/bash r,
}
Reload apparmor:
service apparmor reload
If you encounter problems with the next step, you may want to put the php-fpm profile into complain mode with aa-complain usr.sbin.php-fpm7.4
followed a little later with aa-logprof
to update the profile. Once happy with it, you can enforce again with aa-enforce usr.sbin.php-fpm7.4
Final steps¶
Browse to the setup page in a browser at https://pfa2.example.com/setup.php. The Postfix Admin Setup Checker should pass all the tests.
Enter a new password and select 'Generate Password Hash'.
Follow the instructions and add the password hash to /usr/share/postfixadmin/config.local.php
$CONF['setup_password'] = 'password_hash'
proceed to create a superadmin account and you should then be free to log in with the new credentials.
Configuring Postfix¶
First add MySQL support to Postfix:
apt install postfix-mysql
Edit the main Postfix configuration file:
nano /etc/postfix/main.cf
and add the following lines at the end:
virtual_mailbox_domains = proxy:mysql:/etc/postfix/sql/mysql_virtual_domains_maps.cf
virtual_mailbox_maps =
proxy:mysql:/etc/postfix/sql/mysql_virtual_mailbox_maps.cf,
proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_domain_mailbox_maps.cf
virtual_alias_maps =
proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_maps.cf,
proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_domain_maps.cf,
proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_domain_catchall_maps.cf
virtual_transport = lmtp:unix:private/dovecot-lmtp
Next we create the cf files one by one. First we create the sql directory to store the config files in:
mkdir /etc/postfix/sql/
Create the files as follows:
nano /etc/postfix/sql/mysql_virtual_domains_maps.cf
user = postfix
#Replace password with the postfixadmin password you set earlier
password = password
hosts = localhost
dbname = postfix
query = SELECT domain FROM domain WHERE domain='%s' AND active = '1'
#query = SELECT domain FROM domain WHERE domain='%s'
#optional query to use when relaying for backup MX
#query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '0' AND active = '1'
#expansion_limit = 100
nano /etc/postfix/sql/mysql_virtual_mailbox_maps.cf
user = postfix
#Replace password with the postfixadmin password you set earlier
password = password
hosts = localhost
dbname = postfix
query = SELECT maildir FROM mailbox WHERE username='%s' AND active = '1'
#expansion_limit = 100
nano /etc/postfix/sql/mysql_virtual_alias_domain_mailbox_maps.cf
user = postfix
#Replace password with the postfixadmin password you set earlier
password = password
hosts = localhost
dbname = postfix
query = SELECT maildir FROM mailbox,alias_domain WHERE alias_domain.alias_domain = '%d' and mailbox.username = CONCAT('%u', '@', alias_domain.target_domain) AND mailbox.active = 1 AND alias_domain.active='1'
nano /etc/postfix/sql/mysql_virtual_alias_maps.cf
user = postfix
#Replace password with the postfixadmin password you set earlier
password = password
hosts = localhost
dbname = postfix
query = SELECT goto FROM alias WHERE address='%s' AND active = '1'
#expansion_limit = 100
nano /etc/postfix/sql/mysql_virtual_alias_domain_maps.cf
user = postfix
#Replace password with the postfixadmin password you set earlier
password = password
hosts = localhost
dbname = postfix
query = SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' and alias.address = CONCAT('%u', '@', alias_domain.target_domain) AND alias.active = 1 AND alias_domain.active='1'
nano /etc/postfix/sql/mysql_virtual_alias_domain_catchall_maps.cf
# handles catch-all settings of target-domain
user = postfix
#Replace password with the postfixadmin password you set earlier
password = password
hosts = localhost
dbname = postfix
query = SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' and alias.address = CONCAT('@', alias_domain.target_domain) AND alias.active = 1 AND alias_domain.active='1'
Since the database passwords are stored in plain text so they should be readable only by user postfix and root, which is done by executing the following two commands.
chmod 0640 /etc/postfix/sql/*
setfacl -R -m u:postfix:rx /etc/postfix/sql/
Remove the apex domain name from the mydestination variable:
postconf -e "mydestination = \$myhostname, localhost.\$mydomain, localhost"
Edit the main Postfix configuration file:
nano /etc/postfix/main.cf
and add the following to the end of the file:
virtual_mailbox_base = /var/vmail
virtual_minimum_uid = 2000
virtual_uid_maps = static:2000
virtual_gid_maps = static:2000
Restart Postfix:
systemctl restart postfix
Create a user named vmail with ID 2000 and a group with ID 2000:
adduser vmail --system --group --uid 2000 --disabled-login --no-create-home
Create the mail base location:
mkdir /var/vmail/
Make vmail the owner:
chown vmail:vmail /var/vmail/ -R
Configuring Dovecot¶
Install the mysql dovercot plugin:
apt install dovecot-mysql
Edit the 10-mail.conf file:
nano /etc/dovecot/conf.d/10-mail.conf
Change or add the following lines to change the mail location:
mail_location = maildir:/var/vmail/%d/%n
mail_home = /var/vmail/%d/%n
Now edit the 10-auth.conf file:
nano /etc/dovecot/conf.d/10-auth.conf
Change or uncomment the following lines:
auth_username_format = %u
!include auth-sql.conf.ext
#Comment the below line if you don’t want local Unix users to send emails
#without registering email addresses in PostfixAdmin
#!include auth-system.conf.ext
#Uncomment the below lines for extra login debug info in log
#auth_debug = yes
#auth_debug_passwords = yes
Edit the dovecot-sql.conf.ext file (replace 'password' with the password you set for postfixadmin):
nano /etc/dovecot/dovecot-sql.conf.ext
driver = mysql
connect = host=localhost dbname=postfix user=postfix password=password
default_pass_scheme = ARGON2I
password_query = SELECT username AS user,password FROM mailbox WHERE username = '%u' AND active='1'
user_query = SELECT CONCAT('/var/vmail/',maildir) AS home, CONCAT('maildir:/var/vmail/',maildir) AS mail, 2000 AS uid, 2000 AS gid, CONCAT('*:bytes=',quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active='1'
iterate_query = SELECT username AS user FROM mailbox
Edit the /etc/dovecot/conf.d/10-master.conf file, find the service service auth section and replace it with the following lines:
nano /etc/dovecot/conf.d/10-master.conf
service auth {
unix_listener auth-userdb {
mode = 0600
user = vmail
}
# Postfix smtp-auth
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
# Auth process is run as this user.
user = dovecot
}
Then change the service auth-worker section to the following:
service auth-worker {
user = vmail
}
Enable the Dovecot service to start on server boot, and restart Dovecot so that all of these new configuration files are in effect:
systemctl enable dovecot
systemctl restart dovecot
Setting up Quota support¶
Open the conf.d/10-master.conf file, and modify it as follows:
nano /etc/dovecot/conf.d/10-master.conf
...
service dict {
unix_listener dict {
mode = 0660
user = vmail
group = vmail
}
}
...
Next, edit the conf.d/10-mail.conf file and edit the following variables:
nano /etc/dovecot/conf.d/10-mail.conf
...
mail_plugins = $mail_plugins quota
...
Open the conf.d/20-imap.conf file and activate the imap_quota plugin:
nano /etc/dovecot/conf.d/20-imap.conf
...
protocol imap {
...
mail_plugins = $mail_plugins imap_sieve imap_quota
...
}
...
Edit the 15-lda.conf file:
nano /etc/dovecot/conf.d/15-lda.conf
...
mail_plugins = $mail_plugins quota
...
Edit the 90-quota.conf file:
nano /etc/dovecot/conf.d/90-quota.conf
plugin {
quota = dict:User quota::proxy::sqlquota
quota_rule = *:storage=5GB
quota_rule2 = Trash:storage=+100M
quota_grace = 10%%
quota_exceeded_message = Quota exceeded, please contact your system administrator.
quota_warning = storage=100%% quota-warning 100 %u
quota_warning2 = storage=95%% quota-warning 95 %u
quota_warning3 = storage=90%% quota-warning 90 %u
quota_warning4 = storage=85%% quota-warning 85 %u
}
service quota-warning {
executable = script /usr/local/bin/quota-warning.sh
user = vmail
unix_listener quota-warning {
group = vmail
mode = 0660
user = vmail
}
}
dict {
sqlquota = mysql:/etc/dovecot/dovecot-dict-sql.conf.ext
}
We also need to tell dovecot how to access the quota SQL dictionary. Open the dovecot-dict-sql.conf.ext file and edit the following lines:
nano /etc/dovecot/dovecot-dict-sql.conf.ext
...
connect = host=127.0.0.1 dbname=postfixadmin user=postfixadmin password=password
...
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
# }
# }
...
Make sure to replace 'password' with the correct password.
Create the following shell script which will send an email to the user if its quota exceeds a specified limit:
nano /usr/local/bin/quota-warning.sh
#!/bin/sh
PERCENT=$1
USER=$2
cat << EOF | /usr/lib/dovecot/dovecot-lda -d $USER -o "plugin/quota=dict:User quota::noenforcing:proxy::sqlquota"
From: postmaster@example.com
Subject: Quota warning
Your mailbox is now $PERCENT% full.
EOF
Make the script executable by running the following chmod command:
chmod +x /usr/local/bin/quota-warning.sh
Finally, restart the Dovecot service:
service dovecot restart