Gerardo Zamudio about Linux, open source, and Internet

Slackware 15 -current Mail Server with MariaDB, Postfix, and Dovecot


Happy new year! The start of a new year is a perfect time for a fresh mail server, don’t you agree? :)

Slackware 15 has not been released yet but there is demand for an updated guide given Slackware has had many changes since its last stable release.

For the purposes of this guide, the most notable changes are Postfix and Dovecot now being the default MTA and IMAP/POP3 servers, respectively. This doesn’t affect this guide too much. In fact, most of the configuration remains the same as in my previous post for Slackware 14.2.

This post contains some suggestions for a mildly secure mail server running on a Slackware Linux host. The guide assumes a default, fresh installation of Slackware64 15 (post 14.2 -current) that includes at least the A/, AP/, D/, L/, and N/ package series. By the end of the tutorial, you will have:

  • Postfix for encrypted connections over SMTP
  • Dovecot for local mail directories and encrypted connections over POP and IMAP
  • MariaDB to store mailbox information
  • Postgrey which will require unknown senders to resend their mail, eliminating most spam
  • SpamAssassin for e-mail spam filtering based on content-matching rules
  • ClamAV to detect trojans, viruses, malware and other malicious threats in your email
  • amavisd-new to manage ClamAV and SpamAssasin
  • OpenDKIM a DomainKeys Identified Mail (DKIM) milter to sign and/or verify mail
  • nginx as a webserver
  • Postfix Admin to manage mailboxes and domains using a TLS secured web user interface
  • Roundcube with some plugins as a TLS secured webmail client
  • Pigeonhole to add support for email filter rules (forwarding, placing in folders, etc)



Set up an FQDN as your hostname. We’ll use

echo "" > /etc/HOSTNAME
hostname -F /etc/HOSTNAME

PHP + nginx


In order to use GD with PHP we’ll need the following packages. You won’t have these if you only installed the recommended package series from above.

# slackpkg install libX11 libXpm libxcb libXau libXdmcp fontconfig libXext libXt libSM libICE

Install nginx

My last guide used nginx 1.14.2, but it’s now at version 1.18.0! I won’t go through all the changes here but I recommend you take a look at the CHANGELOG.

Create your user and use that to run the SlackBuild. If the version of nginx is newer than the one defined in the SlackBuild, simply update the version number in the nginx.SlackBuild file before running it.

useradd -r -M -U -c "User for nginx" -d /srv/httpd -s /bin/false nginx
NGINXUSER=nginx NGINXGROUP=nginx ./nginx.SlackBuild

Remember that if you want to enable syntax highlighlting for the nginx.conf file in vim, you can copy the contents of the contrib/vim directory from the extracted nginx source to ~/.vim.

tar -xvzf nginx-1.18.0.tar.gz 
mv nginx-1.18.0/contrib/vim ~/.vim

SSL Certificates

I’m using ECDSA certificates and the secp384r1 curve here. Remember to change the configuration below if you use something else.

Configure nginx

At minimum, your nginx.conf file will need an events section which can be left blank to activate the defaults or modified depending on the load you’re expecting on the server. Set worker_processes equal to the number of real CPU cores on the machine.

user nginx;
worker_processes 4;
pid /var/run/;

