Setup Email Server From Scratch On FreeBSD #2 - 05 PostfixAdmin

04 Dovecot IMAP <- Intro -> 06 SPF DKIM DMARC

We believe in data independence, and support others who want data independence.
Debian Email From Scratch version 2 finished 2025-07-30.

We are still adding to it but it all works!



#################
# Postfix Admin #
#################

PostfixAdmin requires a database but we installed this already on page 02 where
we installed LAMP, Linux Apache MySQL PHP.

Install PostfixAdmin

apt install postfixadmin
Use db-common config and enter a <supersecret> password, twice.

Don't forget your <supersecret> postfixadmin password.

nano /etc/apache2/sites-available/ssl-mx.okdeb.conf
Alias /admin-login /usr/share/postfixadmin/public <Directory /usr/share/postfixadmin/> Options FollowSymLinks MultiViews AllowOverride All Require all denied Require ip trusted_network/24 trusted_ip1 trusted_ip2 trusted_ipv6 </Directory>

apachectl restart

Setup access control

apt install acl
setfacl -R -m u:www-data:rwx /usr/share/postfixadmin/templates_c/
setfacl -R -m u:www-data:rx /etc/letsencrypt/live/ /etc/letsencrypt/archive/

Configure PostfixAdmin

Use one of these configs, not both ...

more secure and efficient
$CONF['database_host'] = 'localhost';
$CONF['database_socket'] = '/run/mysqld/mysqld.sock';

less secure less efficient
$CONF['database_host'] = 'localhost:3306';
$CONF['database_socket'] = '';

nano /usr/share/postfixadmin/config.local.php
<?php $CONF['configured'] = true; $CONF['database_type'] = 'mysqli'; // $CONF['database_host'] = 'localhost:3306'; // $CONF['database_socket'] = ''; $CONF['database_host'] = 'localhost'; $CONF['database_socket'] = '/run/mysqld/mysqld.sock'; $CONF['database_port'] = '3306'; $CONF['database_user'] = 'postfixadmin'; $CONF['database_password'] = 'supersecret'; $CONF['database_name'] = 'postfixadmin'; $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 } //$CONF['setup_password'] = ''; $CONF['base_url'] = '/admin-login/'; $CONF['quota'] = 'YES'; $CONF['used_quotas'] = 'YES'; //allow maxmium 100 mailboxes for each domain $CONF['mailboxes'] = '100'; //default 10240MB quota for each user $CONF['maxquota'] = '10240'; //default 102400MB quota for each domain $CONF['domain_quota_default'] = '102400'; ?>

Configure stats-reader and stats-writer sockets.

nano /etc/dovecot/conf.d/10-master.conf
service stats { unix_listener stats-reader { user = www-data group = www-data mode = 0660 } unix_listener stats-writer { user = www-data group = www-data mode = 0660 } }

add www to group dovecot
adduser www-data dovecot
systemctl restart postfix dovecot

root@okdeb.com:/usr/share/postfixadmin# ls -ld /var/run/dovecot/stats-*
srw-rw---- 1 www-data www-data 0 Jul 16 15:18 /var/run/dovecot/stats-reader
srw-rw---- 1 www-data www-data 0 Jul 16 15:18 /var/run/dovecot/stats-writer

If permissions are not correct use ...
chown www-data:www-data /var/run/dovecot/stats-reader /var/run/dovecot/stats-writer
chmod 660 /var/run/dovecot/stats-reader /var/run/dovecot/stats-writer


Navigate to the setup web address.

https://mx.okdeb.com/admin-login/setup.php

Enter a password and generate setup password hash and paste into config.local.conf.

nano /usr/share/postfixadmin/config.local.php
<?php $CONF['configured'] = true; $CONF['database_type'] = 'mysqli'; $CONF['database_host'] = 'localhost'; $CONF['database_socket'] = '/run/mysqld/mysqld.sock'; $CONF['database_port'] = '3306'; $CONF['database_user'] = 'postfixadmin'; $CONF['database_password'] = 'supersecret'; $CONF['database_name'] = 'postfixadmin'; $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://> $CONF['dovecotpw'] = "/usr/bin/doveadm pw -r 5"; # debian } $CONF['setup_password'] = '$2y$10$fds_SAMPLE_HASH_ONLY_ddlfdfudedEFDFGdsnjalksd'; $CONF['base_url'] = '/admin-login/'; $CONF['quota'] = 'YES'; $CONF['used_quotas'] = 'YES'; //allow maxmium 100 mailboxes for each domain $CONF['mailboxes'] = '100'; //default 10240MB quota for each user $CONF['maxquota'] = '10240'; //default 102400MB quota for each domain $CONF['domain_quota_default'] = '102400'; ?>

Login with setup the password on the same page. Create an super admin account
using a valid email address like adminuser@okdeb.com. A unix account isn't
needed but to add one use ...

Reload the setup page

https://mx.okdeb.com/admin-login/setup.php

Generate Administrator accounts.
postmaster@okdeb.com
<password>
<password>

Add postmaster@okdeb.com alias to aliases, redirect to user@okdeb.com for now...
nano /etc/mail/aliases
example_admin: user
newaliases

