Setup Email Server From Scratch On FreeBSD - 05 PostfixAdmin
04 Dovecot IMAP <- Intro -> 06 SPF DMARC And DKIM
This tutorial is partially complete 2025-05-14
Postfix, Dovecot, & PostfixAdmin work with MySQL and virtual accounts
can be created with PostfixAdmin and used with an email client. Incoming SPF
milter and outgoing OpenDKIM milter signing tested and working.
#################
# Postfix Admin #
#################
I had a lot of problems installing postfixadmin. I'll list some of them here.
1. Could not find some php files in postfixadmin (vendor?) with the git version
so compiled postfixadmin-php83 and it didn't work right either, so changed to
"pkg install postfixadmin33-php83", postfixadmin33-php82 also worked.
2. Some dependencies were missing, but the above pkg install and the packages
listed below in this document fixed those problems.
4. Connect to mysql failed. The socket path is different than default and
$CONF['database_port'] doesn't do what is expected. To fix this use the correct
socket path or configure the INET Port as shown below. I am not sure
$CONF['database_port'] = '3306' does anything unless using postgressql but I
included it just in case. The unix socket will be faster and use less resources.
# Unix Socket
$CONF['database_host'] = 'localhost';
$CONF['database_socket'] = '/var/run/mysql/mysql.sock';
$CONF['database_port'] = '';
# INET Port
$CONF['database_host'] = 'localhost:3306';
$CONF['database_socket'] = '';
$CONF['database_port'] = '3306';
3. I had mistyped the mysql password in config.inc.php.
5. I didn't have ARGGON2I in first version of dovecot I compiled, so I went back
and enabled this, and doveadm showed it as enabled. Then later compiled the
dovecot-fts-flatcurve version and forgot to enable the LIBSODIUM/ARGON2I, and my
setup didn't work, kept getting a 'doveadm pw -r 5' 'your password will not
work' type error on the postfixadmin setup page. Switched everything to php82
then found the problem using doveadm -l and testing with doveadm pw -s ARGON2I
-r 5. After recompiling dovecot-fts-flatcurve with LIBSODIUM/ARGON2I it worked.
6. Then I changed the php82 version back to php83, and it still worked.
# Install php
pkg install php83 mod_php83 php83-bcmath php83-bz2 php83-curl php83-gd php83-gmp php83-imap php83-intl
pkg install php83-ldap php83-mbstring php83-mysqli php83-pdo php83-pdo_pgsql php83-pdo_sqlite php83-pecl-imagick
pkg install php83-pecl-redis php83-pgsql php83-session php83-tokenizer php83-xml php83-zip php83-zlib php83-pdo_mysql
# Install postfixadmin
pkg install postfixadmin33-php83
# It is probably a good idea to leave postfixadmin in the default location to
# simplify upgrades.
# Change to postfixadmin33 installation directory and follow the postfixadmin
# installation instructions ...
cd /usr/local/www/postfixadmin33
mkdir /usr/local/www/postfixadmin33/templates_c
chown -R www:www /usr/local/www/postfixadmin33/templates_c
chmod -R 770 /usr/local/www/postfixadmin33/templates_c
chown -R root:www /usr/local/etc/letsencrypt/live
find /usr/local/etc/letsencrypt/live -type d -exec chmod 750 {} \;
find /usr/local/etc/letsencrypt/live -type f -exec chmod 640 {} \;
chown -R root:www /usr/local/etc/letsencrypt/archive
find /usr/local/etc/letsencrypt/archive -type d -exec chmod 750 {} \;
find /usr/local/etc/letsencrypt/archive -type f -exec chmod 640 {} \;
# Create the postfixadmin database
drop database postfixadmin;
create database postfixadmin;
create user 'postfixadmin'@'localhost' identified by 'super_secret_password';
grant all privileges on postfixadmin.* to 'postfixadmin'@'localhost';
flush privileges;
# To reset the postfixadmin password
alter user 'postfixadmin'@'localhost' identified by 'new_password';
flush privileges;
# Setup statistics in dovecot - add this to the end of the file
nano /usr/local/etc/dovecot/conf.d/10-master.conf
# ---
service stats {
unix_listener stats-reader {
user = www
group = www
mode = 0660
}
unix_listener stats-writer {
user = www
group = www
mode = 0660
}
}
# ---
# add www to group dovecot
pw group mod dovecot -m www
service dovecot restart
chown www:www /var/run/dovecot/stats-reader /var/run/dovecot/stats-writer
chmod 660 /var/run/dovecot/stats-reader /var/run/dovecot/stats-writer
# Add the Directory to the Virtual Host Secure it only allowed ip's or users can
# access. Check apache docs on how to setup user authentication if you wish.
# This doesn't provide total security but reduces the attack surface. Login is
# still protected by https encrypted username and password. It is also a good
# idea to configure this with a different alias or as a different virtual host
# separate from the website and roundcube login path.
nano /usr/local/etc/apache24/10-mx.okbsd-le-ssl.conf
# ---
Alias /postfixadmin /usr/local/www/postfixadmin33/public
<Directory /usr/local/www/postfixadmin33/>
Options FollowSymLinks MultiViews
AllowOverride All
#Order allow,deny
#allow from all
Require all denied
Require ip trusted_network/24 trusted_ip1 trusted_ip2 trusted_ipv6
</Directory>
# ---
apachectl restart
# Configure postfixadmin
nano /usr/local/etc/www/postfixadmin33/config.local.php
# ---
<?php
$CONF['database_type'] = 'mysqli';
$CONF['database_host'] = 'localhost';
$CONF['database_user'] = 'postfixadmin';
$CONF['database_password'] = 'super_secret_password';
$CONF['database_name'] = 'postfixadmin';
// $CONF['encrypt'] = 'dovecot:SHA512';
$CONF['encrypt'] = 'dovecot:ARGON2I';
$CONF['dovecotpw'] = "/usr/local/bin/doveadm pw -r 5";
$CONF['database_socket'] = '/var/run/mysql/mysql.sock';
$CONF['configured'] = true;
?>
# ---
# Go the the setup page and generate a setup password hash
https://mx.okbsd.com/postfixadmin/setup.php
# Enter a setup password and submit, then copy the hash and paste into config.local.php
nano /usr/local/etc/www/postfixadmin33/config.local.php
# ---
<?php
$CONF['database_type'] = 'mysqli';
$CONF['database_host'] = 'localhost';
$CONF['database_user'] = 'postfixadmin';
$CONF['database_password'] = 'super_secret_password';
$CONF['database_name'] = 'postfixadmin';
// $CONF['encrypt'] = 'dovecot:SHA512';
$CONF['encrypt'] = 'dovecot:ARGON2I';
$CONF['dovecotpw'] = "/usr/local/bin/doveadm pw -r 5";
$CONF['database_socket'] = '/var/run/mysql/mysql.sock';
$CONF['configured'] = true;
$CONF['setup_password'] = '$2y$10$fds_SAMPLE_HASH_ONLY_ddlfdfudedEFDFGdsnjalksd';
?>
# ---
# Reload the setup page
https://mx.okbsd.com/postfixadmin/setup.php
# Generate Administrator accounts.
admin@okbsd.com
<password>
<password>
# Add admin@okbsd.com alias to aliases, redirect to user@okbsd.com for now...
nano /etc/mail/aliases
# ---
admin: user
# ---
newaliases
# Login
https://mx.okbsd.com/postfixadmin
admin@okbsd.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 = \$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
# ---
service postfix restart
# check if vmail user and group have already been added
grep vmail /etc/group /etc/passwd
/etc/group:vmail:*:2000:
/etc/passwd:vmail:*:2000:2000::/nonexistent:/usr/sbin/nologin
pw group add vmail -g 2000
pwadduser
user: vmail
uid: 2000
homedir: /nonexistent
shell: nologin
lockout after creation: yes
chsh vmail
- remove the full name if you wish
mkdir /var/vmail/
chown -R vmail:vmail /var/vmail
# Configure Dovecot MySQL
cd /etc
ln -s /usr/local/etc/dovecot
# Change the following
nano /etc/dovecot/conf.d/10-mail.conf
# ---
mail_location = maildir:~/Maildir
mail_home = /var/vmail/%d/%n
# ---
# Change the following
nano /etc/dovecot/conf.d/10-auth.conf
# ---
auth_username_format = %u
auth_default_realm = okbsd.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
# ---
service dovecot restart
# At this point Thunderbird user@okbsd.com will get disconnected because dovecot
# will be using the new virtual mailbox database and user@okbsd.com doesn't exist
# there.
# Add domain and mailboxes
Add Domain
Domain: okbsd.com
Description: my first mail domain
Aliases: 0
Mailboxes: 0
Active: x
Add default mail aliaes: x
Pass expires: 3650
# You may choose to add user@okbsd.com back again, but it is good to add an
# admin email address for the default aliases, including postmaster@okbsd.com.
Add Mail Account admin@okbsd.com
Username: admin
****
****
Name: OkBSD Admin
Quota: 0
Active: x
Send Welcome email:
Add Mailbox
# Virtual List -> Show All - fix the aliases
abuse@okbsd.com admin@okbsd.com
** more ** admin@okbsd.com
** more ** admin@okbsd.com
** more ** admin@okbsd.com
# Add New Alias
root@okbsd.com admin@okbsd.com
# Add admin@okbsd.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
cd /usr/local/etc
tar cfzv dovecot_backup.tgz dovecot
tar cfzv apache24_backup.tgz apache24
cd /usr/local/www
tar cfzv postfixadmin_backup.tgz postfixadmin33
cd /var/www
tar cfzv okbsd_backup.tgz okbsd
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>