Setup Email Server From Scratch On FreeBSD #2 - 07 RoundCube WebMail

06 SPF DMARC And DKIM <- Intro -> 09 Create Virtual Domains

We believe in data independence, and support others who want data independence.
This tutorial is partially complete 2025-08-07

This is version 2 and everthing works up to and including Roundcube.

######################################
# Setup RoundCube Webmail On FreeBSD #
######################################

# Getting Roundcube setup and installing the modules could be a tutorial by itself.

# Download Roundcube from https://roundcube.net/download/ and get the complete stable version 
# roundcubemail-1.6.11-complete.tar.gz.

cd /tmp
wget https://github.com/roundcube/roundcubemail/releases/download/1.6.11/roundcubemail-1.6.11-complete.tar.gz
tar xfzv roundcubemail-1.6.11-complete.tar.gz
mv /tmp/roundcubemail-1.6.11 /usr/local/www/roundcube
chown -R root:wheel /usr/local/www/roundcube
mkdir -p /usr/local/www/roundcube/temp
mkdir -p /usr/local/www/roundcube/logs
chown -R www:www /usr/local/www/roundcube/temp
chown -R www:www /usr/local/www/roundcube/logs

# Add PHP Modules - some of these may not be neeeded, but with these everything works as expected

pkg install php83-pear-DB_ldap2 php83-pecl-imagick php83-pecl-redis php83-mysqli openssl zoneinfo-2025.a
pkg install php83-imap php83-curl php83-zip php83-mbstring php83-bz2 php83-intl php83-gmp php83-gd
pkg install php83-pear wget Crypt_GPG py311-lesscpy php83-composer gnupg
pkg install php83-gettext php83-phar php83-enchant sabredav sabre php83-xmlreader

pkg install gnupg
pear install Crypt_GPG

# We need lessc and lesscpy is not lessc, so p311-lesscpy may not be needed, to be cautious
# install it, or try without.

pkg install py311-lesscpy

# Add /proc filesystem - some python functions require /proc.

mount -t procfs proc /proc
echo 'proc    /proc           procfs          rw      0       0' >> /etc/fstab

# Install less package with npm which has lessc.

pkg install node
pkg install npm
npm install -g less

# Get your timezone for adding to php.ini

