Postfix with Dovecot2 and Virtualdomain with Mysql


We use the following folder structure:

                 ^  ^  ^
                 |  |  |- Directory were emails are stored in maildir format
                 |  |- the username part of the email address
                 |- the domain
                       ^- Folder to store sieve filters
               ^- Hold configuration files for dovecot
                        ^- Directory for global sieve scripts for all users
               ^- Hold configuration files for postfix                        

We will use Postfix, Dovecot2 with virtual domains managed by ViMbAdmin and everything stored on a mysql database. As password scheme BLF-CRYPT is used, see

Base System

I will start from a plain installation. Make sure your system is up to date:

portsnap fetch
portsnap update
freebsd-update fetch
freebsd-update install

Install MYSQL

cd /usr/ports/databases/mysql56-server/
make install clean
echo 'mysql_enable="YES"' >> /etc/rc.conf
service mysql-server start

Install amavisd-new

cd /usr/ports/security/amavisd-new
make install clean
sysrc amavisd_enable="YES"

Install clamav and clamsmtp

pkg install clamsmtp
pkg install clamav
sysrc clamsmtpd_enable="YES"
sysrc clamav_clamd_enable="YES"
sysrc clamav_freshclam_enable="YES"

Install mailman

cd /usr/ports/mail/mailman/
make install clean

Install Apache/PHP

cd /usr/ports/databases/db6/
make install clean
cd /usr/ports/www/apache24/
make install clean
sysrc apache24_enable="YES"
sysrc apache24ssl_enable="YES"
sysrc apache24_http_accept_enable="YES"
cd /usr/ports/lang/php5-extensions
make install clean
cd /usr/ports/databases/pecl-memcached
make install clean
cp /usr/local/etc/php.ini-production /usr/local/etc/php.ini
cd /usr/ports/devel/php-composer
make install clean

Make sure correct timezone is set in php.ini:

date.timezone = "Europe/Berlin"

Install Dovecot

cd /usr/ports/mail/dovecot2
#(select MYSQL)
make install clean
sysrc dovecot_enable="YES"

Copy standard config files:

cp -a /usr/local/etc/dovecot/example-config/ /usr/local/etc/dovecot/
cd /usr/ports/mail/dovecot2-pigeonhole/
make install clean

Install Postfix

cd /usr/ports/mail/postfix
make install clean
sysrc sendmail_enable="NO"
sysrc sendmail_submit_enable="NO"
sysrc sendmail_outbound_enable="NO"
sysrc sendmail_msp_queue_enable="NO"
sysrc postfix_enable="YES"
sysrc -f /etc/periodic.conf daily_clean_hoststat_enable="NO"
sysrc -f /etc/periodic.conf daily_status_mail_rejects_enable="NO"
sysrc -f /etc/periodic.conf daily_status_include_submit_mailq="NO"
sysrc -f /etc/periodic.conf daily_submit_queuerun="NO"

Installing Postfix SPF

cd /usr/ports/mail/postfix-policyd-spf-perl
make install clean

Install ViMbAdmin

Create several accounts in the mysql database, we give the users only the rights they require, e.g. for dovecot and postfix user select permissions are enough. The account vimbadmin needs more rights to edit data (make sure the replace password! pwgen -s 20 is be a good start). Make sure that every user has an own password!

mysql -u root -p
create database vimbadmin;
grant all privileges on vimbadmin.* to 'vimbadmin'@'localhost' identified by 'password';
grant select on vimbadmin.* to 'dovecot'@'localhost' identified by 'password';
grant select on vimbadmin.* to 'postfix'@'localhost' identified by 'password';

Install ViMbAdmin (follow the instruction

mkdir -p /usr/local/www
cd /usr/local/www
git clone
# git clone
cd ViMbAdmin
composer install --dev
chown -R www var/
cd public
cp .htaccess.dist .htaccess
cd ..

Make sure you change the following options (replace values with correct values for your domains):

resources.doctrine2.connection.options.password = 'password'
defaults.mailbox.uid = 5000
defaults.mailbox.gid = 5000
defaults.mailbox.maildir = "maildir:/usr/local/vmail/%d/%u/mail:LAYOUT=fs"
defaults.mailbox.homedir = "/usr/local/vmail/%d/%u"
defaults.mailbox.min_password_length = 20
defaults.mailbox.password_scheme = "dovecot:BLF-CRYPT"
defaults.mailbox.dovecot_pw_binary = "/usr/local/bin/doveadm pw"    = "smtp-host-name"  = "pop3-hostname"  = "imap-hostname"  = "https://webmail-hostname"
identity.orgname  = "Lostinspace"  = "Lostinspace Support Team" = "admins@hostname"  = "ViMbAdmin Autobot" = "autobot@hostname"   = "ViMbAdmin Autobot"  = "do-not-reply@hostname"
identity.siteurl = "https://link-to-vimbadmin-website/vimbadmin/" = "ViMbAdmin Administrator" = ""

; If you have to authenticate on your mailserver to send email you want to set:
resources.mailer.smtphost = "localhost"
resources.mailer.username = "<user>"
resources.mailer.password = "<password>"
resources.mailer.auth     = "login"
resources.mailer.ssl      = "tls"
resources.mailer.port     = "587"
./bin/doctrine2-cli.php orm:schema-tool:create

Add the config section to the virtual host configuration file you want to add vimbadmin available:

Alias /vimbadmin "/usr/local/www/ViMbAdmin/public/"
<Directory "/usr/local/www/ViMbAdmin/public/">
    Options FollowSymLinks
    AllowOverride None
    Require all granted
    SetEnv APPLICATION_ENV production
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} -s [OR]
    RewriteCond %{REQUEST_FILENAME} -l [OR]
    RewriteCond %{REQUEST_FILENAME} -d
    RewriteRule ^.*$ - [NC,L]
    RewriteRule ^.*$ /vimbadmin/index.php [NC,L]