events {

  worker_connections 1024;

http {

  map $sent_http_content_type $expires {
    default off;
    application/x-javascript 1d;
    text/css 1d;
    ~image/ 1d;

  expires $expires;
  client_max_body_size 12m;
  default_type application/octet-stream;
  gzip on;
  gzip_vary on;
  gzip_http_version 1.0;
  gzip_comp_level 2;
  gzip_min_length 10240;

  gzip_proxied expired no-cache no-store private auth;
  gzip_types text/plain text/css text/xml text/javascript text/json application/json application/x-javascript application/xml application/xml+rss;

  gzip_disable "MSIE [1-6]\.";
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;
  include /etc/nginx/mime.types;
  upstream php_workers {
    server unix:/var/run/php-fpm.sock;
  sendfile on;
  # Hide Nginx version number
  server_tokens off;
  types_hash_max_size 2048;

  include /etc/nginx/conf.d/*.conf;

Create your /etc/nginx/conf.d/mailserver.conf

server {

  # Listen on ipv4
  listen 80;

  server_name _;
  return 301 https://$host$request_uri;

server {
    listen 443 ssl http2;
    server_name _;
    root /var/www/html;
    index index.php index.html;
    location ~ ^/.well-known/ {
        allow all;
        access_log off;
        log_not_found off;
        autoindex off;
        #root /var/www/html;

   location ~ /\. { deny all; }
   location = /favicon.ico { access_log off; log_not_found off; }
   location = /robots.txt { access_log off; log_not_found off; }
   ssl_protocols TLSv1.3 TLSv1.2;
   ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
   ssl_prefer_server_ciphers on;
   ssl_ecdh_curve secp384r1; 
   ssl_certificate /home/acme/;
   ssl_certificate_key /home/acme/;
   # Roundcube
   location ~ ^/mail/(bin|config|installer|logs|SQL|temp|vendor)($|/.*) { deny all; }
   location ~ ^/mail/(CHANGELOG|composer.json|INSTALL|jsdeps.json|LICENSE|README|UPGRADING)($|.*) { deny all; }
   location ~ ^/mail/plugins/.*/* { deny all; }
   location ~ ^/mail/plugins/enigma/home($|/.*) { deny all; }
   # Redirect URI `/mail` to `/mail/`.
   location = /mail {
        return 301 /mail/;
   location ~ ^/mail/(.*\.php)$ {
        add_header Strict-Transport-Security "max-age=31536000";
        include fastcgi_params;
        fastcgi_index index.php;
        fastcgi_pass php_workers;
        fastcgi_param HTTP_PROXY '';
        fastcgi_param SCRIPT_FILENAME /var/www/roundcubemail/$1;
   location ~ ^/mail/(.*) {
        alias /var/www/roundcubemail/$1;
        index index.php;
   # Postfixadmin
   location = /postfixadmin {
        return 301 /postfixadmin/;
   location ~ ^/postfixadmin/(.*\.php)$ {
        add_header Strict-Transport-Security "max-age=31536000";
        include fastcgi_params;
        fastcgi_index index.php;
        fastcgi_pass php_workers;
        fastcgi_param HTTP_PROXY '';
        fastcgi_param SCRIPT_FILENAME /var/www/postfixadmin/public/$1;
   location ~ ^/postfixadmin/(.*) {
        alias /var/www/postfixadmin/public/$1;
        index index.php;
   # Everything else
   location ~ \.php$ {
        include fastcgi_params;
        fastcgi_index index.php;
        fastcgi_pass php_workers;
        fastcgi_param HTTP_PROXY '';
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

Change the permissions of /var/lib/php since we’re using nginx instead of httpd to run PHP:

chown root:nginx /var/lib/php/

PHP-FPM with FastCGI in nginx

We will start by creating a custom /etc/php-fpm.d/mailserver.conf with the following content:

user = nginx
group = nginx
listen = /var/run/php-fpm.sock
listen.owner = nginx = nginx
listen.mode = 0666
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 5
request_terminate_timeout = 10s
request_slowlog_timeout = 10s
access.log = /var/log/php-fpm_access.log
slowlog = /var/log/php-fpm_slow.log
security.limit_extensions = .php .php3 .php4 .php5 .html .htm

Make sure the startup script is executable then start php-fpm at least once to make sure everything is fine:

chmod +x /etc/rc.d/rc.php-fpm
/etc/rc.d/rc.php-fpm start
/etc/rc.d/rc.php-fpm stop

Install php-imagick as well since Roundcube will need it later.


Slackware has chosen Dovecot as the new IMAP/POP3 server as of November 2017. Instead of creating a Unix account for each mailbox, we use Postfix Admin to store user information. In the filesystem, the email will be stored in /var/vmail organized by domain and user, so the email for would be stored in /var/vmail/ Since Dovecot is now part of Slackware, the dovenull and dovecot users already exist so there is no need to create them. You will only need to create the vmail user

useradd -r -d /var/vmail -s /bin/false -u 150 -g 12 vmail
mkdir /var/vmail
chmod 770 /var/vmail
chown vmail:mail /var/vmail

Fill in the database information in /etc/dovecot/dovecot-sql.conf.ext

driver = mysql
connect = host=/var/run/mysql/mysql.sock dbname=mail user=mail password=password-here
default_pass_scheme = SHA512-CRYPT

These are the same credentials Postfix Admin will use, so keep that in mind. We’re not creating the database or user yet. That will come later. Now let’s add the password_query and user_query to /etc/dovecot/dovecot-sql.conf.ext:

password_query = \
  SELECT username as user, password, '/var/vmail/%d/%n' as \
  userdb_home, 'maildir:/var/vmail/%d/%n' as userdb_mail, \
  150 as userdb_uid, 12 as userdb_gid \
  FROM mailbox WHERE username = '%u' AND active = '1'

user_query = \
  SELECT '/var/vmail/%d/%n' as home, 'maildir:/var/vmail/%d/%n' \
  as mail, 150 AS uid, 12 AS gid, \
  concat('dirsize:storage=', quota) AS quota \
  FROM mailbox WHERE username = '%u' AND active = '1'

In the file /etc/dovecot/conf.d/10-auth.conf we will enable the SQL configuration file we just modified, disable plaintext authentication, and comment out the auth-system.conf.ext file that’s loaded by default.

disable_plaintext_auth = yes
auth_mechanisms = plain login
#!include auth-system.conf.ext
!include auth-sql.conf.ext

Since we are using TLS, it’s OK to use plain as the auth mechanism.

Next file to edit is /etc/dovecot/conf.d/10-mail.conf

mail_location = maildir:/var/vmail/%d/%n
mail_uid = vmail
mail_gid = mail
first_valid_uid = 150
last_valid_uid = 150

The SSL configuration is in /etc/dovecot/conf.d/10-ssl.conf as follows:

ssl_cert = </home/acme/
ssl_key = </home/acme/
ssl_min_protocol = TLSv1.1
ssl_curve_list = P-384
ssl_prefer_server_ciphers = yes

Let’s set up the file /etc/dovecot/conf.d/10-master.conf now. We need to uncomment the user, group, and mode lines in the unix_listener auth-userdb section of the service auth block in order to have Dovecot authenticate. Postfix will also need a unix_listener in the postfix spool directory so uncomment that section, and a unix_listener auth-master section too. Make sure to add postfix as the user and group. Don’t worry about creating them now, we’ll do that later. Set up the stats-writer as well. In the end the file should look somewhat like this

service auth {
  unix_listener auth-userdb {
    mode = 0666
    user = vmail
    group = mail

  unix_listener auth-master {
    mode = 0660
    user = vmail
    group = mail
  unix_listener /var/spool/postfix/private/auth {
    mode = 0666
    user = postfix
    group = postfix


service stats {
    unix_listener stats-reader {
        user = vmail
        group = mail
        mode = 0660
    unix_listener stats-writer {
        user = vmail
        group = mail
        mode = 0660

Dovecot Pigeonhole

A new addition to this guide is Dovecot Pigeonhole. This adds support for the Sieve language (RFC 5228) and the ManageSieve protocol (RFC 5804) to Dovecot. Coupled with a Roundcube plugin, this will allow us to filter email based on any number of factors. For example, you can create an rule in Roundcube that will automatically place email coming from to a Bank Notifications folder.

After you install the SlackBuild, copy the following example configuration files into the /etc/dovecot/conf.d directory:


We will need to edit the following Dovecot configuration files now:

First, edit /etc/dovecot/conf.d/20-lmtp.conf, and add

protocol lmtp {
  postmaster_address =
  mail_plugins = $mail_plugins sieve quota
  log_path = /var/log/dovecot-lmtp-errors.log
  info_log_path = /var/log/dovecot-lmtp.log

For /etc/dovecot/conf.d/15-lda.conf, add

protocol lda {
  postmaster_address =
  mail_plugins = $mail_plugins sieve quota
  auth_socket_path = /var/run/dovecot/auth-master
  log_path = /var/log/dovecot-lda-errors.log
  info_log_path = /var/log/dovecot-lda.log

For /etc/dovecot/conf.d/10-mail.conf, add

mail_home = /var/vmail/%d/%n/sieve
mail_location = maildir:/var/vmail/%d/%n

In /etc/dovecot/conf.d/20-managesieve.conf, add

protocols = $protocols sieve
service managesieve-login {
  inet_listener sieve {
    port = 4190
service managesieve {
  process_limit = 1024
protocol sieve {
  log_path = /var/log/dovecot-sieve-errors.log
  info_log_path = /var/log/dovecot-sieve.log
  managesieve_max_line_length = 65536
  managesieve_implementation_string = Dovecot Pigeonhole

Lastly, edit /etc/dovecot/conf.d/90-sieve.conf, and add

plugin {
    sieve = file:/var/vmail/%d/%n/sieve;active=/var/vmail/%d/%n/sieve/.dovecot.sieve
    sieve_default = /etc/dovecot/sieve/default.sieve
    sieve_global = /etc/dovecot/sieve/global/
lda_mailbox_autocreate = yes
lda_mailbox_autosubscribe = yes

Now we need to create some files that are needed for our configuration to work:

mkdir -p /etc/dovecot/sieve/global
chown -R vmail:mail /etc/dovecot/sieve/
touch /var/log/{dovecot-lda-errors.log,dovecot-lda.log}
touch /var/log/{dovecot-sieve-errors.log,dovecot-sieve.log}
touch /var/log/{dovecot-lmtp-errors.log,dovecot-lmtp.log}
chown vmail:dovecot /var/log/dovecot-*

Add postfix to the dovecot group. This is needed by amavisd-new later.

usermod -G dovecot -a postfix


Postfix is now the default MTA in Slackware as of November 2017. We no longer have to create users or compile anything so let’s just get to adding our database configuration.

Create a directory to hold these files

mkdir -p /etc/postfix/mysql

Now let’s create our MySQL map files


user = mail
password = mailpassword
hosts = localhost
dbname = mail
query = SELECT goto FROM alias,alias_domain
  WHERE alias_domain.alias_domain = '%d'
  AND alias.address=concat('%u', '@', alias_domain.target_domain)
  AND = 1


user = mail
password = mailpassword
hosts = localhost
dbname = mail
table = alias
select_field = goto
where_field = address
additional_conditions = and active = '1'


user = mail
password = mailpassword
hosts = localhost
dbname = mail
table = domain
select_field = domain
where_field = domain
additional_conditions = and backupmx = '0' and active = '1'


user = mail
password = mailpassword
hosts = localhost
dbname = mail
query = SELECT maildir FROM mailbox, alias_domain
  WHERE alias_domain.alias_domain = '%d'
  AND mailbox.username=concat('%u', '@', alias_domain.target_domain )
  AND = 1


user = mail
password = mailpassword
hosts = localhost
dbname = mail
table = mailbox
select_field = CONCAT(domain, '/', local_part)
where_field = username
additional_conditions = and active = '1'

Postfix Main Configuration

The only change between here and the past guide is that the append_dot_mydomain directive now defaults to no so we don’t need to include it. Here’s what should be in /etc/postfix/

myhostname =
myorigin = $myhostname
inet_interfaces = all
mynetworks = [::ffff:]/104 [::1]/128
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
header_checks = regexp:/etc/postfix/header_checks
mime_header_checks = regexp:/etc/postfix/header_checks
smtpd_banner = $myhostname ESMTP $mail_name
biff = no

Dovecot authentication section is still the same

smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
broken_sasl_auth_clients = no
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain =
smtpd_sasl_authenticated_header = yes

With newer versions of Postfix, you can now include both ECDSA and RSA certificates in your configuration. I’m disabling SSLv2, SSLv3, and TLSv1.0 in the configuration below. We’re excluding known insecure ciphers and setting the encryption level to may. You can set this to encrypt, if you want, but the Postfix documentation strongly advises against this for a public facing server.

lmtp_tls_ciphers = high
lmtp_tls_mandatory_ciphers = high
lmtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1
lmtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1
smtp_tls_ciphers = high
smtp_tls_loglevel = 1
smtp_tls_mandatory_ciphers = high
smtp_tls_mandatory_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, 3DES, DES+MD5, LOW, DSS, PSK, aECDH, EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CDC3-SHA, KRB5-DE5, CBC3-SHA
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1
smtp_tls_note_starttls_offer = yes
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1
smtp_tls_security_level = may
smtpd_reject_unlisted_recipient = yes
smtpd_reject_unlisted_sender = yes
smtpd_tls_auth_only = yes
smtpd_tls_cert_file= /home/acme/
smtpd_tls_eccert_file = /home/acme/
smtpd_tls_eckey_file = /home/acme/
smtpd_tls_eecdh_grade = ultra
smtpd_tls_key_file= /home/acme/
smtpd_tls_loglevel = 1
smtpd_tls_mandatory_ciphers = high
smtpd_tls_mandatory_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, 3DES, DES+MD5, LOW, DSS, PSK, aECDH, EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CDC3-SHA, KRB5-DE5, CBC3-SHA
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1
smtpd_tls_received_header = yes
smtpd_tls_security_level = may
smtpd_tls_session_cache_timeout = 3600s
tls_preempt_cipherlist = yes
tls_random_source = dev:/dev/urandom

Feel free to set these parameters to whatever fits your needs

delay_warning_time = 4h
maximal_queue_lifetime = 5d
minimal_backoff_time = 1000s
maximal_backoff_time = 8000s
message_size_limit = 20480000
smtp_helo_timeout = 60s
smtpd_recipient_limit = 16
smtpd_soft_error_limit = 3
smtpd_hard_error_limit = 12

Now we set up milters along with the Postgrey and OpenDKIM sockets

smtpd_helo_restrictions = permit_mynetworks, permit_sasl_authenticated, warn_if_reject reject_non_fqdn_hostname, reject_invalid_hostname, permit
smtpd_sender_restrictions = permit_sasl_authenticated, permit_mynetworks, warn_if_reject reject_non_fqdn_sender, reject_unknown_sender_domain, reject_unauth_pipelining, permit
smtpd_client_restrictions = reject_rbl_client, reject_rbl_client, reject_rbl_client
smtpd_recipient_restrictions = reject_unauth_pipelining, permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination, check_policy_service unix:/var/run/postgrey/postgrey.sock, permit
smtpd_data_restrictions = reject_unauth_pipelining
smtpd_relay_restrictions = reject_unauth_pipelining, permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination, check_policy_service unix:/var/run/postgrey/postgrey.sock, permit
smtpd_helo_required = yes
smtpd_delay_reject = yes
disable_vrfy_command = yes
mailbox_size_limit = 0
recipient_delimiter = +

We need to point Postfix to our database map files. Use the same UID and GID that was used for vmail user and the existing mail group from /etc/group

virtual_mailbox_base = /var/vmail
virtual_mailbox_maps = mysql:/etc/postfix/mysql/, mysql:/etc/postfix/mysql/
virtual_uid_maps = static:150
virtual_gid_maps = static:12
virtual_alias_maps = mysql:/etc/postfix/mysql/, mysql:/etc/postfix/mysql/
virtual_mailbox_domains = mysql:/etc/postfix/mysql/

The final part of the file will set up Dovecot and Amavis

virtual_transport = dovecot
dovecot_destination_recipient_limit = 1
content_filter = amavis-forward:unix:amavis/amavisd.sock

Postfix Master Configuration

In the file /etc/postfix/, we’ll set up SMTP with TLS on port 587. Most options can just be enabled by uncommenting them. Make sure you comment out the -o syslog_name=postfix/$service_name option right under relay.

smtp      inet  n       -       n       -       -       smtpd
submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_enforce_tls=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject_unauth_destination,reject
  -o smtpd_sasl_tls_security_options=noanonymous
  -o cleanup_service_name=subcleanup
  -o tls_preempt_cipherlist=yes

subcleanup unix n       -       -       -       0       cleanup
  -o header_checks=regexp:/etc/postfix/submission_header_checks

relay     unix  -       -       n       -       -       smtp
#       -o syslog_name=postfix/$service_name
#       -o smtp_helo_timeout=5 -o smtp_connect_timeout=5

maildrop  unix  -       n       n       -       -       pipe
  flags=DRXhu user=vmail argv=/usr/local/bin/maildrop -d ${recipient}

dovecot      unix   -        n      n       -       -   pipe
  flags=DRXhu user=vmail:mail argv=/usr/libexec/dovecot/dovecot-lda -f ${sender} -d $(recipient)  

For Amavis, just make sure you set the max number of processes it’s allowed to run. In this example, it’s 4 (the same as the nginx configuration).

amavis-forward      unix    -       -       -       -       4       lmtp
  -o lmtp_data_done_timeout=1200
  -o lmtp_send_xforward_command=yes
  -o disable_dns_lookups=yes
  -o max_use=20
amavis/amavis-accept unix    n       -       -       -       -       smtpd
  -o content_filter=
  -o local_recipient_maps=
  -o relay_recipient_maps=
  -o cleanup_service_name=cleanup
  -o smtpd_restriction_classes=
  -o smtpd_delay_reject=no
  -o smtpd_client_restrictions=permit_mynetworks,reject
  -o smtpd_helo_restrictions=
  -o smtpd_sender_restrictions=
  -o smtpd_recipient_restrictions=permit_mynetworks,reject
  -o smtpd_data_restrictions=reject_unauth_pipelining
  -o smtpd_end_of_data_restrictions=
  -o mynetworks=
  -o smtpd_error_sleep_time=0
  -o smtpd_soft_error_limit=1001
  -o smtpd_hard_error_limit=1000
  -o smtpd_client_connection_count_limit=0
  -o smtpd_client_connection_rate_limit=0
  -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_milters

Run the newaliases command to generate the aliases database file

cd /etc/postfix/

Amavis with ClamAV and SpamAssassin

We are going to be using amavsid-new as the interface bewtween Postfix, ClamAV and SpamAssassin. We need a ton of things for Amavis and SpamAssassin. You can go the SlackBuilds route or just use CPAN. I’m going to just go with CPAN this time around. First, install the zeromq, pyzor, unrar, arj, cabextract, lzop, nomarch, p7zip, libmspack, and GeoIP dependencies. This allows SpamAssassin and ClamAV to handle different compressed files as well.

cpan install CPAN
cpan install App::cpanminus
cpanm -f -v Log::Log4perl
cpanm -f -v Test::Deep Test::Base Test::YAML YAML Module::Signature Module::Build Test::Pod Test::Pod::Coverage Test::Perl::Critic inc::latest Encode::Detect Image::Info TimeDate Net::LibIDN Net::SSLeay Socket6 IO::Socket::IP IO::Socket::SSL IO::Socket::INET6 Crypt::OpenSSL::Bignum Crypt::OpenSSL::Random Crypt::OpenSSL::RSA Geography::Countries IP::Country Digest::SHA Digest::SHA1 Digest::HMAC HTML::Tagset HTML::Parser Test::LeakTrace Authen::NTLM Data::Dump LWP Net::CIDR::Lite PAR::Dist ExtUtils::MakeMaker ExtUtils::Install Net::HTTP WWW::RobotRules HTTP::Date File::Listing IO::HTML Encode::Locale LWP::Protocol::https LWP::MediaTypes HTTP::Message HTTP::Negotiate HTTP::Cookies HTTP::Daemon Bundle::LWP NetAddr::IP Net::Server Net::Ident MailTools Net::IP Net::DNS Net::DNS::Resolver::Programmable Mail::SPF Mail::DKIM Geo::IP Net::Patricia Convert::TNEF Convert::UUlib Convert::BinHex Archive::Zip IO::Stringy MIME::Tools Unix::Syslog BerkeleyDB IO::Multiplex Net::LibIDN File::LibMagic


Get the SpamAssassin SlackBuild and build it.

After SpamAssassin is installed, edit the /etc/spamassassin.conf file and set the following options. You only really need ENABLED but the rest are a good idea.

OPTIONS="--create-prefs --max-children 5 --helper-home-dir"

The SpamAssassin source no longer includes rules, so you’ll have to download them. Run sa-update to do this.


You’ll need a user and group created first.

groupadd -g 210 clamav
useradd -u 210 -d /dev/null -s /bin/false -g clamav clamav

Now go ahead and grab the SlackBuild. After you install, make sure DatabaseMirror is set to the new mirror in /etc/freshclam.conf. Next, edit the /etc/clamd.conf file and change theLocalSocket and LocalSocketGroup options

LocalSocket /var/run/clamav/clamd.sock
LocalSocketGroup amavis

Let’s install amavisd-new before we update the ClamAV virus definitions.


Create a user and group before you run the script.

groupadd -g 225 amavis
useradd -m -d /var/lib/amavis -s /bin/bash -u 225 -g 225 amavis

While we’re at it, go ahead and add the amavis user to the clamav group and vice versa.

usermod -G clamav -a amavis
usermod -G amavis -a clamav

Run the SlackBuild to install the package. Afer you install, uncomment the lines @bypass_virus_checks_maps and @bypass_spam_checks_maps at the top of /etc/amavisd.conf and add the following

@bypass_virus_checks_maps = (
   %bypass_virus_checks, @bypass_virus_checks_acl, $bypass_virus_checks_re);

@bypass_spam_checks_maps = (
   %bypass_spam_checks, @bypass_spam_checks_acl, $bypass_spam_checks_re);

Next, uncomment @lookup_sql_dsn and modify it to connect to your database using the Unix socket and the proper credentials. Amavis uses the DBD::mysql Perl module. The documentation states setting the host value to localhost will use the socket. This configuration will enable spam checking for the domains you’ve added to your database either manually or through Postfix Admin

@lookup_sql_dsn = (
$sql_select_policy = 'SELECT domain from domain WHERE CONCAT("@",domain) IN (%k)';

There are a number of configuration options we can change in /etc/amavisd.conf. The only really important one is the unix socket configuration. My recommendations are:

  • Set $max_servers to the same number of processes we allowed Amavis to use in /etc/postfix/, which in my case is 4
  • Set the $sa_tag_level_deflt opton to a large negative number. This will ensure that spam headers are added to every single email
  • Change the user and group to amavis that we created earlier
  • Configure the home directory for configuraiton files and quarantine emails
  • Set the domain name (not the same as your hostname).
  • Uncomment the amavis section in @av_scanners. Leave the rest of this section as is.
  • Comment out the $inet_socket_port line
  • Set the Unix socket for Amavis in $unix_socketname
  • Comment out forward_method for DKIM signing since we’ll use OpenDKIM for that
  • Set the pid_file to /var/run/amavis/

The end result should look something like:

$max_servers  = 4;
$sa_tag_level_deflt  = -9999;
$daemon_user  = 'amavis';
$daemon_group = 'amavis';
$mydomain = '';
$myhostname = '';
$MYHOME = '/var/lib/amavis';
$QUARANTINEDIR = "$MYHOME/virusmails";
$unix_socketname = "/var/spool/postfix/amavis/amavisd.sock";
$unix_socket_mode = 0660;
$pid_file  = "/var/run/amavis/";
$interface_policy{'SOCK'} = 'mysock';
$policy_bank{'mysock'} = {
   protocol => 'LMTP',
   auth_required_release => 0,
@av_scanners = (
   \&ask_daemon, ["CONTSCAN {}\n", "/var/run/clamav/clamd.sock"],
   qr/\bOK$/m, qr/\bFOUND$/m,
   qr/^.*?: (?!Infected Archive)(.*) FOUND$/m ],

Since we are putting the Amavis socket in the Postfix queue directory, we’ll need to create it and set the permissions

mkdir /var/spool/postfix/amavis/
chmod 770 /var/spool/postfix/amavis/
chown amavis:postfix /var/spool/postfix/amavis/
usermod -G amavis -a postfix

Use the following commands to let Postfix create the amavis-accept PID:

cd /var/spool/postfix/public
ln -s ../amavis amavis
cd /var/spool/postfix/pid
mkdir unix.amavis
chown root:root unix.amavis
chmod 700 unix.amavis

In order for the above to work, we need to set a $forward_method in /etc/amavisd.conf

$forward_method = 'smtp:/var/spool/postfix/amavis/amavis-accept';

If for any reason this is not working, you can edit the /usr/sbin/amavisd Perl script directly. Look around line 926 or search for 10025. Comment out the existing $forward_method and replace it with this:

$forward_method = $have_inet6 && !$have_inet4 ? 'smtp:[::1]:10025' : 'smtp:/var/spool/postfix/amavis/amavis-accept';

Go through the rest of /etc/amavisd.conf file and modify any settings you might want changed.

We also need to fix some permissions to get all three of these to play nicely.

chmod 775 /var/lib/spamassassin/
chown amavis:amavis /var/lib/spamassassin/
chown -R amavis:amavis /var/lib/spamassassin/
chown -R amavis:amavis /var/lib/amavis
chown -R clamav:amavis /var/lib/clamav

Now go ahead and update your virus database by running freshclam as root. Don’t worry if you get a message from freshclam saying clamd was not updated. This is because we have not started clamd yet.


Set up the user and group first, then run the SlackBuild to install Postgrey.

groupadd -g 301 postgrey
useradd -u 301 -d /var/lib/postgrey -s /bin/false -g postgrey postgrey

Go and edit the SlackBuild to set the values of POSTGREYUSR, POSTGREYGRP, POSTGREYUID and POSTGREYGID to the values you set earlier when you created them. After you install, you may want to get an updated version of postgrey_whitelist_clients from the Postgrey site and place it in /etc/postfix, replacing the one included with the SlackBuild.

We need to edit the /etc/postgrey.conf file and replace the PORT line with the path to the Unix socket we are going to set up. Set the correct HOST and make sure USER and GROUP are also correct.


In the /etc/rc.d/rc.postgrey script we are going to find the postgrey_start() function and edit the postgrey flags to make sure it uses a socket instead of TCP. We are basically changing --inet=$PORT to --unix=$SOCKET

postgrey_start() {
  echo "Starting postgrey milter:  /usr/bin/postgrey -d --inet=$PORT --pidfile=$PIDFILE --user=$USER --group=$GROUP --dbdir=/var/lib/postgrey --hostname=$HOST"
  mkdir -p $(dirname $PIDFILE)
  chown ${USER}:${GROUP} $(dirname $PIDFILE)

  /usr/bin/postgrey -d \
                    --inet=$PORT \
                    --pidfile=$PIDFILE \
                    --user=$USER --group=$GROUP \
                    --dbdir=/var/lib/postgrey \

The extracted source includes an init script, too. It’s in contrib/postgrey.init if you want to use it.

OpenDKIM, DNS and Building Trust

The setup up to this point should be pretty much complete and meet most people’s needs. Some mail servers are quite picky when it comes to receiving email. Gmail particularly doesn’t like when an email is not signed. The next section will walk you through signing your email with DomainKeys Identified Mail and setting up Sender Policy Framework. If you are using a hosting provider for your server, you will need to contact them and have them set up a PTR record for your IP address. This is also known as rDNS. Some mail servers will reject your email if the IP you are sending from does not point back to your domain name. In general, it should look something like this: PTR 600

If you’re hosting at home, you can try asking your ISP to set this up for you but it is unlikely they’ll want to. They may be willing if you purchase a static IP. You’ll also need to add MX records to your domain’s DNS records. You can add something like this MX 600 10

That’s assuming points to your mail sever’s IP and you want a priority of 10. You can ask your DNS provider to add these for you. Check that the record has propagated with host:

$ host -t MX mail is handled by 10


Install (libbsd), (opendbx), then grab my SlackBuild and install OpenDKIM. Set up the user and group first:

groupadd -g 305 opendkim
useradd -r -u 305 -g opendkim -d /var/run/opendkim/ -s /sbin/nologin -c "OpenDKIM Milter" opendkim

We’ll need to create the run directory as well:

mkdir -p /var/run/opendkim
chown opendkim:opendkim /var/run/opendkim

Add this to the /etc/rc.d/rc.opendkim script to have it created automatically

mkdir -p $(dirname $PID_FILE)
chown ${USER}:${GROUP} $(dirname $PID_FILE)

We are using MariaDB here so set the USE_MYSQL variable to yes and run the SlackBuild. I used a modified version of CentOS’s init script for rc.opendkim, but feel free to grab the one included in the source in the contrib/init/generic/ directory.

Once it’s installed, we’ll need to set up a basic configuration file. You can copy the sample one from opendkim/opendkim.conf.simple in the extracted source to /etc/opendkim and add the user and group we created earlier. Note that the SlackBuild already does this for you:

UserID opendkim:opendkim
KeyFile /etc/opendkim/keys/default.private

The KeyFile is default.private since this is what’s set in /etc/rc.d/rc.opendkim. The string default is called the selector. Feel free to use any selector you want. The selector is used to differentiate between multiple DKIM records for your domain.

You’ll notice my init script will automatically create some default keys for you in /etc/opendkim/keys and it also creates the directory if it doesn’t exist. We’re using Unix sockets in this guide, so let’s change a line in /etc/opendkim.conf:

Socket local:/var/run/opendkim/opendkim.sock
UMask 0002

We’ll generate a 2048 bit key with the opendkim-genkey command. You can try something stronger like 4096 bits, but RFC 6376 suggests it might not fit in a 512 byte DNS UDP response. See section 3.3.3 Key Sizes for more information.

opendkim-genkey -b 2048 -D /etc/opendkim/keys -s default -d

The selector is set with the -s parameter. You’ll end up with two files inside /etc/opendkim/keys/, default.private, which is your key, and default.txt which has a nicely formatted record you’ll need to add to your DNS zone. If you don’t manage your own DNS or have access to your zone file, simply copy the text starting with v=DKIM1 as a TXT record in whatever control panel your DNS provider uses. For the example above, this is what I got (truncated for demonstration):

"v=DKIM1; k=rsa;

You’ll need to wait a while before the DNS record propagates but once it does you can check it with dig

dig TXT

Sender Policy Framework

This basically consists of adding another TXT record to your DNS zone. There used to be an SPF type record but this was removed in RFC 7208. You’ll want to add something like this:

v=spf1 a mx ~all


We’ll set up MariaDB first (the scripts are still named rc.mysqld):

chmod +x /etc/rc.d/rc.mysqld
/usr/bin/mysql_install_db --user=mysql
/etc/rc.d/rc.mysqld start

Now create the user and database we’ll be using for Roundcube, Postfix Admin, and all other components.

GRANT ALL PRIVILEGES ON mail.* TO "mail"@"localhost" \
IDENTIFIED BY "password-here";

Roundcube and Postfix Admin

The same advice from my previous post regarding web based interfaces applies.

Postfix Admin

Not many updates to Postfix Admin this time. We still set public/ as the root directory in our nginx configuration above so just proceed with the installation as normal:

cd /var/www/
tar -xzf postfixadmin-3.2.4.tar.gz
ln -s postfixadmin-postfixadmin-3.2.4 postfixadmin
cd postfixadmin
mkdir templates_c
chown nginx:root templates_c

Make a copy of as config.local.php and make your changes there. The ones we need for this guide are

$CONF['configured'] = true;
$CONF['database_type'] = 'mysqli';
$CONF['database_user'] = 'mail';
$CONF['database_password'] = "password-here";
$CONF['database_name'] = 'mail';
$CONF['database_socket'] = '/var/run/mysql/mysql.sock';
$CONF['admin_email'] = '';
$CONF['encrypt'] = 'dovecot:SHA512-CRYPT';
$CONF['dovecotpw'] = "/usr/bin/doveadm pw";
$CONF['default_aliases'] = array (
    'abuse' => '',
    'hostmaster' => '',
    'postmaster' => '',
    'webmaster' => '',
    'virusalert' => ''
$CONF['domain_path'] = 'NO';
$CONF['domain_in_mailbox'] = 'YES';
$CONF['footer_text'] = 'Return to';
$CONF['footer_link'] = ';'
$CONF['password_expiration'] = 'NO';

Comment out the $CONF['database_host'] = 'localhost'; line because we are using sockets here.

This tells Postfix Admin to use Dovecot’s crypt() scheme for passwords and to connect to MariaDB using Unix sockets. Next, visit and complete the setup. Make sure you add your $CONF['setup_password'] obtained from setup.php to config.local.php. Do this before clicking on Add Admin.

Add Email Domains and Mailboxes

Log in to and head over to Domain List > New Domain. Fill in whatever works for you here to add a new domain, then head to Virtual List > Add Mailbox and create your first user. I set up as an email domain and as my first user. Doing this will generate the needed database schema that Postfix will use. Go ahead and play around with this and make sure your aliases are set up


The latest version of Roundcube is now 1.4.10. There is a cool new mobile friendly theme now, too. We’ve already set up the nginx configuration for Postfix Admin and Roundcube so now we just have to download their files and run the installers.

cd /var/www/
tar -xvzf roundcubemail-1.4.10-complete.tar.gz
ln -s roundcubemail-1.4.10 roundcubemail
cd roundcubemail
chown nginx:root logs/ temp/

We’ll need to create a database for Roundcube to use. Log in to *8MariaDB** with mysql and create it. Make sure you use a strong password.

CREATE DATABASE roundcubemail CHARACTER SET utf8 COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON roundcubemail.* TO "roundcube"@"localhost" \
IDENTIFIED BY "password-here";

Roundcube includes an SQL file that can create the necessary database structure for you

mysql -u roundcube roundcubemail -p < SQL/mysql.initial.sql

I recommend you check out the INSTALL file included in the source for a more complete guide on the installation. Now head over to and make sure everything is OK in the Check environment section. You will probably need to temporarily comment out the location blocks in /etc/nginx/conf.d/ that block access to Roundcube URLs such as /installer.

My installation complained about the date.timezone setting in php.ini so I had to set that. Get a list of supported time zones from here. You may need to restart nginx and php-fpm for the changes to take effect.

Click Next when you’re done to move on to the Create config section. You can leave most of these settings alone. If you want to know what a specific setting does, check out Roundcube’s wiki. Fill in the Database setup section with the user and database you created earlier.

IMAP and SMTP Settings

Fill in your database information in the Database Setup section. Make sure you do not enable spellchecking support. If you do, Roundcube will connect to external services to check your spelling. Why would we go through all this trouble to have a third party see every word we type?

We’re going to set default_host to tls://localhost in the IMAP Settings section and leave the default port. In the SMTP Settings section, set smtp_server to tls://localhost , smtp_port to 587 and check the Use the current IMAP username and password for SMTP authentication option. In the Plugins section, enable managesieve.

Click on Create config.

After it’s done generating your configuraiton file, go through config/ and see if you’d like to change anything. I recommend disabling SSL peer verification for IMAP and SMTP since we are using localhost and the SSL certificates likely won’t match the hostname. This is needed in PHP 5.6 or later and Slackware is on PHP 7.4 as of this writing.

$config['default_host'] = 'tls://localhost';
$config['imap_conn_options'] = array(
    'ssl' => array(
        'verify_peer'  => false,
        'verify_peer_name' => false,
$config['smtp_server'] = 'tls://localhost';
$config['smtp_conn_options'] = array(
    'ssl' => array(
        'verify_peer'      => false,
        'verify_peer_name' => false,
$config['log_dir'] = '/var/www/roundcubemail/logs/';
$config['temp_dir'] = '/var/www/roundcubemail/temp/';
$config['plugins'] = array('managesieve');
$config['enable_spellcheck'] = false;
$config['prefer_html'] = false;
$config['mime_param_folding'] = 0;

Start Up Email Services

Let’s start up the services we have so far in order to get all necessary files created properly. We’ll need the services running in order to set up Roundcube and Postfix Admin correctly. You can place your startup commands in /etc/rc.d/rc.local and the stop commands in /etc/rc.d/rc.local_shutdown.

Since now Dovecot and Postfix are part of Slackware, their start up scripts are run before /etc/rc.d/rc.local. My workaround here is just to issue restarts since Postfix depends on OpenDKIM to be running before starting. I didn’t think too much about how to solve this problem.

Make sure both scripts are executable and add this content


if [ -x /etc/rc.d/rc.php-fpm ]; then
        /etc/rc.d/rc.php-fpm start

if [ -x /etc/rc.d/rc.nginx ]; then
        /etc/rc.d/rc.nginx start

if [ -x /etc/rc.d/rc.postgrey ]; then
        /etc/rc.d/rc.postgrey start

if [ -x /etc/rc.d/rc.clamav ]; then
        /etc/rc.d/rc.clamav start

if [ -x /etc/rc.d/rc.spamd ]; then
        /etc/rc.d/rc.spamd start

if [ -x /etc/rc.d/rc.amavisd-new ]; then
        /etc/rc.d/rc.amavisd-new start

if [ -x /etc/rc.d/rc.opendkim ]; then
        /etc/rc.d/rc.opendkim start

if [ -x /etc/rc.d/rc.postfix ]; then
        /etc/rc.d/rc.postfix restart

if [ -x /etc/rc.d/rc.dovecot ]; then
        /etc/rc.d/rc.dovecot restart


if [ -x /etc/rc.d/rc.nginx ]; then
        /etc/rc.d/rc.nginx stop

if [ -x /etc/rc.d/rc.php-fpm ]; then
        /etc/rc.d/rc.php-fpm stop

if [ -x /etc/rc.d/rc.postgrey ]; then
        /etc/rc.d/rc.postgrey stop

if [ -x /etc/rc.d/rc.amavisd-new ]; then
        /etc/rc.d/rc.amavisd-new stop

if [ -x /etc/rc.d/rc.clamav ]; then
        /etc/rc.d/rc.clamav stop

if [ -x /etc/rc.d/rc.spamd ]; then
        /etc/rc.d/rc.spamd stop

if [ -x /etc/rc.d/rc.opendkim ]; then
        /etc/rc.d/rc.opendkim stop

Finishing Up

Go ahead and create some filters in Roundcube and make sure emails are going to the right folders. Test sending email to and from your own domain as well as to external domains. Connect your email clients. They should automatically detect your sever’s open ports but you can use the following settings to set it up manually. You can substitute the hostname for if that points to your server’s IP as well.

Incoming (IMAP) server:
Port: 143
Authentication: Plain (Normal Password)

Outgoing (SMTP) server:
Port: 587
Authentication: Plain (Normal Password)
Requires authentication

The first time you receive an email, it’ll be greylisted thanks to Postgrey. Check /var/log/maillog:

Apr 24 22:38:56 mail postgrey[945]: action=greylist, reason=new,, client_address=,,
Apr 24 22:38:56 mail postfix/smtpd[19527]: NOQUEUE: reject: RCPT from[]: 450 4.2.0 <>: Recipient address rejected: Greylisted, see; from=<> to=<> proto=ESMTP helo=<>

Most mail servers will try once more after some time and will be allowed the second time they send. Spammers generally only try once so this should stop some most of the common spam you could receive. Once the mail passes through, Amavis will check for viruses and other malware.

Apr 24 22:48:23 mail amavis[1078]: (01078-02) Passed CLEAN {RelayedInbound}, []:43126 [] <> -> <>

You can also test your anti-virus using the EICAR test file. Send yourself an email from another mail server with only the following string in the body:


It will be detected as a virus and you’ll see this in the log as well:

Apr 24 23:05:25 mail postfix/qmgr[1060]: BF9D558771: from=<>, size=2092, nrcpt=1 (queue active)
Apr 24 23:05:25 mail amavis[21120]: (21120-01) Blocked INFECTED (Eicar-Test-Signature) {DiscardedInbound,Quarantined}, []:36539 [] <> -> <>, quarantine: /var/lib/amavis/virusmails

In a similar fashion, you can test your spam filter using the GTUBE. Send a message from another mail server to yourself with only the following string in the body:


It will be detected as spam and you’ll see this in the log as well:

Apr 24 23:13:44 mail amavis[21119]: (21119-01) Passed SPAM {RelayedTaggedInternal,Quarantined}, []:36539 [] <> -> <>, quarantine: /var/lib/amavis/virusmails

Other Considerations

You’ll notice in your /var/log/maillog file that there will be a ton of bots and compromised third party servers trying to relay using your server, trying to log in using common usernames (admin, support, test, etc) and generally just trying to wreak havoc on your mail server. The setup suggestions I described in this article should prevent most of those attacks. You may consider trying something like fail2ban to automatically ban these IPs. Get yourself a nice firewall using AlienBob’s Easy Firewall Generator and block anything you don’t need.

I tested this set up by following it step by step on a fresh server and it worked for me. I may have missed some things. Contact me if you have any comments. Encrypted email is strongly encouraged and preferred.