ls /usr/share/zoneinfo/*

# Configure php.ini including the timezone - the values in php.ini affect roundcube max attachment size.

nano /usr/local/etc/php.ini
# ---
max_execution_time = 600
max_input_time = 300
max_input_vars = 3000
memory_limit = 256M
post_max_size = 200M
upload_max_filesize = 100M
...
date.timezone = US/Pacific
# ---

# Edit MySQL configuration

nano /usr/local/etc/mysql/my.cnf
# --- add to the bottom of the mysqld section
[mysqld]
...
max_connections = 500
tmp_table_size = 128M
max_heap_table_size = 128M
sort_buffer_size = 2M
slow_query_log = 1
# skip-name-resolve
lower_case_table_names=1
character-set-server     = utf8mb4
# collation-server      = utf8mb4_general_ci
collation_server        = utf8mb4_unicode_520_ci
# ---

service mysql-server restart

# Create Roundcube Database

mysql -u root
# ---
> create database roundcubemail;
> CREATE USER 'roundcube'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'secretpasswd';
> grant all privileges on roundcubemail.* to 'roundcube'@'localhost';
> flush privileges;
> exit;
# ---

# Test the mysql login

mysql -u roundcube -psecretpasswd roundcubemail
> exit;

# Import Database Tables

mysql roundcubemail < /usr/local/www/roundcube/SQL/mysql.initial.sql

# Change Apache DocumentRoot and Directory to Roundcube

nano /usr/local/etc/apache24/Includes/ssl-mail.okbsd.conf
# ---
        #DocumentRoot /usr/local/www/okbsd/html/
        #<Directory /usr/local/www/okbsd/html/>
        #        Options FollowSymLinks
        #        AllowOverride All
        #        Require all granted
        #</Directory>
        DocumentRoot /usr/local/www/roundcube/
        Alias /webmail "/usr/local/www/roundcube"
        Alias /roundcube "/usr/local/www/roundcube"
        <Directory /usr/local/www/roundcube/>
                # Options FollowSymLinks MultiViews
                Options FollowSymLinks
                AllowOverride All
                # Require all granted
                Require all denied
                Require ip 10.0.0.0/8 192.168.0.0/16 127.0.0.1 trusted_ip3 trusted_ipv6
        </Directory>
# ---

# Don't use Alias /mail "/usr/share/roundcube" because /mail is used for autoconfig. 
# MultiViews doesn't seem to be required. It is better to configure Roundcube with 
# ssl/https only to force https password encryption.

service php_fpm restart
apachectl restart

# Configure Roundcube imap, smtp, and spell checking.

cd /usr/local/www/roundcube/config
cp /usr/local/www/roundcube/config/config.inc.php.sample /usr/local/www/roundcube/config/config.inc.php

# Please change the DES Key to a random 24 char string and make it exactly 24 chars and set your 
# super_secret_password the same as you provided when creating the roundcube database. Please count the 
# chars in your key, base 64 defaults to 32 so remove 8 chars.

openssl rand -base64 24

nano /usr/local/www/roundcube/config/config.inc.php
# ---
$config['db_dsnw'] = 'mysql://roundcube:secretpasswd@localhost/roundcubemail';
$config['imap_host'] = 'tls://imap.okbsd.com:143';
$config['smtp_host'] = 'tls://smtp.okbsd.com:587';

// Replace the default key with 24 random characters.
$config['des_key'] = '123456789012345678901234';

// Enable more plugins
$config['plugins'] = ['acl', 'additional_message_headers', 'archive', 'attachment_reminder',
'autologon', 'debug_logger', 'emoticons', 'enigma', 'filesystem_attachments', 'help',
'hide_blockquote', 'http_authentication', 'identicon', 'identity_select', 'jqueryui',
'krb_authentication', 'managesieve', 'markasjunk', 'new_user_dialog', 'new_user_identity',
'newmail_notifier', 'password', 'reconnect', 'redundant_attachments', 'show_additional_headers',
'squirrelmail_usercopy', 'subscriptions_option', 'userinfo', 'vcard_attachments', 'virtuser_file',
'virtuser_query', 'zipdownload','automatic_addressbook'];

// skin name: folder from skins/
$config['enable_spellcheck'] = true;
$config['spellcheck_engine'] = 'enchant';

// this will make max upload 75% so 100M => 75M
$config['max_message_size'] = '134M';
$config['create_default_folders'] = true;

?>
# ---

# Fix directories and permissions - any files can contain mysql passwords should not writable by user
# or group www and not be world readable.

chown root:www /usr/local/www/roundcube/config/config.inc.php
chmod 640 /usr/local/www/roundcube/config/config.inc.php

# Enable Enigma - needed for setting mail users signature and pgp options.

# Create enigma home
mkdir -p /usr/local/www/roundcube/plugins/enigma/home
chown www:www /usr/local/www/roundcube/plugins/enigma/home
chmod 750 /usr/local/www/roundcube/plugins/enigma/home

# Configure Enigma
cd /usr/local/www/roundcube/plugins/enigma
cp /usr/local/www/roundcube/plugins/enigma/config.inc.php.dist /usr/local/www/roundcube/plugins/enigma/config.inc.php

nano /usr/local/www/roundcube/plugins/enigma/config.inc.php
# ---
$config['enigma_pgp_homedir'] = "/usr/local/www/roundcube/plugins/enigma/home";
# ---

chown root:www /usr/local/www/roundcube/plugins/enigma/config.inc.php
chmod 640 /usr/local/www/roundcube/plugins/enigma/config.inc.php

# Automatic Addressbook

cd /tmp
git clone https://github.com/sblaisot/automatic_addressbook.git
mv /tmp/automatic_addressbook /usr/local/www/roundcube/plugins
cd /usr/local/www/roundcube/plugins/automatic_addressbook/SQL
mysql -u roundcube -psecretpasswd roundcubemail < mysql.initial.sql
cd /usr/local/www/roundcube/plugins/automatic_addressbook/config
cp /usr/local/www/roundcube/plugins/automatic_addressbook/config/config.inc.php.dist /usr/local/www/roundcube/plugins/automatic_addressbook/config/config.inc.php

chown root:www /usr/local/www/roundcube/plugins/automatic_addressbook/config/config.inc.php
chmod 640 /usr/local/www/roundcube/plugins/automatic_addressbook/config/config.inc.php

# Test Roundcube Login ...

https://mail.okbsd.com

# Check Settings -> Identities -> Click On User -> If fixed it shows Signature and Manage PGP Keys

# Sieve Install - seive is provided by dovecot-pideonhole and is needed for mail filtering in Roundcube.

echo 'DEFAULT_VERSIONS+=ssl=openssl' >> /etc/make.conf

cd /usr/ports/mail/dovecot-pigeonhole
make
X DOCS
X EXAMPLES
X LDAP
X MANAGESIEVE
X GSSAPI_NONE

make install

nano /usr/local/etc/dovecot/dovecot.conf
# ---
protocols = imap lmtp sieve
# ... append to bottom
service managesieve-login {
  inet_listener sieve {
    port = 4190
  }
  inet_listener sieve_deprecated {
    port = 2000
  }
  service_count = 1
  process_min_avail = 0
  vsz_limit = 64M
}
service managesieve {
  process_limit = 1024
}
protocol sieve {
    managesieve_max_line_length = 65536
    managesieve_implementation_string = dovecot
    log_path = /var/log/dovecot-sieve-errors.log
    info_log_path = /var/log/dovecot-sieve.log
}
# ---

nano /usr/local/etc/dovecot/conf.d/10-master.conf
# ---
service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0600
    user = postfix
    group = postfix
  }
}
# ---

nano /usr/local/etc/postfix/main.cf
# ---
mailbox_transport = lmtp:unix:private/dovecot-lmtp
smtputf8_enable = no
# ---

nano /usr/local/etc/dovecot/conf.d/15-lda.conf
# ---
protocol lda {
  # Space separated list of plugins to load (default is global mail_plugins).
  mail_plugins = $mail_plugins sieve
}
# ---

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

nano /usr/local/etc/dovecot/conf.d/10-mail.conf
# ---
mail_location = maildir:~/Maildir
mail_home = /var/vmail/%d/%n
# ---

mkdir -p /usr/local/etc/dovecot/sieve/global

nano /usr/local/etc/dovecot/sieve/before-global.sieve
# ---
require "fileinto";

if header :contains "X-Spam-Flag" "YES"
{
   fileinto "Junk";
   stop;
}
# ---

sievec /usr/local/etc/dovecot/sieve/before-global.sieve

nano /usr/local/etc/dovecot/conf.d/90-plugin.conf
# ---
plugin {
  #setting_name = value
  sieve = file:~/sieve;active=~/.dovecot.sieve
  sieve_default = /usr/local/etc/dovecot/sieve/default.sieve
  sieve_global = /usr/local/etc/dovecot/sieve/global/
  sieve_before = /usr/local/etc/dovecot/sieve/before-global.sieve
}
# ---

service postfix restart
service dovecot restart

# Configure managesieve in Roundcube

cd /usr/local/www/roundcube/plugins/managesieve
cp /usr/local/www/roundcube/plugins/managesieve/config.inc.php.dist /usr/local/www/roundcube/plugins/managesieve/config.inc.php

chown root:www /usr/local/www/roundcube/plugins/managesieve/config.inc.php
chmod 640 /usr/local/www/roundcube/plugins/managesieve/config.inc.php

nano /usr/local/www/roundcube/plugins/managesieve/config.inc.php
# --- add or change
// $config['managesieve_host'] = 'localhost';
$config['managesieve_host'] = 'tls://smtp.okbsd.com';
$config['managesieve_usetls'] = true;
$config['managesieve_default'] = '/usr/local/etc/dovecot/sieve/global';
$config['managesieve_debug'] = true;
$config['managesieve_usetls'] = true;
# ---

apachectl restart

# Remove Roundcube Header

nano /usr/local/etc/postfix/smtp_header_checks
# --- create or add
/^User-Agent.*Roundcube Webmail/            IGNORE
# ---

nano /usr/local/etc/postfix/main.cf
# --- append to end of file
smtp_header_checks = regexp:/usr/local/etc/postfix/smtp_header_checks
# ---

postmap /usr/local/etc/postfix/smtp_header_checks
service postfix restart

# Configure RoundCube Password Plugin

cd /usr/local/www/roundcube/plugins/password
cp /usr/local/www/roundcube/plugins/password/config.inc.php.dist /usr/local/www/roundcube/plugins/password/config.inc.php

nano /usr/local/www/roundcube/plugins/password/config.inc.php
# ---
$config['password_algorithm'] = 'dovecot';
$config['password_dovecotpw'] = '/usr/local/bin/doveadm pw -r 5';
$config['password_dovecotpw_method'] = 'ARGON2I';
$config['password_dovecotpw_with_method'] = true;
$config['password_db_dsn'] = 'mysql://postfixadmin:secretpasswd@localhost/postfixadmin';
$config['password_query'] = 'UPDATE mailbox SET password=%P,modified=NOW() WHERE username=%u';
$config['password_strength_driver'] = null;
$config['password_minimum_length'] = 8;
$config['password_minimum_score'] = 0;
# ---

chown root:www /usr/local/www/roundcube/plugins/password/config.inc.php
chmod 640 /usr/local/www/roundcube/plugins/password/config.inc.php

# Enable plugins 'automatic_addressbook','password' in Roundcube remove redundant_attachments

nano /usr/local/www/roundcube/config/config.inc.php
# ---
$config['plugins'] = ['acl', 'additional_message_headers', 'archive', 'attachment_reminder',
'autologon', 'debug_logger', 'emoticons', 'enigma', 'filesystem_attachments', 'help',
'hide_blockquote', 'http_authentication', 'identicon', 'identity_select', 'jqueryui',
'krb_authentication', 'managesieve', 'markasjunk', 'new_user_dialog', 'new_user_identity',
'newmail_notifier', 'password', 'reconnect', 'show_additional_headers',
'squirrelmail_usercopy', 'subscriptions_option', 'userinfo', 'vcard_attachments', 'virtuser_file',
'virtuser_query', 'zipdownload','automatic_addressbook','password'];
//'redundant_attachments',
# ---

# If you have problems double check ARGON2I has been compiled into dovecot.
 
/usr/local/bin/doveadm pw -l
SHA1 SSHA512 SCRAM-SHA-256 BLF-CRYPT PLAIN HMAC-MD5 OTP SHA512 SHA DES-CRYPT CRYPT SSHA
MD5-CRYPT PLAIN-MD4 PLAIN-MD5 SCRAM-SHA-1 SHA512-CRYPT CLEAR CLEARTEXT ARGON2I ARGON2ID
SSHA256 MD5 PBKDF2 SHA256 CRAM-MD5 PLAIN-TRUNC SHA256-CRYPT SMD5 DIGEST-MD5 LDAP-MD5

# Check that changing password works in roundcube

# I was not able to change passwords because the password_db_dsn default is to use
# 127.0.0.1 and I had put skip-name-resolve in my.cnf.

cat /usr/local/www/roundcube/logs/errors.log
Host '127.0.0.1' is not allowed to connect to this MySQL server

mysql -u postfixadmin -psecretpasswd -h 127.0.0.1 postfixadmin
Host '127.0.0.1' is not allowed to connect to this MySQL server

mysql -u postfixadmin -psecretpasswd -h localhost postfixadmin
> exit

# Two solutions, get rid of skip-name-resolve in my.cnf (preferred) or change the password_db_dsn line.

nano /usr/local/etc/mysql/my.cnf
# ---
# skip-name-resolve
# ---

nano /usr/local/www/roundcube/plugins/password/config.inc.php
# ---
// $config['password_db_dsn'] = 'mysql://postfixadmin:secretpasswd@127.0.0.1/postfixadmin';
$config['password_db_dsn'] = 'mysql://postfixadmin:secretpasswd@localhost/postfixadmin';
# ---

# It is best to disable skip-name-resolve for flexibility but some sources say localhost uses the mysql.sock
# which is more efficient and secure than using the inet port 3306. When at all possible configure to unix
# sockets which have less overhead and are slightly faster.

# Install Calendar

cd /tmp
git clone https://git.kolab.org/diffusion/RPK/roundcubemail-plugins-kolab.git

cd /usr/local/www/roundcube/plugins
cp -r /tmp/roundcubemail-plugins-kolab/plugins/calendar .
cp -r /tmp/roundcubemail-plugins-kolab/plugins/libcalendaring .
cp -r /tmp/roundcubemail-plugins-kolab/plugins/libkolab .

# Install sabre/dav

cd /usr/local/www/roundcube

nano /usr/local/www/roundcube/composer.json
# ---
{
    "name": "roundcube/roundcubemail",
    "version": "1.6.11",
    "description": "The Roundcube Webmail suite",
    "license": "GPL-3.0-or-later",
    "repositories": [
        {
            "type": "composer",
            "url": "https://plugins.roundcube.net"
        }
    ],
    "require": {
        "php": ">=7.3.0",
        "pear/pear-core-minimal": "~1.10.1",
        "pear/auth_sasl": "~1.1.0",
        "pear/mail_mime": "~1.10.0",
        "pear/net_smtp": "~1.10.0",
        "pear/crypt_gpg": "~1.6.3",
        "pear/net_sieve": "~1.4.5",
        "roundcube/plugin-installer": "~0.3.1",
        "roundcube/rtf-html-php": "~2.1",
        "masterminds/html5": "~2.7.0",
        "bacon/bacon-qr-code": "^2.0.0",
        "guzzlehttp/guzzle": "^7.3.0",
        "kolab/net_ldap3": "~1.1.4",
    },
    "suggest": {
        "bjeavons/zxcvbn-php": "^1.0 required for Zxcvbn password strength driver"
    },
    "config": {
        "allow-plugins": {
            "roundcube/plugin-installer": true
        }
    }
}
# ---

# Run composer to install the dependencies. Initially I just ran composer require sabre/dav but
# composer said it could not find the version of roundcube/roundcube so defaulted to 1.0.0 which
# downgraded the install. Adding "version": "1.6.11", is the above file fixed this and it would
# be better to do this first.

composer require sabre/dav ~4.7.0 sabre/vobject ^4.5.7 --with-all-dependencies

# Test what versions were installed.

grep sabre composer.json
        "sabre/dav": "~4.7.0",
        "sabre/vobject": "^4.5.7"

# Copy calendar configuration and set permissions.

cd /usr/local/www/roundcube/plugins/calendar
cp /usr/local/www/roundcube/plugins/calendar/config.inc.php.dist /usr/local/www/roundcube/plugins/calendar/config.inc.php
chown root:www /usr/local/www/roundcube/plugins/calendar/config.inc.php
chmod 640 /usr/local/www/roundcube/plugins/calendar/config.inc.php

# Initialize the calendar database

cd /usr/local/www/roundcube/plugins/libkolab/SQL
mysql -u roundcube -psuper_secret_password  roundcubemail < /usr/local/www/roundcube/plugins/libkolab/SQL/mysql.initial.sql

cd /usr/local/www/roundcube
bin/initdb.sh --dir=plugins/calendar/drivers/database/SQL

# Generate the compiled css files with lessc, this will generate a message of missing svg files, but calendar works.

lessc -x /usr/local/www/roundcube/plugins/libkolab/skins/elastic/libkolab.less > /usr/local/www/roundcube/plugins/libkolab/skins/elastic/libkolab.min.css

# Enable calendar in Roundcube main configuration file.

# To customize displayed product_name based on the virtual server used include the routine shown here. 
# Creating default folders is a nice touch. If you do not use the default path in apache set url_base in the 
# roundcube configuration. Roundcube max "attachments" size is 2M by default, use max_message_size to 
# override. Also, default values in php.ini need to be modified to increase max attachment size.

nano /usr/local/www/roundcube/config/config.inc.php
# ---
...
$config['db_dsnw'] = 'mysql://roundcube:secretpasswd@localhost/roundcubemail';
$config['imap_host'] = 'tls://imap.okbsd.com:143';
$config['smtp_host'] = 'tls://smtp.okbsd.com:587';
$config['smtp_user'] = '%u';
$config['smtp_pass'] = '%p';
$config['support_url'] = '';
...
// Name your service. This is displayed on the login screen and in the window title
$sn = $_SERVER['SERVER_NAME'];
if (preg_match('/coragarden\.com/', $sn)) {
        $config['product_name'] = 'Cora Garden Webmail';
} else if (preg_match('/okbsd\.com/', $sn)) {
	$config['product_name'] = 'OkBSD Webmail';
} else if (preg_match('/okdeb\.com/', $sn)) {
        $config['product_name'] = 'OkDeb Webmail';
} else {
        $config['product_name'] = 'Roundcube Webmail';
}

$config['des_key'] = '123456789012345678901234';

$config['plugins'] = ['acl', 'additional_message_headers', 'archive', 'attachment_reminder',
'autologon', 'debug_logger', 'emoticons', 'enigma', 'filesystem_attachments', 'help',
'hide_blockquote', 'http_authentication', 'identicon', 'identity_select', 'jqueryui',
'krb_authentication', 'managesieve', 'markasjunk', 'new_user_dialog', 'new_user_identity',
'newmail_notifier', 'password', 'reconnect', 'show_additional_headers',
'squirrelmail_usercopy', 'subscriptions_option', 'userinfo', 'vcard_attachments', 'virtuser_file',
'virtuser_query', 'zipdownload', 'automatic_addressbook', 'calendar', 'password'];
// 'redundant_attachments'

// skin name: folder from skins/
$config['skin'] = 'elastic';
$config['max_pagesize'] = 1000;

$config['enable_spellcheck'] = true;
$config['spellcheck_engine'] = 'enchant';

// this will make max upload 75% so 100M => 75M or 134M => 100M
$config['max_message_size'] = '134M';

$config['create_default_folders'] = true;
// $config['url_base'] = '/mail-login/';

?>
# ---

# Test that everything works as expected

# Install the larry skin which are compatable with calendar.

cd /tmp
git clone https://github.com/roundcube/larry.git
cp -r larry /var/www/roundcube/skins/
cd /var/www/roundcube/config

# You can now select which skin you'd like.

Settings -> Preferences - > User Interface

# There are many more roundcube skins but most require payment.

# Free colorful larry skins https://github.com/roundcube/larry.git that
# DO NOT WORK WITH CALENDAR!

cd /tmp
git clone https://github.com/texxasrulez/roundcube_skins.git
cd /tmp/roundcube_skins/skins
mv * /var/www/roundcube/skins

# Try html5_notifier, I didn't see any notifications when I used it.
# html5_notifier doesn't seem to work under Debian with Firefox...

cd /tmp
git clone https://github.com/stremlau/html5_notifier.git
mv /tmp/html5_notifier /var/www/roundcube/plugins/
cd /var/www/roundcube/plugins/html5_notifier/config

cp /var/www/roundcube/plugins/html5_notifier/config/config.inc.php.dist \
/var/www/roundcube/plugins/html5_notifier/config/config.inc.php

nano /var/www/roundcube/config/config.inc.php
# --- append to the end of modules list
'virtuser_query', 'zipdownload', 'automatic_addressbook', 'calendar','password', 'html5_notifier'];
# ---

# A vexiadmin plugin allows users to manage spam filtering

# Roundcube has an extensive set of plugins which are beyond the scope of this setup
# tutorial. It would be interesting to set this up with caldav cardav and maybe webdav
# could be integrated for sharing larger files.

# Next - Multiple "Virtual" Mail Domains

06 SPF DMARC And DKIM <- Intro -> 09 Create Virtual Domains