Now access the website:


and follow the instructions there.

Create user and group that store the emails:

pw groupadd vmail -g 5000
pw useradd vmail -u 5000 -g vmail -s /usr/sbin/nologin -d /nonexistent -c "Virtual Mail Owner"
mkdir -p /usr/local/vmail
chown vmail /usr/local/vmail
chgrp vmail /usr/local/vmail
chmod 770 /usr/local/vmail

Configure Dovecot

Create dh.pem:

dd if=/var/db/dovecot/ssl-parameters.dat bs=1 skip=88 | openssl dhparam -inform der > /usr/local/etc/dovecot/dh.pem

Now we configure dovecot, set the config files based on this diff.:

diff -ur /usr/local/share/doc/dovecot/example-config/conf.d/10-auth.conf ./conf.d/10-auth.conf
--- /usr/local/share/doc/dovecot/example-config/conf.d/10-auth.conf     2014-08-19 20:38:20.043506000 +0200
+++ ./conf.d/10-auth.conf       2014-08-19 20:06:07.528052364 +0200
@@ -119,7 +119,7 @@
 #!include auth-deny.conf.ext
 #!include auth-master.conf.ext
-!include auth-system.conf.ext
+#!include auth-system.conf.ext
 #!include auth-sql.conf.ext
 #!include auth-ldap.conf.ext
 #!include auth-passwdfile.conf.ext
diff -ur /usr/local/share/doc/dovecot/example-config/conf.d/10-ssl.conf ./conf.d/10-ssl.conf
--- /usr/local/share/doc/dovecot/example-config/conf.d/10-ssl.conf      2014-08-19 20:38:20.044506000 +0200
+++ ./conf.d/10-ssl.conf        2014-08-19 22:27:15.827087484 +0200
@@ -9,8 +9,8 @@
 # dropping root privileges, so keep the key file unreadable by anyone but
 # root. Included doc/ can be used to easily generate self-signed
 # certificate, just make sure to update the domains in dovecot-openssl.cnf
-ssl_cert = </etc/ssl/certs/dovecot.pem
-ssl_key = </etc/ssl/private/dovecot.pem
+#ssl_cert = </etc/ssl/certs/dovecot.pem
+#ssl_key = </etc/ssl/private/dovecot.pem
 # If key file is password protected, give the password here. Alternatively
 # give it when starting dovecot with -p parameter. Since this file is often
diff -ur /usr/local/share/doc/dovecot/example-config/dovecot-sql.conf.ext ./dovecot-sql.conf.ext
--- /usr/local/share/doc/dovecot/example-config/dovecot-sql.conf.ext    2014-08-19 20:38:20.064506000 +0200
+++ ./dovecot-sql.conf.ext      2014-08-19 22:33:01.703040984 +0200
@@ -29,7 +29,7 @@
 # );
 # Database driver: mysql, pgsql, sqlite
-#driver =
+driver = mysql
 # Database connection string. This is driver-specific setting.
@@ -68,14 +68,14 @@
 #   connect = dbname=virtual user=virtual password=blarg
 #   connect = /etc/dovecot/authdb.sqlite
-#connect =
+connect = host=localhost dbname=vimbadmin user=dovecot password=<password>
 # Default password scheme.
 # List of supported schemes is in
-#default_pass_scheme = MD5
+default_pass_scheme = BLF-CRYPT
 # passdb query to retrieve the password. It can return fields:
 #   password - The user's password. This field must be returned.
@@ -137,5 +137,12 @@
 #    home AS userdb_home, uid AS userdb_uid, gid AS userdb_gid \
 #  FROM users WHERE userid = '%u'
+password_query = SELECT username as user, password as password, \
+        homedir AS userdb_home, maildir AS userdb_mail, \
+        concat('*:bytes=', quota) as userdb_quota_rule, uid as userdb_uid, gid as userdb_gid \
+    FROM mailbox \
+        WHERE username = '%Lu' AND active = '1' \
+            AND ( access_restriction = 'ALL' OR LOCATE( '%Us', access_restriction ) > 0 )
+user_query = SELECT homedir AS home, maildir AS mail, \
+        concat('*:bytes=', quota) as quota_rule, uid, gid \
+    FROM mailbox WHERE username = '%u'
 # Query to get a list of all usernames.