Login
https://mx.okdeb.com/postfixadmin
postmaster@okdeb.com
<password>

Configure Postfix to use MariaDB

Add to end of file main.cf
nano /etc/postfix/main.cf
mailbox_transport = lmtp:unix:private/dovecot-lmtp smtputf8_enable = no 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

mkdir /etc/postfix/sql/
nano /etc/postfix/sql/mysql_virtual_domains_maps.cf
user = postfixadmin password = password hosts = localhost dbname = postfixadmin 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 = postfixadmin password = password hosts = localhost dbname = postfixadmin 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 = postfixadmin password = password hosts = localhost dbname = postfixadmin 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 = postfixadmin password = password hosts = localhost dbname = postfixadmin 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 = postfixadmin password = password hosts = localhost dbname = postfixadmin 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 = postfixadmin password = password hosts = localhost dbname = postfixadmin 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'

chown -R root:postfix /etc/postfix/sql
chmod 0640 /etc/postfix/sql/*
chmod 0750 /etc/postfix/sql

Change postfix destination
postconf mydestination
mydestination = $mydomain, $myhostname, localhost.$mydomain, localhost

postconf -e "mydestination = okdeb.com, \$myhostname, localhost.\$mydomain, localhost"


Add to end of file
nano /etc/postfix/main.cf
virtual_mailbox_base = /var/vmail virtual_minimum_uid = 2000 virtual_uid_maps = static:2000 virtual_gid_maps = static:2000

adduser vmail --system --group --uid 2000 --disabled-login --no-create-home

mkdir /var/vmail/
chown -R vmail:vmail /var/vmail

systemctl restart postfix dovecot



Configure Dovecot MySQL
Make sure you have dovecot-mysql package installed

apt install dovecot-mysql dovecot-lmtpd

Change the following
nano /etc/dovecot/conf.d/10-mail.conf
mail_location = maildir:~/Maildir mail_home = /var/vmail/%d/%n

Change the following, for mysql authentication use auth_username_format = %u

nano /etc/dovecot/conf.d/10-auth.conf
auth_username_format = %u auth_default_realm = okdeb.com #!include auth-system.conf.ext !include auth-sql.conf.ext auth_debug = yes auth_debug_passwords = yes

Change the following
nano /etc/dovecot/dovecot-sql.conf.ext
driver = mysql connect = host=localhost dbname=postfixadmin user=postfixadmin password=super_secret_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, 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

nano /etc/dovecot/20-lmtp.conf
protocol lmtp { # Space separated list of plugins to load (default is global mail_plugins). mail_plugins = $mail_plugins }

nano /etc/dovecot/20-imap.conf
protocol imap { # Space separated list of plugins to load (default is global mail_plugins). mail_plugins = $mail_plugins # Maximum number of IMAP connections allowed for a user from each IP address. # NOTE: The username is compared case-sensitively. #mail_max_userip_connections = 10 }

systemctl restart postfix dovecot

If you have trouble restarting and get an error like module not found, unknown,
not loaded make sure dovecot-mysql is installed with apt get dovecot-mysql.

Thunderbird jack@okdeb.com will get disconnected because dovecot will be using
the new virtual mailbox database and jack@okdeb.com doesn't exist there.

Go to PostfixAdmin -> Domain List -> Add domain and mailboxes
Add Domain
Domain: okdeb.com
Description: my first mail domain
Aliases: 0
Mailboxes: 0
Mailbox Quota: 0
Domain Quota: 0
Active: x
Add default mail aliaes: x
Pass expires: 3650

Go to Virtual List
You may choose to add jack@okdeb.com back again, but it is good to add an
admin email address for the default aliases, including postmaster@okdeb.com.

Add Mail Account jack@okdeb.com
Username: admin
****
****
Name: OkDeb Admin
Quota: 0
Active: x
Send Welcome email:
Add Mailbox

Virtual List -> Show All - fix the aliases - by RFC keep postmaster@okdeb.com at least
abuse@okdeb.com jack@okdeb.com
** more ** jack@okdeb.com
postmaster@okdeb.com jack@okdeb.com
** more ** jack@okdeb.com
Add New Alias
root@okdeb.com jack@okdeb.com

Add or repair jack@okdeb.com to Thunderbird and test send and recieve to the account
created entirely by PostfixAdmin

##############
# SUGGESTION #
##############

If everything works as expected it is a good idea to backup the configurations
now. We've done a lot of changes and made a lot of progress so let's make sure
if we have problems later we can roll back to the previous working configurations.

cd /etc
tar cfzv postfix_backup.tgz postfix
tar cfzv dovecot_backup.tgz dovecot
tar cfzv apache24_backup.tgz apache2

cd /usr/share
tar cfzv postfixadmin_backup.tgz postfixadmin

cd /var/www
tar cfzv okdeb_backup.tgz okdeb

If you have other users on your system make these tgz files unreadable and/or
move them to a safe directory.
chmod 600 <filename.tgz>



Next Up SPF DMARC and DKIM

04 Dovecot IMAP <- Intro -> 06 SPF DKIM DMARC