- #iterate_query = SELECT username AS user FROM users
+ iterate_query = SELECT username AS user FROM mailbox

Now create a new config file that hold all settings:

service auth {
  unix_listener auth-userdb {
    mode = 0666
    user = vmail
    group = vmail
  # Postfix smtp-auth
  unix_listener /var/spool/postfix/private/auth {
    mode = 0666
    user = postfix
    group = postfix
  # Auth process is run as this user.
  #user = $default_internal_user
service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0660
    group = postfix
    user = postfix
  user = vmail
# ***** Configure location for mailbox
mail_location = maildir:/usr/local/vmail/%d/%u
# ***** Authenticate against sql database *****
auth_mechanisms = plain login
passdb {
  driver = sql
  args = /usr/local/etc/dovecot/dovecot-sql.conf.ext
userdb {
  driver = prefetch
userdb {
  driver = sql
  args = /usr/local/etc/dovecot/dovecot-sql.conf.ext
# ***** use uid and gid for vmail
mail_uid = 5000
mail_gid = 5000
mail_privileged_group = 5000
mail_access_groups = 5000
first_valid_uid = 5000
last_valid_uid = 5000
first_valid_gid = 5000
last_valid_gid = 5000
maildir_copy_with_hardlinks = yes
# ***** Modules we use *****
mail_plugins = $mail_plugins
# **** SSL config *****
ssl = yes
ssl_cert = </usr/local/etc/letsencrypt/live/
ssl_key = </usr/local/etc/letsencrypt/live/
ssl_require_crl = no
ssl_prefer_server_ciphers = yes
# **** Configure IMAP *****
protocol imap {
  # Space separated list of plugins to load (default is global mail_plugins).
  mail_plugins = $mail_plugins quota imap_quota
# ***** Configure POP3 *****
protocol pop3 {
  # Space separated list of plugins to load (default is global mail_plugins).
  mail_plugins = $mail_plugins quota
pop3_client_workarounds = outlook-no-nuls oe-ns-eoh
# ***** LDA Config *****
postmaster_address = postmaster@%d
hostname =
quota_full_tempfail = yes
recipient_delimiter = +
lda_mailbox_autocreate = yes
lda_mailbox_autosubscribe = yes
protocol lda {
  mail_plugins = $mail_plugins sieve quota
# ***** LMTP Config *****
protocol lmtp {
    postmaster_address = postmaster@%d
    mail_plugins = quota sieve
# ***** Plugin Configuration *****
plugin {
# autocreate plugin
# This plugin allows administrator to specify mailboxes that must always
# exist for all users. They can optionally also be subscribed. The
# mailboxes are created and subscribed always after user logs in.
# Namespaces are fully supported, so namespace prefixes need to be used
# where necessary.
  autocreate = Sent
  autocreate2 = Drafts
  autocreate3 = Junk
  autocreate4 = Trash
  #autocreate5 = ..etc..
  autosubscribe = Sent
  autosubscribe2 = Drafts
  autosubscribe3 = Junk
  autosubscribe4 = Trash
  #autosubscribe5 = ..etc
  sieve = ~/sieve/dovecot.sieve
  sieve_dir = ~/sieve
  sieve_extensions = +notify +imapflags +spamtest +spamtestplus +relational +comparator-i;ascii-numeric
  sieve_before = /usr/local/etc/dovecot/sieve/
# ***** Quota Configuration *****
  quota = maildir:User quota
# ***** Configure Sieve *****
protocols = $protocols sieve
service managesieve-login {
  inet_listener sieve {
    port = 4190
service managesieve {
protocol sieve {
## Mailbox definitions
# NOTE: Assumes "namespace inbox" has been defined in 10-mail.conf.
namespace inbox {
  #mailbox name {
    # auto=create will automatically create this mailbox.
    # auto=subscribe will both create and subscribe to the mailbox.
    #auto = no
    # Space separated list of IMAP SPECIAL-USE attributes as specified by
    # RFC 6154: \All \Archive \Drafts \Flagged \Junk \Sent \Trash
    #special_use =
  # These mailboxes are widely used and could perhaps be created automatically:
  mailbox Drafts {
    special_use = \Drafts
  mailbox Junk {
    special_use = \Junk
  mailbox Trash {
    special_use = \Trash
  # For \Sent mailboxes there are two widely used names. We'll mark both of
  # them as \Sent. User typically deletes one of them if duplicates are created.
  mailbox Sent {
    special_use = \Sent
  mailbox "Sent Messages" {
    special_use = \Sent
  # If you have a virtual "All messages" mailbox:
  #mailbox virtual/All {
  #  special_use = \All
  # If you have a virtual "Flagged" mailbox:
  #mailbox virtual/Flagged {
  #  special_use = \Flagged
# ***** Logging *****
auth_verbose = yes
auth_debug_passwords = yes
mail_debug = yes

To use the sieve plugin in Thunderbird use this here:

Configure Sieve

As we configured the folder /usr/local/etc/dovecot/sieve to hold standard scripts for all users:

mkdir -p /usr/local/etc/dovecot/sieve
chown vmail:vmail /usr/local/etc/dovecot/sieve

Now create a new file with content:

require ["fileinto"];
if anyof (header :contains "X-Spam-Flag" "YES")
 fileinto "Junk";
/* Other messages get filed into INBOX */

Compile all rules:

cd /usr/local/etc/dovecot/sieve
sievec .
chown vmail .
chown vmail *
chgrp vmail .
chgrp vmail *

Migrate mbox to Maildir

We have to migrate a mbox to Maildir. The inbox is on: /var/mail/<user>. The other folders are /usr/home/<user>/mail.

Make sure the dovecot user can read/write to the folders (make sure you remember the permission to undo it):

chgrp vmail /var/mail
chmod g+w /var/mail
chgrp vmail /var/mail/<user>
chgrp -R vmail /usr/home/<user>/mail

Now we convert it:

dsync -v -u <newdovecotuser> mirror mbox:/usr/home/<user>/mail/:INBOX=/var/mail/<user>

Restore permissions on the old folder/files or remove them if migration was successfully finished.

Configure Mailman

MTA = 'Postfix'
SMTPHOST = 'full-smtp-host-name-to-connect'

Create required files:

cd /usr/local/mailman
chown mailman:mailman data/aliases*
chmod g+w data/aliases*
chown mailman:mailman data/virtual-mailman*
chmod g+w data/virtual-mailman*

Configure clamav

Copy standard configuration files and modify them:

cp clamd.conf.sample clamd.conf
cp freshclam.conf.sample freshclam.conf
cp clamsmtpd.conf.sample clamsmtpd.conf
--- freshclam.conf.sample       2016-03-19 10:55:28.000000000 +0100
+++ freshclam.conf      2016-03-19 11:27:09.857817239 +0100
@@ -71,6 +71,7 @@
 # code. See for the full list.
 # You can use for IPv6 connections.
--- clamsmtpd.conf.sample       2016-04-02 04:13:28.000000000 +0200
+++ clamsmtpd.conf      2016-04-02 12:46:37.399587985 +0200
@@ -8,7 +8,7 @@
 # The address to send scanned mail to.
 # This option is required unless TransparentProxy is enabled
-OutAddress: 10026
+OutAddress: 10029
@@ -26,13 +26,13 @@
 #XClient: off
 # Address to listen on (defaults to all local addresses on port 10025)
 # The address clamd is listening on
-#ClamAddress: /var/run/clamav/clamd.sock
+ClamAddress: /var/run/clamav/clamd.sock
 # A header to add to all scanned email
-#Header: X-Virus-Scanned: ClamAV using ClamSMTP
+Header: X-Virus-Scanned: ClamAV using ClamSMTP
 # Directory for temporary files
 #TempDirectory: /tmp
@@ -47,7 +47,7 @@
 #TransparentProxy: off
 # User to switch to
-#User: clamav
+User: clamav
 # Virus actions: There's an option to run a script every time a virus is found.
 # !IMPORTANT! This can open a hole in your server's security big enough to drive

Configure Postfix

Create SSL related files:

cd /etc/mail/certs
cd /usr/local/etc/apache24/ssl_keys
openssl gendh -out dh_512.pem -2 512
openssl gendh -out dh_1024.pem -2 1024
openssl gendh -out dh_2048.pem -2 2048
openssl gendh -out dh_4096.pem -2 4096
# New way
openssl genpkey -genparam -algorithm DH -out dh_512.pem -pkeyopt dh_paramgen_prime_len:512
openssl genpkey -genparam -algorithm DH -out dh_1024.pem -pkeyopt dh_paramgen_prime_len:1024
openssl genpkey -genparam -algorithm DH -out dh_2048.pem -pkeyopt dh_paramgen_prime_len:2048
openssl genpkey -genparam -algorithm DH -out dh_4096.pem -pkeyopt dh_paramgen_prime_len:4096

Add the following lines to

# enable TLS
tls_append_default_CA = yes
smtpd_tls_received_header = yes
smtpd_tls_key_file = /usr/local/etc/apache24/ssl_keys/req.pem
smtpd_tls_cert_file = /usr/local/etc/apache24/ssl_keys/newcert.pem
#smtp_tls_CAfile= /etc/mail/certs/newcert.pem
#smtp_tls_CApath = /etc/mail/certs/
smtpd_tls_loglevel = 1
# enable smtp auth as Server
smtpd_sasl_auth_enable = yes
smtpd_recipient_restrictions =
        check_client_access hash:/usr/local/etc/postfix/client_checks,
        check_sender_access hash:/usr/local/etc/postfix/sender_checks,
        check_policy_service unix:private/spf-policy,
smtpd_helo_restrictions =
#       check_helo_access hash:/etc/postfix/ehlo_whitelist,
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
broken_sasl_auth_clients = yes
smtpd_helo_required = yes
strict_rfc821_envelopes = yes
disable_vrfy_command = yes
smtpd_delay_reject = yes
smtpd_sender_restrictions =
#       check_sender_access hash:/etc/postfix/sender_access,
smtpd_data_restrictions =
smtpd_client_restrictions =
#       check_client_access hash:/etc/postfix/client_access,
# enable ipv6 and ipv4
inet_protocols = all
# limit message size to 100MB
message_size_limit = 104857600
mailbox_size_limit = 512000000
# increase timeouts to prevent queue write file errors
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated      defer_unauth_destination
# Virtual Domain Configuration
virtual_alias_maps = mysql:/usr/local/etc/postfix/mysql/,
virtual_gid_maps = static:5000
virtual_mailbox_base = /usr/local/vmail
virtual_mailbox_domains = mysql:/usr/local/etc/postfix/mysql/
virtual_mailbox_maps = mysql:/usr/local/etc/postfix/mysql/
virtual_minimum_uid = 5000
virtual_uid_maps = static:5000
#dovecot_destination_recipient_limit = 1
virtual_transport = lmtp:unix:private/dovecot-lmtp
home_mailbox = Maildir/
smtpd_sasl_authenticated_header = yes
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = $myhostname
# Mailman
alias_maps = hash:/etc/mail/aliases,
spf-policy_time_limit = 3600
# optimize SSL configuration
smtpd_tls_security_level = may
smtpd_tls_mandatory_protocols = !SSLv2 !SSLv3
smtpd_tls_protocols = !SSLv2 !SSLv3
smtpd_tls_dh1024_param_file = /usr/local/etc/apache24/ssl_keys/dh_2048.pem
smtpd_tls_dh512_param_file = /usr/local/etc/apache24/ssl_keys/dh_512.pem
smtpd_tls_eecdh_grade = strong
tls_preempt_cipherlist = yes
smtpd_tls_loglevel = 1
smtp_dns_support_level = dnssec
smtp_tls_mandatory_protocols = !SSLv2 !SSLv3
smtp_tls_protocols = !SSLv2, !SSLv3
smtp_tls_mandatory_ciphers = high
smtp_tls_loglevel = 1

Edit to have this:

smtp      inet  n       -       n       -       -       smtpd
smtpd     pass  -       -       n       -       -       smtpd
        -o content_filter=smtp-amavis:[]:10024
submission inet n       -       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=$mua_client_restrictions
#  -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       -       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=$mua_client_restrictions
#  -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
# options for amavis
smtp-amavis             unix    -               -               n               -               2               lmtp
    -o lmtp_data_done_timeout=1200
    -o lmtp_send_xforward_command=yes
    -o disable_dns_lookups=yes
#  -o content_filter=lmtp:unix:/var/run/dspam.sock
# reinject mail from amavisd
[localhost]:10025 inet  n -       n       -       -        smtpd
  -o content_filter=clamav:[]:10028
  -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks
  -o smtpd_helo_restrictions=
  -o smtpd_client_restrictions=
  -o smtpd_sender_restrictions=
  -o smtpd_recipient_restrictions=permit_mynetworks,reject
  -o mynetworks=localhost
  -o smtpd_authorized_xforward_hosts=localhost
# SPF check
spf-policy      unix    -       n       n       -       0       spawn
  user=nobody argv=/usr/local/libexec/postfix-policyd-spf-perl
# AV scan filter (used by content_filter)
clamav     unix  -       -       n       -       16      smtp
    -o smtp_send_xforward_command=yes
    -o smtp_enforce_tls=no
    -o smtp_tls_security_level=none
# For injecting mail back into postfix from the clamav filter inet  n -       n       -       16      smtpd
    -o content_filter=
    -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks
    -o smtpd_helo_restrictions=
    -o smtpd_client_restrictions=
    -o smtpd_sender_restrictions=
    -o smtpd_recipient_restrictions=permit_mynetworks,reject
    -o mynetworks_style=host
    -o smtpd_authorized_xforward_hosts=localhost
    -o smtp_enforce_tls=no
    -o smtp_tls_security_level=none

Create SQL related configuration:

mkdir -p /usr/local/etc/postfix/mysql

Create the following files:

user = postfix
password = <password>
hosts =
dbname = vimbadmin
query = SELECT goto FROM alias WHERE address = '%s' AND active = '1'
user = postfix
password = <password>
hosts =
dbname = vimbadmin
query = SELECT domain FROM domain WHERE domain = '%s' AND backupmx = '0' AND active = '1'
user = postfix
password = <password>
hosts =
dbname = vimbadmin
table = mailbox
select_field = maildir
where_field = username
user = postfix
password = <password>
hosts =
dbname = vimbadmin
table = domain
select_field = transport
where_field = domain
additional_conditions = and backupmx = '0' and active = '1'

If you would like to block domain add the following lines:


To block senders:

email    REJECT User unkown

Create the map with:

cd /usr/local/etc/postfix
postmap client_checks
postmap sender_checks

Make sure you build you alias databases

cd /etc/mail
postalias aliases
postalias aliases.own

Install and Configure SRS

SRS is sender Rewriting Scheme daemon which is required if you forward mails and to not break SPF.

cd /usr/ports/mail/postsrsd
make install clean
sysrc postsrsd_enable="YES"
sysrc postsrsd_flags=" -4"

Add to file from postfix:

# Sender Rewriting
sender_canonical_maps = tcp:
sender_canonical_classes = envelope_sender
recipient_canonical_maps = tcp:
recipient_canonical_classes= envelope_recipient

Start it with:

/usr/local/etc/rc.d/postsrsd restart
/usr/local/etc/rc.d/postfix restart

WARNING! Make sure you include all domains you host to the ignore list for postsrsd. If not that could break SPF setup and will in worst case cause that all emails are bounced and not accepted by remote mail servers!


Maybe you have to apply this patch to fix a bug:



Postscreen is used to block spammers as early as possible using some checks including rbl lists.

Add the following lines in

## Postscreen setup
postscreen_access_list = permit_mynetworks,cidr:/usr/local/etc/postfix/postscreen_access.cidr
postscreen_blacklist_action = drop
# DNS Blackhole Lists
postscreen_dnsbl_threshold = 8
postscreen_dnsbl_sites =*7*7*5[10;11;12]*4*8*6*3*2*2*2[10..11]*8[4..7]*6*4*3*2*3*1*1*2*2*3[18;19;20]*-2[0..255].0*-3[0..255].1*-4[0..255].[2..255]*-6*-2
postscreen_dnsbl_action = enforce
# Pregreeting
postscreen_greet_action = enforce
# Additional Postscreen Tests
postscreen_pipelining_enable = no
postscreen_non_smtp_command_enable = no
postscreen_non_smtp_command_action = drop
postscreen_bare_newline_enable = no

In remove the smtp line and add/change the following:

smtp      inet  n       -       n       -       1       postscreen
smtpd     pass  -       -       n       -       -       smtpd
        -o content_filter=smtp-amavis:[]:10024
dnsblog   unix  -       -       n       -       0       dnsblog
tlsproxy  unix  -       -       n       -       0       tlsproxy

Create the file:

# Rules are evaluated in the order as specified.       permit
::1             permit

You must restart postfix if you have change the postscreen_access.cidr!



cd /usr/ports/mail/opendkim
make install clean
sysrc milteropendkim_enable="YES"
mkdir -p /var/db/opendkim
chown mailnull /var/db/opendkim/

Generate key for domain

cd /var/db/opendkim
opendkim-genkey -r -D /var/db/opendkim -d
mv default.private
mv default.txt

Copy the public key to your DNS server:

cp /usr/local/etc/namedb/master/
chown bind /usr/local/etc/namedb/master/
chmod 644 /usr/local/etc/namedb/master/

Make sure your DNS zone includes:

; include dkim public key
$INCLUDE /usr/local/etc/namedb/master/
_adsp._domainkey        IN TXT "dkim=unknown"

Increase your serial and reload the zone. Make sure your zone is correct with:

host -t TXT
dig +norec @localhost -t TXT

Now we configure what key is used for what domain:

# format:
#  $pattern     $keyname
# format
#  $keyname     $domain:$selector:$keypath

The last part is the configuration file:

--- opendkim.conf.sample        2015-04-29 12:06:58.290800000 +0200
+++ opendkim.conf       2015-04-29 15:56:05.735861987 +0200
@@ -116,7 +116,7 @@
 ##  operation.  Thus, cores will be dumped here and configuration files
 ##  are read relative to this location.
-# BaseDirectory                /var/run/opendkim
+BaseDirectory          /var/db/opendkim
 ##  BodyLengthDB dataset
 ##     default (none)
@@ -175,7 +175,7 @@
 ##  Specify for which domain(s) signing should be done.  No default; must
 ##  be specified for signing.
 ##  DomainKeysCompat { yes | no }
 ##     default "no"
@@ -261,7 +261,7 @@
 ##  a base64-encoded DER format private key, or a path to a file containing
 ##  one of those.
-# KeyTable             dataset
+KeyTable               /usr/local/etc/mail/DkimKeyTable
 ##  LocalADSP dataset
 ##     default (none)
@@ -290,7 +290,7 @@
 ##  in the amount of log data generated for each message, so it should be
 ##  limited to debugging use and not enabled for general operation.
-# LogWhy               no
+LogWhy         Yes
 ##  MacroList macro[=value][,...]
@@ -659,7 +659,7 @@
 ##  is set, all possible lookup keys will be attempted which may result
 ##  in multiple signatures being applied.
-# SigningTable         filename
+SigningTable           refile:/usr/local/etc/mail/DkimSigningTable
 ##  SingleAuthResult { yes | no}
 ##     default "no"
@@ -687,7 +687,7 @@
 ##  inet:port                  to listen on all interfaces
 ##  local:/path/to/socket      to listen on a UNIX domain socket
-Socket                 inet:port@localhost
+Socket                 inet:8891@localhost
 ##  SoftwareHeader { yes | no }
 ##     default "no"
@@ -746,7 +746,7 @@
 ##  Log success activity to syslog?
-# SyslogSuccess                No
+SyslogSuccess          Yes
 ##  TemporaryDirectory path
 ##     default /tmp

Now start the milter with:

/usr/local/etc/rc.d/milter-opendkim start

Check the /var/log/maillog for possible error messages, if you found no error message we can continue setting up postfix:

# OpenDKIM
milter_default_action = accept
milter_protocol = 2
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891

To test it write an email to

OpenDmarc (WIP)

A first comment: I do NOT recommend to use DMARC. It breaks most of the existing mailing list and you will get problem from a lot mail servers, that are not configured 100% correctly. DMARC will cause a lot of false-positives. I decided to not use it for any domain I manage. Depending on your needs, this could be different.


cd /usr/ports/mail/opendmarc
make install clean


cp /usr/local/etc/mail/opendmarc.conf.sample opendmarc.conf

Modify the configuration like this:

--- opendmarc.conf.sample       2015-04-29 11:17:12.018006000 +0200
+++ opendmarc.conf      2015-04-30 05:35:34.395463225 +0200
@@ -90,7 +90,7 @@
 ##  Requests addition of the specified email address to the envelope of
 ##  any message that fails the DMARC evaluation.
-# CopyFailuresTo postmaster@localhost
 ##  DNSTimeout (integer)
 ##     default 5
@@ -118,7 +118,7 @@
 ##  purported sender of the message has requested such reports.  Reports are
 ##  formatted per RFC6591.
-# FailureReports false
+FailureReports true
 ##  FailureReportsBcc (string)
 ##     default (none)
@@ -129,7 +129,7 @@
 ##  If no request is made, they address(es) are used in a To: field.  There
 ##  is no default.
-# FailureReportsBcc postmaster@example.coom
 ##  FailureReportsOnNone { true | false }
 ##     default "false"
@@ -273,7 +273,7 @@
 ##  either in the configuration file or on the command line.  If an IP
 ##  address is used, it must be enclosed in square brackets.
-# Socket inet:8893@localhost
+Socket inet:8893@localhost
 ##  SoftwareHeader { true | false }
 ##     default "false"
@@ -283,7 +283,7 @@
 ##  delivery.  The product's name, version, and the job ID are included in
 ##  the header field's contents.
-# SoftwareHeader false
+SoftwareHeader true
 ##  SPFIgnoreResults { true | false }
 ##     default "false"
@@ -312,7 +312,7 @@
 ##  Log via calls to syslog(3) any interesting activity.
-# Syslog false
+Syslog true
 ##  SyslogFacility facility-name
 ##     default "mail"
@@ -343,7 +343,7 @@
 ##  specific file mode on creation regardless of the process umask.  See
 ##  umask(2) for more information.
-# UMask 077
+UMask 0002
 ##  UserID user[:group]
 ##     default (none)
sysrc opendmarc_enable="YES"
touch /var/run/
chown mailnull:mailnull /var/run/
/usr/local/etc/rc.d/opendmarc start

In postfix add:

# OpenDKIM (port 8891), OpenDMARC (port 8893)
milter_default_action = accept
smtpd_milters = inet:localhost:8891, inet:localhost:8893
non_smtpd_milters = inet:localhost:8891, inet:localhost:8893

Restart postfix:

service postfix restart

The last step would be to add a dmarc TXT record to your DNS zone. You can use therefore:

A record could look like:

_dmarc                  IN TXT "v=DMARC1; p=none; sp=none;;; rf=afrf; pct=100; ri=86400"

To test the setup you can send an email to mentioned addresses here:

Antispam Plugin for Dovecot

cd /usr/ports/mail/dovecot2-antispam-plugin
make install clean
# antispam plugin
protocol imap {
    mail_plugins = $mail_plugins antispam

SOLR integration in dovecot

Make sure dovecot is compiled with solr support.

Make sure solr is running.

Create a new core for solr:

su -m solr -c "/usr/local/solr/bin/solr create_core -c dovecot"

Make sure we switch result from json to xml by editing:

  <!-- The following response writers are implicitly configured unless
     <queryResponseWriter name="xml"
                          class="solr.XMLResponseWriter" />

Copy the dovecot schema.xml configuration:

rm /var/db/solr/dovecot/conf/managed-schema

Create the schema file:

<?xml version="1.0" encoding="UTF-8" ?>
For fts-solr:
This is the Solr schema file, place it into solr/conf/schema.xml. You may
want to modify the tokenizers and filters.
<schema name="dovecot" version="1.5">
    <!-- IMAP has 32bit unsigned ints but java ints are signed, so use longs -->
    <fieldType name="string" class="solr.StrField" />
    <fieldType name="long" class="solr.TrieLongField" />
    <fieldType name="text" class="solr.TextField" positionIncrementGap="100">
      <analyzer type="index">
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_en.txt"/>
        <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1"/>
        <filter class="solr.LowerCaseFilterFactory"/>
        <filter class="solr.EnglishPossessiveFilterFactory"/>
        <filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt"/>
        <filter class="solr.EnglishMinimalStemFilterFactory"/>
      <analyzer type="query">
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_en.txt"/>
        <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="0" catenateNumbers="0" catenateAll="0" splitOnCaseChange="1"/>
        <filter class="solr.LowerCaseFilterFactory"/>
        <filter class="solr.EnglishPossessiveFilterFactory"/>
        <filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt"/>
        <filter class="solr.EnglishMinimalStemFilterFactory"/>
    <!-- boolean type: "true" or "false" -->
    <fieldType name="boolean" class="solr.BoolField" sortMissingLast="true"/>
    <fieldType name="booleans" class="solr.BoolField" sortMissingLast="true" multiValued="true"/>
      Numeric field types that index values using KD-trees.
      Point fields don't support FieldCache, so they must have docValues="true" if needed for sorting, faceting, functions, etc.
    <fieldType name="pint" class="solr.IntPointField" docValues="true"/>
    <fieldType name="pfloat" class="solr.FloatPointField" docValues="true"/>
    <fieldType name="plong" class="solr.LongPointField" docValues="true"/>
    <fieldType name="pdouble" class="solr.DoublePointField" docValues="true"/>
    <fieldType name="pints" class="solr.IntPointField" docValues="true" multiValued="true"/>
    <fieldType name="pfloats" class="solr.FloatPointField" docValues="true" multiValued="true"/>
    <fieldType name="plongs" class="solr.LongPointField" docValues="true" multiValued="true"/>
    <fieldType name="pdoubles" class="solr.DoublePointField" docValues="true" multiValued="true"/>
    <!-- The format for this date field is of the form 1995-12-31T23:59:59Z, and
         is a more restricted form of the canonical representation of dateTime
         The trailing "Z" designates UTC time and is mandatory.
         Optional fractional seconds are allowed: 1995-12-31T23:59:59.999Z
         All other components are mandatory.
         Expressions can also be used to denote calculations that should be
         performed relative to "NOW" to determine the value, ie...
                  ... Round to the start of the current hour
                  ... Exactly 1 day prior to now
                  ... 6 months and 3 days in the future from the start of
                      the current day
    <!-- KD-tree versions of date fields -->
    <fieldType name="pdate" class="solr.DatePointField" docValues="true"/>
    <fieldType name="pdates" class="solr.DatePointField" docValues="true" multiValued="true"/>
    <!--Binary data type. The data should be sent/retrieved in as Base64 encoded Strings -->
    <fieldType name="binary" class="solr.BinaryField"/>
    <fieldType name="text_general" class="solr.TextField" positionIncrementGap="100" multiValued="true">
      <analyzer type="index">
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
        <!-- in this example, we will only use synonyms at query time
        <filter class="solr.SynonymGraphFilterFactory" synonyms="index_synonyms.txt" ignoreCase="true" expand="false"/>
        <filter class="solr.FlattenGraphFilterFactory"/>
        <filter class="solr.LowerCaseFilterFactory"/>
      <analyzer type="query">
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
        <filter class="solr.SynonymGraphFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
        <filter class="solr.LowerCaseFilterFactory"/>
   <field name="id" type="string" indexed="true" stored="true" required="true" />
   <field name="uid" type="long" indexed="true" stored="true" required="true" />
   <field name="box" type="string" indexed="true" stored="true" required="true" />
   <field name="_text_" type="text_general" indexed="true" stored="false" multiValued="true"/>
   <field name="user" type="string" indexed="true" stored="true" required="true" />
   <field name="hdr" type="text" indexed="true" stored="false" />
   <field name="body" type="text" indexed="true" stored="false" />
   <field name="from" type="text" indexed="true" stored="false" />
   <field name="to" type="text" indexed="true" stored="false" />
   <field name="cc" type="text" indexed="true" stored="false" />
   <field name="bcc" type="text" indexed="true" stored="false" />
   <field name="subject" type="text" indexed="true" stored="false" />
   <!-- Used by Solr internally: -->
   <field name="_version_" type="long" indexed="true" stored="true"/>

Now restart solr and check the logfile:

tail -F /var/log/solr/solr.log
service solr restart

You should not see an error message but something like this:

2017-10-12 15:57:07.584 INFO  (searcherExecutor-7-thread-1-processing-x:dovecot) [   x:dovecot] o.a.s.c.SolrCore [dovecot] Registered new searcher Searcher@2ed6c198[dovecot] main{ExitableDirectoryReader(UninvertingDirectoryReader())}

Now we can configure dovecot.

mail_plugins = $mail_plugins fts fts_solr
plugin {
    fts_autoindex = yes
    fts = solr
    fts_solr = url=

Restart dovecot:

service dovecot restart

Keep the tail on the solr log file running and execute:

doveadm index -u idefix inbox

Now after some seconds you should see that solr is is indexing emails.

We would like to add some maintenance tasks for solr:

# Optimize solr dovecot storage
2               2               *               *               *               root    curl ""
5               */1             *               *               *               root    curl ""

That's it, have fun ;)

freebsd/postfix_dovecot_virtual.txt · Zuletzt geändert: 2018/12/10 13:11 (Externe Bearbeitung)