Now this is a feature that caused me quite some headache. But in the end it was pretty simple. Quotas are size limits for users. You can make sure that users do not waste infinite amounts of disk space but are forced to clean up old emails every now and then.

The magic happens in two places. Postfix needs to reject new emails if the user’s mailbox is over quota. And Dovecot needs to keep track of the quota and how much the user has already used up of it.

Dovecot quota policy service

Let’s start with Dovecot. Find the file /etc/dovecot/conf.d/90-quota.conf and edit it. There are several “plugin {…}” sections. Take one and make it look like:

plugin {
  quota = maildir:User quota

  quota_status_success = DUNNO
  quota_status_nouser = DUNNO
  quota_status_overquota = "452 4.2.2 Mailbox is full and cannot receive any more emails"

The first line defines that you want to calculate the used space in a user’s maildir. There are several backends like that but the maildir is the best choice in this context. The string “User quota” is just some random string that may be queried from a mail user agent.

The lines starting with “quota_status_…” set return values for the service that will set up in a minute. It will tell Postfix that it will not interfere (DUNNO – colloquial way to say “don’t know”). And it will return a string with a return code 452 if the user is over quota. Codes starting with “4” mean temporary errors. It will tell the sending party that it is worth retrying at a later time. However if the user does not resolve the issue it will lead to a bounce error email after three days.

In the same file (90-quota.conf) add a new section:

service quota-status {
  executable = /usr/lib/dovecot/quota-status -p postfix
  unix_listener /var/spool/postfix/private/quota-status {
    user = postfix

This creates a new Dovecot service responding to requests from other processes. You surely recognize that we put it into the jail that Postfix runs in (/var/spool/postfix). So Postfix can access it.

Time to restart Dovecot:

systemctl restart dovecot

Take a look at the /var/spool/postfix/private directory. If all went as intended you will find a socket file called quota-status there. Otherwise please check the /var/log/mail.log file for errors.

Postfix recipient restrictions

If we stopped here then Dovecot would reject emails for users who have no space left. However Postfix would happily receive new emails and only later get rejected when talking to Dovecot via LMTP. It will then keep the email in its queue and retry for a while. In the end it will send a bounce back to the sender telling them about the problem. So why is this bad?

  1. The sender will assume that the email was delivered while it is stuck in the queue for up to three days.
  2. Spam emails use forged senders. So at the time that Postfix generates the bounce email it will likely send it to an innocent person. This is called backscatter and considered a mail server misconfiguration. Such a problem may get your mail server blacklisted. You don’t need that.

So the next logical step is to make Postfix check whether a mailbox is over quota whenever a new email arrives. Let’s hook up into the “RCPT TO” phase of the SMTP dialog when a new email comes in. Postfix checks its smtpd_recipient_restrictions configuration at this stage. Run this command in the shell:

postconf "smtpd_recipient_restrictions = \
     reject_unauth_destination \
     check_policy_service unix:private/quota-status"

This adds two checks:

  1. reject_unauth_destination checks whether the mail server is the final destination for the recipient’s email address. This is pretty much the default behavior if you do not define any restrictions.
  2. check_policy_service connects to the socket file at /var/spool/postfix/private/quota-status that was put there by Dovecot. It will use it to ask Dovecot whether the user is over quota in which case the email would get rejected.

Test it

If you are curious to see this working, then set John’s mailbox to just a few KB and send him several test emails using swaks. After a few emails you will see the rejection message:

-> RCPT TO:john@example.org
 <** 552 5.2.2 john@example.org: Recipient address rejected: Mailbox is full and cannot receive any more emails

Your users may complain that they have deleted many emails but are still over quota. Let them check if they actually emptied the Trash folder. Of course emails in that folder also contribute to the disk space usage. Once the Trash folder is expunged the problem should be gone.

Automatic warning emails

The last step is to inform the poor users if they accidentally went over quota. After all they do not necessarily reccognize that on their own. Let’s do that by sending them an email with a warning. Yes, we will make sure that the email gets through even if the quota is reached.

You still have the 90-quota.conf open in your editor? Good. Add this section to the file (derived from the Dovecot documentation):

plugin {
   quota_warning = storage=95%% quota-warning 95 %u
   quota_warning2 = storage=80%% quota-warning 80 %u
   quota_warning3 = -storage=100%% quota-warning below %u
service quota-warning {
   executable = script /usr/local/bin/quota-warning.sh
   unix_listener quota-warning {
     group = dovecot
     mode = 0660

This section defines three automatic quota warnings. The first (quota_warning) is triggered if the user reaches 95% of the quota. The second (quota_warning2) at 80%. And the third if the user had reached 100% but has removed enough emails to have gone below 100%. These lines follow this schema:

  • Trigger (e.g. “storage=95%”). The “%” sign needs to be used twice if you want to emit a literal percent sign. So this is not a typo.
  • The socket you want to call in that case. Our socket is the “service quota-warning” that calls a shell script.
  • Additional parameters that are passed to the shell script in our case. They tell the script the percentage that has been reached (e.g. 95) and the address of the user who should get the warning.

Apparently we need the script to run. So please create a new file at /usr/local/bin/quota-warning.sh and put these lines into it:

cat << EOF | /usr/lib/dovecot/dovecot-lda -d $USER -o "plugin/quota=maildir:User quota:noenforcing"
From: postmaster@webmail.example.org
Subject: Quota warning - $PERCENT% reached

Your mailbox can only store a limited amount of emails.
Currently it is $PERCENT% full. If you reach 100% then
new emails cannot be stored. Thanks for your understanding.

Make this file executable:

chmod +x /usr/local/bin/quota-warning.sh

Time to restart Dovecot again:

systemctl restart dovecot

Recalculate quota

If you directly remove files from a user’s Maildir instead of properly accessing the mailbox using IMAP then you will mix up the quota calculation. To force Dovecot to recalculate the quota you will have to run:

doveadm quota recalc -u john@example.org

Success stories

I would appreciate if you leave a comment here telling others if your mail server is running. Perhaps even a word about the purpose of the mail server and the number of users you have. That allows others to see that their time is well-spent and indeed leads to a working mail server. Thanks for your feedback.

Blocking malware

My usual rant: I consider virus scanners snake oil. If anyone wants to seriously infect a computer by sending an infected attachment then virus scanners won’t help you. They are based on patterns of known malware and (if you are lucky) a working heuristic algorithm trying to detect unusual behavior. It has been proven time and again that a custom-made virus – even a slightly altered variant of a well-known virus – goes easily undetected by your virus scanner. So why do they even exist? FUD. IT managers know well about the weaknesses of this approach. But if any Wind*ws computer gets infected they can tell senior management that they employed all common measures. I have talked to several managers and they all tell the same story. A german manager recently called yet another outbreak of Emotet a “digital natural disaster”. Nothing you could do against it, huh? Free hint: only allow known and verified types of attachments, educate your users and limit their access. The oletools can be integrated into rspamd to help with M$ office macros.

Pros and cons of ClamAV

As usual I prefer to stay with open-source components. So the obvious choice is the famous ClamAV virus scanner. But its detection rate is ridiculously low with its default signatures. Fortunately there are additional signatures to significantly improve that. And they is even a Debian package called clamav-unofficial-sigs that gets them. (Thanks for the hint, Pieter.)


Let’s get going and start ClamAV. First install the necessary package:

apt install clamav clamav-daemon clamav-unofficial-sigs

The daemon is a permanent process running on your system waiting for connections from other programs. That makes it faster than starting a process to scan email attachments time and again. The default configuration works well so there it nothing to do.

Check if the daemon is running already:

systemctl status clamav-daemon

Look for the “active” (running)” line in the output. Is it there? Very good.

Don’t worry about messages like…

WARNING: Your ClamAV installation is OUTDATED!
WARNING: Local version: 0.101.4 Recommended version: 0.102.1
DON'T PANIC! Read http://www.clamav.net/documents/upgrading-clamav

Do as it says: don’t panic. 🙂 Debian ships with a stable version and stays with it. That is perfectly fine. You do not need to do anything about it.

Let rspamd use ClamAV

Finally you have to tell rspamd how to talk to the ClamAV daemon. Unfortunately the documentation is pretty misleading. Unfortunately the principal developer once again acted childishly and refused to fix it. Oh, well.

The ClamAV daemon is by default creating a socket file on your server at /var/run/clamav/clamd.ctl. Sockets look like files and work as interfaces for interprocess communication. rspamd can communicate through that socket to ask ClamAV if a certain email contains a virus.

The default configuration can be seen in /etc/rspamd/modules.d/antivirus.conf. By default it will just raise the score if a virus was detected. I prefer to reject an email right away if ClamAV finds something. So let’s improve it a bit. Create a new file /etc/rspamd/override.d/antivirus.conf with this content to replace the default section:

clamav {
 symbol = "CLAM_VIRUS";
 type = "clamav";
 action = "reject";
 servers = "/var/run/clamav/clamd.ctl";

So what does this mean?

  • symbol… If a virus is found by ClamAV then rspamd adds a symbol named CLAM_VIRUS. You may remember that these symbols can be assigned scores or added as email headers to see which criteria matched. You can call it whatever you want.
  • type… sets a few defaults in the antivirus module that tell rspamd how to talk to ClamAV
  • action… is an optional parameter that we use to instantly reject the email if a virus is found. We do not add a value to the spam score. Instead we refuse to accept the email during the incoming SMTP connection.
  • servers… defines the way to communicate with ClamAV. In this case we define the path of the socket that the ClamAV daemon opened for us.

Restart rspamd:

systemctl restart rspamd

Virus scanning should now be working. Let’s see if it works as expected.


As mentioned earlier there exists a test signature for virus scanners called EICAR. It is not a virus – just a file that can be fed to your virus scanner to see if it is generally working. Download the test file from their website:

wget https://secure.eicar.org/eicar.com

If you installed the swaks tool as suggested earlier you can use it to send a test email with EICAR attached:

swaks --to john@example.org --attach - --server localhost < eicar.com

One of the last lines should read…

<** 554 5.7.1 clamav: virus found: \"Eicar-Test-Signature\"

This proves that Postfix send the email through rspamd which in turn checked the email using ClamAV which again found the test signature and rejected the email.

You can also test updating your signatures:

chown -R clamav.clamav /var/cache/clamav-unofficial-sigs /var/lib/clamav-unofficial-sigs/configs

If the update worked successfully you can now check which additional signatures ClamAV recognizes:

clamscan --debug 2>&1 /dev/null | grep "loaded"

It may take a few seconds but then it should show you the various signatures it loaded.

Could not resolve host: clamav.securiteinfo.com

The xxx package still uses a source that is not available any more. The issue is recorded as Ubuntu bug #1643172. This leads to daily error emails telling you that clamav.securiteinfo.com could not be resolved. To work around this issue edit the /usr/share/clamav-unofficial-sigs/conf.d/00-clamav-unofficial-sigs.conf file and remove the si_dbs and si_update_hours definitions.

Monitoring and backup

You have come a long way. Glad you made it. So many weird things complicated the journey that evolved from the very beginning of using email roughly 35 years ago. But before you take your much deserved break from these adventures let’s take care of some final tasks.


For me there is nothing more awkward than having your users tell me that my systems are not working properly. As a good sysadmin I should notice outages in advance. The most frequent cause for mail server issues is a disk that is running full. Users are pretty bad with cleaning their mess up. For them the space of their mailbox is infinite. So one way is to use quotas where possible. But at least you should get notified of error conditions on your mail server.

I will not dive into monitoring deeply here. Setting up proper monitoring will easily fill a guide similar to the size of the one you are currently reading. But you should either have your own monitoring system set up or at least use an internet service to check if emails are getting through.

Things I suggest you check frequently:

  • Is there enough space on disk? Especially /var/vmail?
  • Are the SMTP, Submission, POP and IMAP ports responding from other servers?
  • Is your mail server’s IP address blacklisted? There are several services you can use.
  • Are any emails coming through? (I just do a simple count like “grep -c relay= /var/log/mail.log“)
  • Numer of emails in the mail queue? (mailq)
  • Are your backups restoreable? A propos backups…


No matter if you are running your family mail server or are in charge for an organisation – nobody likes to lose email. There are many different backup tools for different purposes. Just to name my open-source favorites that ship with Debian:

  • rsnapshot. A simple solution if you want to send backups to other servers using SSH/SCP. You can also keep backups for different points in time on your local server. But if your disk crashes then all would be lost. Also rsnapshot can become slow with many emails because every file is copied as a link. But you can access the files directly without any restore procedure. It’s basically a frontend for rsync which is very reliable.
  • borg. This tools needs to be installed both on the client and the server. Setting up an archive (where backups live) on the server feels a bit weird. But you are rewarded by compression, encryption and deduplication. Also data is stored in chunks which takes less I/O than rsnapshot. It is very efficient.
  • restic. Similar to Borg in a way. It also offers deduplication, compression and encryption. It’s main advantage is to be able to store backups in S3 buckets. This makes it my favorite tool at the moment because I like off-site backups. And I’m not talking about holy-shit-S3-is-expensive backups that you get at Amaz*n, Micr*s*ft or G**gle. I use Wasabi which costs me $6 per TB per month. (No, they do not sponsor me. I just like them.) That makes them 5-10 times cheaper than other providers.
  • Bareos. Formerly known as Bacula until its author started to have crazy ideas and these guys forked it. This is a full-fledged system to handle many servers in parallel and store things on different media – for example LTO magnetic tapes in a big tape library.

If you cannot backup your entire disk then at least include these directories:

  • /etc (configuration)
  • /var/vmail (the actual emails)
  • /var/log (in case the system breaks you may be able to figure out what happened last)
  • /var/backups (for general backups)
  • /var/lib/rspamd/dkim/ (private part of the DKIM keys)

Also make sure to exclude:

  • /var/lib/mysql

Instead run…

mysqldump mailserver > /var/backups/mailserver.sql

…daily to get a proper database backup that can be restored later.

I also like to backup the output of these files because they help me figure out the specific layout of a server:

  • fdisk -l
  • lshw
  • lsb_release -a
  • dpkg –get-selections

If one of your users just accidentally deleted an email and wants it back then simply get the files from /var/vmail/domain/user/Maildir/… from the backup and put them into the original location. Dovecot will handle everything else. You don’t need to restart anything.

Firewalling and brute force mitigation

So many bad guys everywhere. Paranoia you say? Well, take a look at your mail log. While I was preparing this guide I could barely follow it. The mail server I used for testing was by far not ready and I already got visits by a bunch of foreign IP addresses trying to guess accounts of email users. Scoundrels constantly scan the internet for mail servers and do not tire to find a way to abuse them. Let’s do something about that before they actually guess email accounts successfully.

First install the nftables package:

apt install nftables

Basic firewall rules with nftables

Let’s begin by blocking all incoming network ports except those that are needed to run your mail server. Previous versions of Debian used the iptables tool to help you add firewall rules. Debian Buster comes with a newer version of the Linux kernel that is moving from iptables to nftables. The good news is that you can still use iptables commands. The bad news is that most tools do not yet support nftables. However one distant future day the old syntax will not work any more so let’s dive straight into the new syntax and use nftables.

First let’s make the iptables command use nftables. Run:

update-alternatives --config iptables

You will be able to choose between iptables-nft (the new stuff) and iptables-legacy (the old stuff). Go with iptables-nft. Now the iptables command will manage nftables rules. However you can explicitly use the iptables-legacy command if you want to access the old-school iptables rules.

So where would you store the firewall rules? The recommended way is to create a file /etc/nftables.conf and add rules there. Although the new syntax takes some getting used to, it is at least pretty readable. This is my default file for new mail servers:

#!/usr/sbin/nft -f
flush ruleset
table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;

    iifname lo accept
    ct state established,related accept
    tcp dport { ssh, http, https, imap2, imaps, pop3, pop3s, submission, smtp } ct state new accept

    # ICMP: errors, pings
    ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem, router-solicitation, router-advertisement } accept
    # ICMPv6: errors, pings, routing
    ip6 nexthdr icmpv6 counter accept comment "accept all ICMP types"

    # Reject other packets
    ip protocol tcp reject with tcp reset

Bear with me that I will not explain this file in-depth. Feel free to read more about nftables in the Debian wiki and the wiki of the nftables project.

To active these firewall rules use systemctl:

systemctl enable nftables
systemctl start nftables

If all worked as expected you can see that systemd loaded your rules:

systemctl status nftables

You should see “Active: active (exited)” as the status. It is normal that its status is “exited” because there is no permanent process running. The rules are applied just once and that’s it.

Although nftables has been there for quite a while many projects still do not support it. Fortunately an important tool to block off attackers does: fail2ban. The “#include” line shown in the file above refers to that.

Brute-force attacks

Let’s install fail2ban:

apt install fail2ban

This is a pretty little tool. It knows about a lot of patterns in your log files that show something harmful. The software runs in the background and follows the various log files. Each new line in the log is compared against regular expressions. If a match is found it blocks the IP address that was mentioned in the log. However by default fail2ban uses the deprecated iptables tool. Let’s make it use nftables, too.

Once again edit your /etc/nftables.conf file and at the end add this line:

include "/etc/nftables/fail2ban.conf"

Create that /etc/nftables directory…

mkdir /etc/nftables

…and in there create the fail2ban.conf file containing these lines:

#!/usr/sbin/nft -f

table inet fail2ban {
  chain input {
    type filter hook input priority 100;

Why not ‘table ip’?

As the firewall just needs to block IP addresses you might be tempted to use ‘table ip’ instead of ‘table inet’. However ‘ip’ only deals with IPv4 packets. You would have to manage ‘ip’ and ‘ipv6’ tables. So I am just going with ‘table inet’ here which is more versatile.

Restart nftables

systemctl restart nftables

…and you should see a new table called fail2ban if you run:

nft list tables

Very nice. fail2ban just needs a little fine-tuning to use that table. Create a new file /etc/fail2ban/action.d/nftables-common.local that overrides the settings in the nftables-common.conf file and write:

nftables_table = fail2ban
blocktype = drop

What it means…

  • nftables_table = fail2ban
    (That is the name of the nftables table you just created. There fail2ban will add IP addresses to be blocked.)
  • blocktype = drop
    (This way we just ignore attackers. Packets from them are thrown away instead of answered with an ICMP response.)

Create another file /etc/fail2ban/jail.local with…

banaction = nftables-multiport
chain     = input

If you are curious and would like to get informed about every attacker that was blocked you may add “action = %(action_mwl)s” that also gets WHOIS information about the attacker. However this gets boring quickly and often WHOIS information is censored anyway – especially from organisations that are home to spammers.

By default an IP address gets blocked for 10 minutes (bantime) if more than 5 (maxretry) failures happened within 10 minutes (findtime). Check the jail.conf file for more information and feel free to add your preferred values to your jail.local file.

Also by default only failed login attempts using SSH are detected. There are lots of filters for other events and you just need to enable them. This once again goes into your jail.local file. I suggest you add these lines:

enabled = true

enabled = true
port    = pop3,pop3s,imap2,imaps,submission,465,sieve

enabled = true

Why the port definition for [dovecot]?

You may wonder why in the example above I suggest defining port in the dovecot section. The reason is Debian bug #867374. The default configuration calls the service “imap” instead of “imap2” and that leads to an error because there is no “imap” service defined in /etc/services.

After all these changes it is a good idea to check if your fail2ban configuration is still valid:

fail2ban-server -t

Is fail2ban happy? Great. Restart fail2ban after your last change:

systemctl restart fail2ban

To see what fail2ban did take a look at /var/log/fail2ban.log. This is what it looked like after a few minutes:

fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:43
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:43
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:44
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:46
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:46
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:48
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:49
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:50
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:54
 fail2ban.filter  : INFO    [dovecot] Found - 2019-12-27 20:51:09
 fail2ban.filter  : INFO    [dovecot] Found - 2019-12-27 20:51:29
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:51:33
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:51:35
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:51:35
 fail2ban.actions : NOTICE  [sshd] Ban
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:51:37
 fail2ban.filter  : INFO    [dovecot] Found - 2019-12-27 20:51:42
 fail2ban.filter  : INFO    [dovecot] Found - 2019-12-27 20:52:03
 fail2ban.filter  : INFO    [dovecot] Found - 2019-12-27 20:52:18
 fail2ban.actions : NOTICE  [dovecot] Ban

Status please

fail2ban comes with a handy shell command called fail2ban-client. To see which jails (=where to look for attackers) are active:

$> fail2ban-client status
|- Number of jail:    4
`- Jail list:    apache-auth, dovecot, postfix, sshd

So all four expected jails are active. Do you want to know which IP addresses are banned for failed SSH login attempts?

$> fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Currently failed:    3
|  |- Total failed:    13
|  - File list:    /var/log/auth.log - Actions
   |- Currently banned:    4
   |- Total banned:    5
   `- Banned IP list: 2a03:4000:1c:3f0::1

Yes, it works for IPv6, too. 🙂

Banning magic

Do you wonder how that banning actually works? fail2ban just adds new rules to your host’s firewall. Take a look at the fail2ban table you created earlier:

$> nft list table inet fail2ban
table inet fail2ban {
  set f2b-sshd {
    type ipv4_addr
    elements = {,, }

  set f2b-dovecot {
    type ipv4_addr

  set f2b-sshd6 {
    type ipv6_addr
    elements = { 2a03:4000:1c:3f0::1 }

  chain input { … }

nftables is pretty efficient with handling multiple IP addresses. It uses sets which are basically lists of IP addresses. iptables used to have the ipset functionality for that purpose. You can see in the output that it uses sets it names “elements”.

Look at the current status of a jail:

fail2ban-client status postfix-ispmail

It will show something like:

Status for the jail: postfix-ispmail
|- Filter
| |- Currently failed: 1
| |- Total failed: 168
| `- File list: /var/log/mail.log
`- Actions
 |- Currently banned: 14
 |- Total banned: 36
 `- Banned IP list: …

Grab a bag of popcorn and enjoy how your mail server grows its list of blocked idiots. 🙂

Managing users, aliases and domains

Maybe you already know what you have to do to create mail domains and mail users. After all I tried to explain the database schema in the section that dealt with preparing the database. But if that wasn’t clear enough let me explain what you need to do to manage your mail accounts.

Web interfaces

If you don’t like using SQL queries to manage your mail server you may like to install a web-based management software. Several developers contributed web interfaces for earlier versions of this guide and they will probably still work. The only change in the database schema is the quota field in the virtual_users table. I assume that the tools will add support for it. However you should be able to develop your own management system or integrate the mail server into your own system easily. I have documented a few common SQL queries further down.

These web interfaces have been provided to you by other diligent hands:

ISPmail Admin

Homepage: http://ima.jungclaussen.com/
Demo: http://ima.jungclaussen.com/demo/


GRSoft Virtual Mail Manager

Peter Gutwein has updated his PHP-based web interface to create strong password hashes as recommended in this Jessie guide. It supports english, german, spanish, french, italian, russian and swedish.

Homepage -> http://www.grs-service.ch/pub/grs_mminstallation.html

Here are screenshots of the working application:


Managing the database directly

Common tasks / PHPMyAdmin

This table explains what changes are required in the database for your everyday tasks. You can click your way through PHPMyAdmin following the instructions in this table:

Create a mail domainInsert a new row into the virtual_domains table and set the “name” to the name of the new domain.
Delete a mail domainDelete the row from the virtual_domains table that has the right “name”. All aliases and users will automatically be deleted, too. However the mailboxes will stay on disk at /var/vmail/… and you need to delete them manually.
Create a mail userFind out the “id” of the right domain from the virtual_domains table. The insert a new row into the virtual_users table. Set the domain_id to the value you just looked up in the virtual_domains table. Set the “email” field to the complete email address of the new user. Create a new password in a shell using the “dovecot pw -s BLF-CRYPT” command and insert the result into the “password” field.
Change the password of a userFind the row in the virtual_users table by looking for the right “email” field. Create a new password in a shell using the “dovecot pw -s BLF-CRYPT” command and insert the result into the “password” field.
Delete a mail userFind the row in the virtual_users table by looking for the right “email” field and delete it. The mailbox will stay on disk at /var/vmail/… and you need to delete it manually
Create a mail forwardingYou can forward emails from one (source) email to other addresses (destinations) – even outside of your mail server. Find out the “id” of the right domain (the part after the “@” of the source email address) from the virtual_domains table. Create a new row in the virtual_aliases table for each destination (if you have multiple destination addresses). Set the “source” field to the complete source email address. And set the “destination” field to the respective complete destination email address.
Delete a mail forwarddingFind all rows in the virtual_aliases table by looking for the right “source” email address. Remove all rows that you lead to “destination” addresses you don’t want to forward email to.

SQL queries

Create a mail domainINSERT INTO virtual_domains (name) VALUES (“example.org”);
Delete a mail domainDELETE FROM virtual_domains where name=’example.org’;
Create a mail userINSERT INTO virtual_users (domain_id, email, password) VALUES (SELECT id FROM virtual_domains WHERE name=’example.org’), ‘john@example.org’,'{BLF-CRYPT}$2y$05$.We…’;
Change the password of a userUPDATE virtual_users SET password='{BLF-CRYPT}$2y$05$.We…’ WHERE email=’email@address’;
Delete a mail userDELETE FROM virtual_users WHERE email=’john@example.org’;
Create a mail forwardingINSERT INTO virtual_aliases (domain_id, source, destination) VALUES ( (SELECT id FROM virtual_domains WHERE name=’example.org’), ‘melissa@example.org’, ‘juila@example.net’);
Delete a mail forwardingDELETE FROM virtual_aliases WHERE source=’melissa@example.org’;

Prevent spoofing using DKIM

Email sender spoofing is the act of pretending to be in control of someone else’s email address. This is a common problem with phishing. Often scammers send emails with a sender address of something@paypal.com and hope that the recipient falls for it and trusts them. In fact SMTP does not care which sender address you send. Many mail service providers enforce that you send emails only using your own email address. But some do not. And spammer and scammers obviously could not care less.

Spoofing case without DKIM

So a new method was conceived that added a cryptographic signature to the header of an email that the recipient could check to verify the authenticity of the sender and the integrity of the email. The signature is created using a private key that only the sending mail server has. It can then be verified by the recipient by downloading the corresponding public key from the DNS zone of the sending domain and running a signature check. This works very similar to PGP or S/MIME signing – just on a domain level. Your mail server can sign all outgoing emails automatically. The method used nowadays is called Domain Keys Identified Mail – or short: DKIM.

Let’s take an example. I have just sent an email from GMail to my personal email account on my own mail server. Google uses DKIM signing so the email got this additional header from Google’s mail servers:

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=gmail.com; s=20161025;

I need Google’s DKIM public key to verify that signature. It is stored in their DNS zone as a TXT record of “20161025._domainkey.google.com”. The “20161025” is the key selector that is mentioned in the signature as “s=20161025“. You can use any number of keys as long as you create the signatures with the matching private key.  The “_domainkey” part is the standard subdomain for DKIM keys. So let’s get that TXT record:

dig +short 20161025._domainkey.google.com txt

This returns…

"k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXNZF1j8sJPDleRjf9SPBNem0ik58kF1ilC1nUgKAttl9v7FX9hXJXPmLNhVtSKVZ8yruaeOZLeIxtgtk1s81zzIE5Mj0AiGn2wlFt4kYfqlDfYe95YLQHjynu4i7vj1Tj" "ksf62btcCbL+3XhbK+oD5PlqYhXHWuzoKoEp5L4lCihgkONvU/oy7NNeE6quqfF/y0YSLwF2WVA2Kd8L6R0Ar2dYT/3wZCFknI7xhvPqh9HNcIWBELGPwtXcsHbX1wvBlCgNQAUcdJrf2YWzAwqmZ564/1ipL1IMk1nafPJk75ktumVNz6ORuIn3jbZWp9rRpnaeI9cu/8KfSKH2EY9QIDAQAB"

That is the public key that I can use to verify the signature. An automated check can be done using the “opendkim-testmsg” tool as described later. I can run it and paste the entire email including headers and body into it. If it doesn’t complain with an error message then the signature is correct.

Sounds good? Then let’s implement that for your email domain, too.

Creating a keypair

As explained above you need a private key that your mail server will use and a public key that gets added to your DNS zone. rspamd can create DKIM keys already. You may want to install “dig” though which allows to query DNS records. It works like “nslookup” but is more versatile.

apt install dnsutils

rspamd has its built-in DKIM signing module enabled by default. If you put your key file into /var/lib/rspamd/dkim/ using a certain naming scheme it will pick it up automatically. Create that directory to store keys in:

mkdir /var/lib/rspamd/dkim
chown _rspamd:_rspamd /var/lib/rspamd/dkim

Create your keypair:

rspamadm dkim_keygen -d example.org -s 2019122401

The selector (-s) I chose is 2019122401 because that’s the day I created it. The first (01) key on 2019-12-24. It doesn’t matter though – you can name it anything you want. If you are lazy and unconcerned you could even use “dkim” as the selector and later spare some work by not needing DKIM maps that define which key is supposed to be used for each domain. “dkim” is the default selector if you do not use maps. However if you need to replace your key later without invalidating previously sent emails you are in trouble. So I recommend you rather use maps as explained further below. It gives you more flexibility and is pretty easy to do.

The output will look like this:

2019122401._domainkey IN TXT ( "v=DKIM1; k=rsa; "    "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCZ8e4RtsvuvsgXyo3Jr63twdx3Zm2RtnYPf87wba5ZnFP0sQIagjJ0R3WL+djKhM5EteBeboTeaflLR+AUKWJdQnL5jzGil+yWThH5C+ixhkdFZPdabkxBji+3r6uRxJKBsN0gy2/+Gp4sGs7lmFgioki9OLgQnzzFh2nE/MSACwIDAQAB" ) ;

The first part is the private key. And that includes the “…BEGIN…” and “…END…” lines. This key must be kept secret and will only be used by your mail server to sign outgoing emails.

The second part is the DNS record you need to add to your domain’s DNS zone. Let’s start with that.

Adding the DNS record

Before you start signing your emails you must make sure that the public key is properly present in your DNS zone for the domain you are sending emails from. Otherwise the recipient will be unable to verify the signature and may incorrectly assume that the email was spoofed.

Take a look at the TXT record. It will look something like this:

2019122401._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; " "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxenHupkYLPmFMbJjV9dQICKUl2xH/aexSRUwCuw7TJ9dkddqIN+6tyw4VKhnW8R0/UlbzlSFLmVgMU0uUkwTtVqyDHhtSU7LV/SkVYmUst4dTUF1r+8PvhAm7vobMYKdwvRsOq27ABtZc8P4oU2XXHqqa6LU8s4sNxs12hLW9swIDAQAB" ) ;

If you are running your own DNS server you should be able to copy this entire file and put it into your DNS zone. However if your internet provider offers you just a web interface to manage your domains then create a new TXT record with a host name of “2018022301._domainkey” in your domain and put the string within the double-quotes into it as the value. In my example:

2019122401._domainkey ➠p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxenHupkYLPmFMbJjV9dQICKUl2xH/aexSRUwCuw7TJ9dkddqIN+6tyw4VKhnW8R0/UlbzlSFLmVgMU0uUkwTtVqyDHhtSU7LV/SkVYmUst4dTUF1r+8PvhAm7vobMYKdwvRsOq27ABtZc8P4oU2XXHqqa6LU8s4sNxs12hLW9swIDAQAB

Be aware that the string you got contains two strings “…” + “…” that must be merged into one to work. There must usually not be any quotes in the record data. Depending on your ISP it may take a while until the new record is visible on the internet. You can use dig to verify that:

dig +trace 2019122401._domainkey.example.org txt

If you get the TXT entry like as follows then you are ready to enable DKIM signing in rspamd for that domain:

TXT "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxenHupkYLPmFMbJjV9dQICKUl2xH/aexSRUwCuw7TJ9dkddqIN+6tyw4VKhnW8R0/UlbzlSFLmVgMU0uUkwTtVqyDHhtSU7LV/SkVYmUst4dTUF1r+8PvhAm7vobMYKdwvRsOq27ABtZc8P4oU2XXHqqa6LU8s4sNxs12hLW9swIDAQAB" "" from server foo.bar in 24 ms.

Enabling DKIM maps in rspamd

As explained above it is advised to use DKIM maps. It’s nothing fancy. Just a simple file defining which selector you want to use for a certain domain. rspamd will assume that your selector is always “dkim” unless specified otherwise in a map. If you used “dkim” then you may get into trouble when you later want to replace your key. DNS is a sluggish system and propagating a new DKIM public key may take a day. Emails signed with a newer key may get rejected while the DNS record is not yet known everywhere in the world.

Using maps is simple. First we need to change the selector_map setting of the dkim_signing module. To do that create a new file in /etc/rspamd/local.d/dkim_signing.conf and make it contain just these two lines:

path = "/var/lib/rspamd/dkim/$domain.$selector.key";
selector_map = "/etc/rspamd/dkim_selectors.map";

The configuration is pretty self-explaining. rspamd will look for the domain-to-key mapping in the dkim_selectors.map file. Create that file and make it contain this line:

example.org 2019122401

That’s all really. rspamd now knows that whenever it sees an outgoing email from anyone@example.org it will get the DKIM private key from /var/lib/rspamd/dkim/example.org.2018022301.key and use it to sign the email.

Reload the configuration:

service rspamd reload

This method works well if you just have a few domains that virtually never change. If you are rather serving random customer domains you should consider putting the keys into a Redis database instead as described in the documentation. There is not yet a way to manage DKIM keys in a database like MySQL.


Selectors may seem unnecessary. But you will need them once you want to replace your current signing key for any reason. In that case you need to generate a new key using a different selector/name. Now that both DNS records are online you can make your mail server use the new signing key. You should leave the old key in place for a while.

Adding the domain key to rspamd

Take the private key that was created earlier (the multi-line string including “…BEGIN PRIVATE KEYS…” and “…END PRIVATE KEY…”) and put it into a file at the location where rspamd will look for it:


The name of the file has to be  DOMAIN + dot + SELECTOR + “.key” like above. If you name the file incorrectly you will get an error in your rspamd.log file like “lua_dkim_sign_handler: cannot load dkim key /var/lib/rspamd/dkim/example.org.dkim.key“.

Make sure that only _rspamd can read it:

chown _rspamd /var/lib/rspamd/dkim/*
chmod u=r,go= /var/lib/rspamd/dkim/*

rspamd will automatically pick up the files and does not need to be restarted.

Send a test email

If you have another email account at another location then you could just send a test email there via your mail server. If you take a look at the received email it should have a DKIM header now like:

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.org;
	s=2019122401; t=1519400838;
DKIM signature verification failed

To verify the signature install the opendkim-tools package, copy the entire test email (including headers and body), run opendkim-testmsg in your shell and paste the email (finish with CTRL-D).

If you get no output then the signature verified correctly. But if you get something like “opendkim-testmsg: dkim_eom(): Unable to verify” then double-check your DNS record.

You can also use websites like dkimvalidator.com, isnotspam.com or mail-tester.com service to verify that your signatures are working well.


Adding DKIM signatures is a good first step. But you can take it further by telling receiving mail servers that they should not accept any email from your domain without a valid signature or from servers that you do no operate. There are two concepts that aim to help. The older SPF and the newer DMARC. Either of them means creating a machine-readable string in a predefined format and adding a TXT record to your DNS zone. Receiving mail servers can check those records and take your advice (as the domain owner) what to do if the criteria of the email are not met. It could accept the email anyway or flag it as spam or reject it altogether.

Let’s take a look at a typical SPF record:

"v=spf1 ip4: mx ~all"

What it means:

  1. this is an SPF record of version 1 of the standard (there is currently no other version)
  2. please accept emails from the IP address
  3. alternatively accept emails from any server that is mentioned in our domain’s MX record (the server(s) that receive email for your domain)
  4. any other email should be considered suspicious – it might be spam or worse

There are websites like SPFwizard that help you create your SPF string to add to your DNS domain. Keep in mind though:

  • You should know which mail servers send email from your domain. Do not forget to include mailing list or newsletter services that send in your name.
  • Start with “~all” to mark emails as spam that do not meet the criteria. If all goes well switch to “-all” after a few weeks if you like.
  • Note that forwarding emails from your domain may break SPF because suddenly the email appears to be coming from an IP address that is not authorized. This has been a common problem for mailing lists and is gradually being fixed by resending the email from the domain of the mailing list service.

I mentioned that DMARC is the newer standard. So why use SPF anyway? Because some email providers value your effort if you use SPF, too. Technically it’s sufficient to specify a DMARC entry. In my opinion restricting the IP addresses allowed to send is a little dangerous and a little inflexible. It is far more interesting to require that emails from your domain have a valid DKIM signature. Such a record may roughly look like:

"v=DMARC1; p=reject; adkim=s"

However to create a proper DMARC entry I suggest you use one of the web sites that aid you there and explain the restrictions and extra features.

Mail client auto-configuration

Have you ever worked with Thunderbird as a mail client? When you add a mail account you get a wizard like this:

You click on Continue and Thunderbird tries to find out which parameters are needed to connect to your mail server properly. But usually you just end up with this:

What is necessary to help Thunderbird (and other mail clients like Evolution, Kmail, Kontact or even Outl**k) with that? Unfortunately there is no standard for it yet. Thunderbird has a couple of methods implemented.

For large ISPs they maintain their ISPDB database. But your market share may not be enough to be listed there. The next best option is to provide an XML file in a certain format through your web server. Thunderbird will look for this URL:


The syntax of the XML file is described here and here. A simple configuration file looks like this:


I suggest putting this file into /var/www/html/autoconfig-mail/config-v1.1.xml and editing your Apache virtual host configuration (/etc/apache2/sites-enabled/webmail.example.org-https.conf) slightly:

Alias /.well-known/autoconfig/mail /var/www/html/autoconfig-mail

Try to load that URL /.well-known/autoconfig/mail/config-v1.1.xml from your domain in your web browser. At least Firefox and Chrome-based browsers validate the XML content automatically and tell you if you made a syntax mistake. No errors? Very good.

Now next time you set up an account using Thunderbird (or other mail clients that support auto-configuration this way) your users will have a very pleasant quick way to add their email account. Try it out.

Keep in mind that every domain you are hosting requires such an auto-configuration file. I’m confident that you find a way to generate those files automatically. 🙂

DNS records

MX entries

So now you have your working mail server. But how do other mail servers get to know you? The answer lies in the most important service on the internet: DNS. Assume that a mail server somewhere on the other end of the internet wants to send an email to john@example.org. The server must find out the name (and IP address) of the mail server responsible for the example.org domain. This is how that works:


The remote server queries its DNS server for the MX (Mail eXchanger) record of the “example.org” domain. (If no MX record was found it tries again and asks for A (address) records. That’s a fallback solution.) Let’s run a query for a real domain to get an idea. Let’s ask which servers receive email for …@gmail.com.

$> host -t MX gmail.com
gmail.com mail is handled by 10 alt1.gmail-smtp-in.l.google.com.
gmail.com mail is handled by 20 alt2.gmail-smtp-in.l.google.com.
gmail.com mail is handled by 30 alt3.gmail-smtp-in.l.google.com.
gmail.com mail is handled by 40 alt4.gmail-smtp-in.l.google.com.
gmail.com mail is handled by 5 gmail-smtp-in.l.google.com.

So as a result we get 5 different MX records. Each of them consists of a priority and the host name of the mail server. A mail server would pick the entry with the highest priority (=the lowest number) and establish an SMTP connection to that host. In this example that would be the priority 5 server gmail-smtp-in.l.google.com. If that server could not be reached then the next best server with priority 10 would be used and so on. So all you have to do in your own DNS zone is add an MX entry pointing to your mail server. If you want to run a backup mail server (which is outside of the scope of this tutorial) then you can add a second entry with a an equal or lower priority.

A mistake some people make is using an IP address in MX records. That is not allowed. An MX record always points to a host name. You will have to add an A record for your MX record to point to the actual IP address of your mail server.

For science

In the above example it is very unlikely that a mail server will ever have to use the server with priority 40. Adventurous system administrators can add such a low-priority entry and see who connects to it. An interesting observation is that spammers often try these servers first – hoping that it is just for backup purposes and less restrictive or lazily configured than the main server. If you see someone connecting to the lowest-priority address first without having tried a higher-priority mail server then you can be pretty certain that it’s not a friend who’s knocking at your door.

Fallback to A entries

It’s best to explicitly name mail servers explicitly for your domain in the MX records. If you can’t do that for whatever reason then the remote mail server will just do an A record lookup for the IP address and then send email there. If you just run one server for both the web service and the email service then you can do that. But if the web server for your domain is located at another IP address than your mail server then this won’t work.

Filtering out spam with rspamd

You have come a long way in this guide and your mail server is already fully functional. Now it’s time to deal with the dark side: spam. The amount of spam nowadays has been insane for many years. So we need to detect spam emails and filter them out. I found that rspamd is a well-performing choice for that purpose. rspamd keeps a permanent process running on your mail server that listens to connections from Postfix using the milter (=mail filter) protocol. Every time an email enters your system, Postfix will send it to rspamd to have its content checked. rspamd runs a lot of checks on the email and computes a total score. The higher the score – the more likely the email should be considered unsolicited.

Make Postfix use rspamd

Let’s tell Postfix to send all incoming email through rspamd. Run these commands on the shell:

postconf smtpd_milters=inet:
postconf non_smtpd_milters=inet:
postconf milter_mail_macros="i {mail_addr} {client_addr} {client_name} {auth_authen}"

Postfix will connect to rspamd that is listening to TCP port 11332 on localhost ( and pass the email over that connection. The smtpd_milters setting defines that connection for emails that came into the system from the internet via the SMTP protocol. The non_smtpd_milters setting is optional – it makes Postfix have all emails checked that originate from the system itself. Finally the milter_mail_macros setting defines several variables that rspamd expects for better spam detection. rspamd then runs its checks and tells Postfix whether the email should pass or get rejected.

Testing spam detection

For testing we can use a sample spam email that comes with SpamAssassin. It is called GTUBE (Generic Test for Unsolicited Bulk Email). It contains a certain articial pattern that is recognized as spam by SpamAssassin. Do you know EICAR.COM to test virus scanners? This is the same thing for spam.

I suggest that you download the file on the server:

wget http://spamassassin.apache.org/gtube/gtube.txt

…and send that email to our test user John…

sendmail john@example.org < gtube.txt

Check your /var/log/mail.log. You will find something like this:

Jan 13 09:21:01 mail postfix/cleanup[19945]: 95B7A42212: milter-reject: END-OF-MESSAGE from localhost[]: 5.7.1 Gtube pattern; from=<root@mail.example.org> to=<john@example.org>
Jan 13 09:21:01 jen postfix/cleanup[19945]: 95B7A42212: to=<john@example.org>, relay=none, delay=0.18, delays=0.18/0/0/0, dsn=5.7.1, status=bounced (Gtube pattern)

The interesting parts are those printed in bold letters. “milter-reject” tells that the milter (rspamd) recommended to Postfix to reject the email. It gave the reason “5.7.1 Gtube pattern”. In mail communication you often find these three digit codes. They are defined in RFC 3463. The first digit is most important:

  • 2 = Success
  • 4 = Transient failure (temporary problem – come back later)
  • 5 = Permanent failure (do not try again in this way)

So 5.7.1 tells us that the result code is a permanent failure in delivery. If you looked up the RFC you would find that the 7 stands for an issue regarding the security policy. So it’s not a technical failure but instead a security-relevant component on the system has rejected the email. That’s what rspamd did. It even told us the reason: “Gtube pattern”. So you see that rspamd knows about the Gtube spam test pattern and reacts as expected.

Accordingly you won’t see this email in John’s inbox. This is a great advantage of using milters by the way. Imagine Postfix receiving a spam email and confirming its reception. What should it do when it finds out that it’s unwanted email? According to the SMTP protocol it must not throw away any emails. Would you create a bounce message telling the sender that you did not accept the email? That would be a bad idea because the sender address is very likely forged. You would send the bounce to an innocent person thus creating so called backscatter and make it even worse. So the right approach is to check the email while the sending server is still connected to your Postfix. This allows Postfix to reject the email with a 5.x.x error code and let the other side figure out what to do.

Score metrics (optional)

rspamd will however not reject all spam email. As it computes a score of the spam propability you can tell it which scores you would accept, flag as spam or make the incoming email get rejected. Take a look at the /etc/rspamd/actions.conf file. There are tons of scores defined for various conditions. At the beginning of the file you will find:

actions {
  reject = 15;
  add_header = 6;
  greylist = 4;

These are the default actions. If rspamd computes a score of at least 15 then the email will get rejected at the doorstep just like the Gtube pattern in the previous test. Any score above 6 will add header lines like “X-Spam: Yes” so that your mail software can detect them and maybe file the email to a different folder. Any score above 4 will trigger greylisting which is a mechanism that temporarily rejects the email with a 4.x.x code and waits if the sending server will try again. After a waiting time of a few minutes greylisting will accept the email. The idea is to reject email from systems that do not have a sending queue. Malware like on infected Wind*ws computers used to try sending an email just once which triggered greylisting and successfully rejected the spammer. But even malware programmers have learned and may try again after a few minutes thus circumventing greylisting. Your mileage may vary.

If you like to change these defaults then create a new file in /etc/rspamd/local.d/actions.conf containing your desired limits:

reject = 150;
add_header = 2;
greylist = 5;

This would virtually never reject an email. And it would flag any email with a score of at least 2 as spam. Greylisting would happen at a score above 5. These are not necessarily sane values – they are just meant as an example.

Please take a moment to understand how to change rspamd defaults. You can either create files in /etc/rspamd/override.d/… which will replace entire sections; or create files in /etc/rspamd/local.d/… which will change only small parts of the configuration. There is a helpful page in the rspamd documentation that contains examples. Whatever you do – never change the /etc/rspamd/* files directly because a software update will try to replace them.

Of course restart rspamd after any configuration changes:

systemctl restart rspamd

To check if rspamd has picked up your configuration use this command to see the current configuration:

rspamadm configdump

You may test your configuration using

rspamadm configtest

Alternatively you may check if all required rspamd processes are running…

pgrep -a rspam

11431 rspamd: main process                         
11555 rspamd: rspamd_proxy process (localhost:11332)
11557 rspamd: controller process (localhost:11334)
11559 rspamd: normal process (localhost:11333)

Adding headers

As you may know an email contains of the header and the body. Your users will only see header information like the subject, the sender, the recipient and the date and time the email was sent. But there is way more information like the router the email travelled or extended headers added by the various mail server on the way to the destination. Such extended headers begin with an “X-“. rspamd can add such headers to help you filter out spam. For that purpose create a new configuration override file at /etc/rspamd/override.d/milter_headers.conf with this content:

extended_spam_headers = true;

You would need to restart rspamd of course:

systemctl restart rspamd

As documented it will add these headers:

X-Rspamd-Server: mail
Authentication-Results: dmarc=fail reason="No valid SPF, No valid DKIM" …
X-Rspamd-Queue-Id: C22E55A005B3
X-Spamd-Result: default: False [11.55 / 15.00]
 ASN(0.00)[asn:16276, ipnet:, country:FR]
 DMARC_POLICY_SOFTFAIL(0.10)[Chronopost.fr : No valid SPF, No valid DKIM,none]
X-Spam: Yes

No headers?

Please note that headers are only added by rspamd if you send the email from the outside of your server. If you use swaks locally then you will not get those headers.

Each of the uppercase symbols like FROM_HAS_DN means that a certain detection routing of rspamd was triggered. It does not necessarily mean something bad about the email. For example R_SPF_ALLOW has a negative score that lowers the total score because it is something good about the email. There are a several symbols with a 0.00 score. These do not change the score but show you what rspamd has found. But if you consider certain criteria good or bad then you can define your own scores for them.

The last line here is especially interesting because next on our list is…

Sending spam to the Junk folder

Your users will not realize that their spam emails have an added “X-Spam: Yes” header. It it not actively shown in their mail client. Nor does it move the email out of the inbox into their spam folder. Such emails just appear like normal in their inbox. So let’s aid them by moving spam to a separate Junk folder beneath their inbox automatically. Dovecot has support for Sieve filters which are scripts that run automatically whenever an email comes in.

John could create a new Sieve filter (e.g. using the Roundcube webmail interface) for himself that would save any emails to his “Junk” folder if the header line “X-Spam: Yes” was found. This rule would be useful for all your users though so let’s find a general solution.

Dovecot supports global Sieve filters that apply to all users. Edit the file /etc/dovecot/conf.d/90-sieve.conf. Look for the “sieve_after” lines. They are commented out. Add a new line there:

sieve_after = /etc/dovecot/sieve-after

The “sieve after” filters are executed after the users’ filters. John can define his own filter rules. And after that Dovecot will run any filter rules it finds in files in /etc/dovecot/sieve-after. That is just an arbitrary directory that you create:

mkdir /etc/dovecot/sieve-after

And add a new file /etc/dovecot/sieve-after/spam-to-folder.sieve reading:

require ["fileinto","mailbox"];

if header :contains "X-Spam" "Yes" {
 fileinto :create "Junk";

Separatist warning

If your /var/vmail directory tree is (still) using the “.” separator then you need to call the destination folder “INBOX.Junk” instead of “Junk”.

The “require” lines include functionality to move emails into certain folders (fileinto) and to create folders if they don’t exist yet (mailbox). Then if rspamd marked an email as spam it gets moved into the INBOX.Junk folder which just appears as “Junk” to the user underneath their inbox.

Dovecot cannot deal with such human-readable files though. So we need to compile it:

sievec /etc/dovecot/sieve-after/spam-to-folder.sieve

That generated a machine-readable file /etc/dovecot/sieve-after/spam-to-folder.svbin.

Now all your users will automatically get spam emails moved to their Junk folder. Nice – isn’t it?

/etc/dovecot/sieve only

Please do not put global Sieve files anywhere else. Dovecot only obeys files there. The respective configuration parameter is called sieve_pipe_bin_dir that defines where such Sieve files are expected.

Training the spam detection

One of rspamd’s features is analyzing word patterns using probability theory. That functionality is contained in its “statistical module“. (Yes, the name is pretty misleading.)

(a) No Bayes training

You can start with an empty training database. This is not as bad as it sounds. rspamd has way more functionality to determine if an email is ham or spam. Autolearning takes email that are surely ham or spam and uses them to train the spam filter. The rspamd documentation has further examples how to fine-tune auto learning. After a few hundred emails the training will contribute towards a better detection rate.

If you want to use autolearning just create a new file /etc/rspamd/override.d/classifier-bayes.conf and make it contain:

autolearn = true;

(b) Migrating training data from previous mail server

If your previous mail server used rspamd on Debian Stretch then you can migrate over the training data. Stop the rspamd process on both the old and the new server so that the SQLite databases are closed properly…

systemctl stop rspamd

…and copy over all files like /var/lib/rspamd/*.sqlite to your new mail server.

Set the ownership of the database files on the new server correctly:

chown _rspamd._rspamd /var/lib/rspamd/*sqlite

Then restart rspamd on the both servers:

systemctl start rspamd

Verify that the database is now filled by running…

rspamc stat

At the end of the output you will see something like…

Statfile: BAYES_SPAM type: sqlite3; length: 36.59M; free blocks: 0; total blocks: 497.23k; free: 0.00%; learned: 3622; users: 233; languages: 14
Statfile: BAYES_HAM type: sqlite3; length: 28.00M; free blocks: 0; total blocks: 374.15k; free: 0.00%; learned: 31913; users: 97; languages: 6
Total learns: 35535

(c) Training from your existing ham and spam emails

Have you been running a mail server with mailboxes in a Malidir structure before but without rspamd? Then you probably have a good amount of ham and spam emails. Let’s use those to train rspamd. It is important to train both ham and spam emails. The rspamc command will allow you to feed entire directories/folders of emails to the learning process. An example to train spam:

rspamc learn_spam /var/vmail/example.org/john/Maildir/.INBOX.Junk/cur

And this would be an example to train ham from John’s inbox:

rspamc learn_ham /var/vmail/example.org/john/Maildir/cur

Of course the quality of spam detection will depend on how good the source data is. So do not train spam emails from users who randomly put mails into their Junk folder that is not actually spam.

Check the number of emails you learned by running…

rspamc stat

Bayes spam checking will not work before it learned at least 200 spam and ham emails. Teaching rspamd fewer emails or just spam emails will not work. This is defined by the min_learns variable defined in /etc/rspamd/statistic.conf.

In the output you will find lines beginning with “Statfile” like these…

Statfile: BAYES_SPAM type: sqlite3; length: 41.78M; free blocks: 0; total blocks: 0; free: 0.00%; learned: 0; users: 1; languages: 1
Statfile: BAYES_HAM type: sqlite3; length: 51.12M; free blocks: 0; total blocks: 0; free: 0.00%; learned: 0; users: 1; languages: 1
Total learns: 0

This is what you usually start with. The more emails you feed into the training process the better the detection rate will be. Some emails however may not be long enough or be too similar to previously trained emails. So don’t worry if you are training 1000 emails but just get a count of 500 emails here.

Per-user spam training

rspamd allows you to train the spam detection per user. It would not keep a global training database that applies to all users. Instead each user gets their own training.

Advantage: users work differently. Some have subscribed to a sales newsletter and now believe that marking it as spam gets them unsubscribed. Yes, that’s stupid but can thoroughly confuse the spam detection. Also you might be very interested in viagr* product informatoin while others do not.

Disadvantage: training still requires many ham and spam mails before it has any effect. So unless a user gets 200 samples of good and evil emails the spam detection cannot work.

If you decide you want to use per-user spam training then add/edit the file /etc/rspamd/override.d/statistic.conf /etc/rspamd/local.d/classifier-bayes.conf and insert:

users_enabled = true;

Better scaling with Redis

So far rspamd has stored its training (and other) data in SQLite databases. If you haven’t heard of it: it’s a database system much like MariaDB but it does not have a server process that you connect to. Instead you access the database files directly using either the “sqlite3” command or using libraries that integrate it SQLite into other programming languages. It is very simple to use. But compared to MariaDB or PostgreSQL its performance is limited. SQLite is completely sufficient for medium-size mail servers. But if you are going big you should instead use Redis as a data store.

Redis is also a kind of database system. It is way more limited than a traditional SQL database. But it is lightning fast the way it works. On my aged server it handles around 50,000 requests per second. It gets it speed from keeping the data in memory. So it doesn’t access the disk to fetch information. (Yet it saves its data to disk frequently to prevent data loss.) People use Redis as a cache or for very fast lookups of simple data structures. And so does rspamd.

Installing Redis is simple:

apt install redis

That’s it. No more configuration needed. Redis mainly consists of a daemon process called redis-server that listens for TCP connections on localhost on port 6379. Enabling Redis in rspamd is simple. Add a file /etc/rspamd/override.d/redis.conf and insert:

servers = "";

Now you need to configure which rspamd modules should use Redis as a backend storage. Several modules support Redis. Just as an example let’s put the spam training data of the statistic module into Redis. Edit the file /etc/rspamd/override.d/statistic.conf and set the backend there. If you also use per-user spam training this file should now look like this:

classifier "bayes" {
   users_enabled = true;
   backend = "redis";

Feel free to use Redis for other lookups, too.

Learning from user actions

Now we are getting to something really cool. Let’s tell Dovecot that moving emails into the Junk folder teaches rspamd instantly that the email is spam. And train an email as ham if it is moved out of the Junk folder. We will add triggers (actually “sieve scripts“) to the action of moving emails via IMAP.

The currently recommended way is to use the “IMAPSieve” plugin instead. There is nothing to install – it comes with the Dovecot packages. We just need to configure it.

First order of business is enabling the IMAPSieve plugin for the IMAP protocol/service in Dovecot. Edit the /etc/dovecot/conf.d/20-imap.conf file and look for the line reading “mail_plugins”. Turn it into:

mail_plugins = $mail_plugins imap_sieve

We also need to edit Dovecot’s Sieve configuration to enable two plugins that are required for our task. Sieve is a scripting language that automates things in conjunction with emails and folders. Edit the file /etc/dovecot/conf.d/90-sieve.conf and put these lines into the plugin {…} section:

# From elsewhere to Junk folder
imapsieve_mailbox1_name = Junk
imapsieve_mailbox1_causes = COPY
imapsieve_mailbox1_before = file:/etc/dovecot/sieve/learn-spam.sieve

# From Junk folder to elsewhere
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Junk
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:/etc/dovecot/sieve/learn-ham.sieve

sieve_pipe_bin_dir = /etc/dovecot/sieve
sieve_global_extensions = +vnd.dovecot.pipe
sieve_plugins = sieve_imapsieve sieve_extprograms

The first rule tells Dovecot to run the Sieve rules as defined in the /etc/dovecot/sieve/learn-spam.sieve file whenever an email is moved into a user’s “Junk” folder. We will create that Sieve script in a minute.

The second rule sets the other way. Whenever an email is moved from the “Junk” folder to any (*) folder then the /etc/dovecot/sieve/learn-ham.sieve Sieve script is called.

The “sieve_pipe_bin_dir” setting defines where executable scripts are allowed to reside. We will put our simple learning scripts there. And finally the “sieve_global_extensions” setting enables the pipe plugin that allows sending email to external commands.


If you come from previous versions of this guide, your mail directories may still use dots (“.”) instead of paths (“/”) to compose the folder structure. A clear sign is that you find directories like “INBOX.Junk” instead of just “Junk” in mail directories within /var/vmail. In that case replace the word “Junk” in the section above by “INBOX.Junk”.

Next up let’s create the Sieve scripts that we told Dovecot about. Create a new directory /etc/dovecot/sieve to put our new files in:

mkdir /etc/dovecot/sieve

Then create the file /etc/dovecot/sieve/learn-spam.sieve and let it contain:

require ["vnd.dovecot.pipe", "copy", "imapsieve"];
pipe :copy "rspamd-learn-spam.sh";

Let’s do the same for /etc/dovecot/sieve/learn-ham.sieve

require ["vnd.dovecot.pipe", "copy", "imapsieve", "variables"];
if string "${mailbox}" "Trash" {
pipe :copy "rspamd-learn-ham.sh";

The above Sieve script avoids training an email as ham if the user moves it to the Trash folder. After all if you clear your Junk folder you do not want to train your spam as regular emails.

Restart Dovecot:

systemctl restart dovecot

These two scripts need to be compiled – that is turning them into machine-readable code:

sievec /etc/dovecot/sieve/learn-spam.sieve
sievec /etc/dovecot/sieve/learn-ham.sieve

This creates two new files “learn-ham.svbin” and “learn-spam.svbin” that look like gibberish inside but are now in a format that Dovecot’s Sieve plugin can understand.

Let’s fix the permissions of these files, too, while we are at it:

chmod u=rw,go= /etc/dovecot/sieve/learn-{spam,ham}.{sieve,svbin}
chown vmail.vmail /etc/dovecot/sieve/learn-{spam,ham}.{sieve,svbin}

And the last step is to create the simple shell scripts that do the actual spam/ham training. The first file is /etc/dovecot/sieve/rspamd-learn-spam.sh which contains:

exec /usr/bin/rspamc learn_spam

That looks simple, doesn’t it? Nothing more is actually needed. The spam email is piped to this script and rspamd learns it as a spam email and adjusts its spam detection database accordingly.

The second script teaches ham and is called /etc/dovecot/sieve/rspamd-learn-ham.sh. Make it contain:

exec /usr/bin/rspamc learn_ham

These two shell scripts must be executable so go fix that:

chmod u=rwx,go= /etc/dovecot/sieve/rspamd-learn-{spam,ham}.sh
chown vmail.vmail /etc/dovecot/sieve/rspamd-learn-{spam,ham}.sh

I hope you haven’t lost your mind yet. It’s really just a chain of things to happen. Let’s reiterate how this process works:

  1. a user moves a spam email into their “Junk” folder
  2. Dovecot realizes that this triggers the Sieve rule “imapsieve_mailbox1” so it calls the Sieve script /etc/dovecot/sieve/learn-spam.sieve (in fact the *.svbin version of the script)
  3. Sieve will take the email and send (“pipe”) it to the executable rspamd-learn-spam.sh shell script
  4. the script in turn runs the email through the “/usr/bin/rspamc learn_spam” command

This works equally for the other way or learning ham emails of course.

I am sure you are eager to try it out. Yes, it’s about time. However to see that it actually works I suggest you edit the /etc/dovecot/conf.d/10-logging.conf file and set “mail_debug=yes”. That will add a lot more detail to the /var/log/mail.log file but on a busy server may also lead to headaches. 🙂

Restart Dovecot…

systemctl restart dovecot

…and watch the /var/log/mail.log file…

tail -f /var/log/mail.log

Now open your IMAP client (Thunderbird, Evolution, Roundcube, mutt or whatever you prefer) and drag an email to your Junk folder. The mail log should read something along the lines of…

imap(john@example.org): Debug: imapsieve: mailbox INBOX.Junk: MOVE event
imap(john@example.org): Debug: imapsieve: Matched static mailbox rule [1]
imap(john@example.org): Debug: sieve: file storage: Using Sieve script path: /etc/dovecot/sieve/learn-spam.sieve
imap(john@example.org): Debug: sieve: file script: Opened script learn-spam' from/etc/dovecot/sieve/learn-spam.sieve'
imap(john@example.org): Debug: sieve: Opening script 1 of 1 from /etc/dovecot/sieve/learn-spam.sieve' imap(john@example.org): Debug: sieve: Loading script /etc/dovecot/sieve/learn-spam.sieve imap(john@example.org): Debug: sieve: Script binary /etc/dovecot/sieve/learn-spam.svbin successfully loaded imap(john@example.org): Debug: sieve: binary save: not saving binary /etc/dovecot/sieve/learn-spam.svbin, because it is already stored imap(john@example.org): Debug: sieve: Executing script from/etc/dovecot/sieve/learn-spam.svbin'
imap(john@example.org): Debug: sieve: action pipe: running program: rspamd-learn-spam.sh
imap(john@example.org): Debug: Mailbox INBOX.Junk: Opened mail UID=3978 because: mail stream
imap(john@example.org): Debug: waiting for program /etc/dovecot/sieve/rspamd-learn-spam.sh' to finish after 0 msecs imap(john@example.org): sieve: pipe action: piped message to programrspamd-learn-spam.sh'

Look for errors and warnings. If at the end you see that Dovecot called the “rspamd-learn-spam.sh” script then you should be fine.

And finally if you pull an email out of the “Junk” folder you should see mailbox rule [2] be called and the email being learned as ham:

imap(john@example.org): Debug: imapsieve: mailbox INBOX: MOVE event
imap(john@example.org): Debug: imapsieve: Matched static mailbox rule [2]
imap(john@example.org): Debug: sieve: file storage: Using Sieve script path: /etc/dovecot/sieve/learn-ham.sieve
imap(john@example.org): Debug: sieve: file script: Opened script learn-ham' from/etc/dovecot/sieve/learn-ham.sieve'
imap(john@example.org): Debug: sieve: Opening script 1 of 1 from /etc/dovecot/sieve/learn-ham.sieve' imap(john@example.org): Debug: sieve: Loading script /etc/dovecot/sieve/learn-ham.sieve imap(john@example.org): Debug: sieve: Script binary /etc/dovecot/sieve/learn-ham.svbin successfully loaded imap(john@example.org): Debug: sieve: binary save: not saving binary /etc/dovecot/sieve/learn-ham.svbin, because it is already stored imap(john@example.org): Debug: sieve: Executing script from/etc/dovecot/sieve/learn-ham.svbin'
imap(john@example.org): Debug: sieve: action pipe: running program: rspamd-learn-ham.sh
imap(john@example.org): Debug: Mailbox INBOX: Opened mail UID=28412 because: mail stream
imap(john@example.org): Debug: waiting for program /etc/dovecot/sieve/rspamd-learn-ham.sh' to finish after 0 msecs imap(john@example.org): sieve: pipe action: piped message to programrspamd-learn-ham.sh'

That’s it. Nifty, isn’t it?


Andi Olsen pointed out that Dovecot has introduced a feature to automatically delete emails in a folder that reach a certain age. This is especially useful for the “Trash” and “Junk” folders. To enable this feature just edit the /etc/dovecot/conf.d/15-mailboxes.conf file and add the autoexpunge parameter where desired. Example:

mailbox Junk {
  special_use = \Junk
  auto = subscribe
  autoexpunge = 30d
mailbox Trash {
  special_use = \Trash
  auto = subscribe
  autoexpunge = 30d

Restart Dovecot after any configuration change.


rspamd keeps a verbose log of its actions in /var/log/rspamd/rspamd.log. If a user complains that a certain email got blocked or at least flagged as spam then take a look at this log. You can match the /var/log/mail.log with it by comparing the Postfix queue ID. Those are the 12-digit hexadecimal number like “95CE05A00547“. Those IDs can be found in the rspamd.log, too:

2018-01-14 06:39:45 #10424(normal) <40985d>; task; rspamd_task_write_log: id: <undef>, qid: <95CE05A00547>,  ip:, from: <…>, (default: F (no action):  [3.40/15.00]  [MISSING_MID(2.50){},MISSING_DATE(1.00){},MIME_GOOD(-0.10){text/plain;},ARC_NA(0.00){},ASN(0.00){asn:8220,  ipnet:,  country:GB;},FROM_EQ_ENVFROM(0.00){},FROM_NO_DN(0.00){},RCPT_COUNT_ONE(0.00){1;},RCVD_COUNT_ZERO(0.00){0;},RCVD_TLS_ALL(0.00){},TO_DN_NONE(0.00){},TO_DOM_EQ_FROM_DOM(0.00){},TO_MATCH_ENVRCPT_ALL(0.00){}]),  len: 181, time: 16.000ms real, 6.385ms virtual, dns req: 0, digest:  <69b289a82827c11f759837c033cd800a>, rcpts: <…>, mime_rcpt:  <…>

Scan outgoing emails, too

Previously I accidentlly recommended to set smtpd_milters to an empty value to prevent scanning outgoing emails. But that was wrong. In fact you want to use rspamd to work on outgoing emails. rspamd determines automatically if an authenticated user wants to send an email out and behaves differently. For example it does not check for IP blacklists or DKIM signatures. Instead it adds a DKIM signature.

So please don’t use “-o smtpd_milters=” anywhere. Sorry for the confusion.

The web interface

rspamd’s web interface

rspamd comes with a neat bonus feature: a web interface. It allows you to check emails for spam, get statistics and fine-tune scores. It is already installed and enabled by default and expects HTTP requests on port 11334 on the localhost interface. I suggest you add a simple proxy configuration to your already working HTTPS-enabled web mail configuration to get access.

First you need to enable Apache’s modules for HTTP proxying and rewriting:

a2enmod proxy_http
a2enmod rewrite

You can either create a new virtual host configuration or just edit the /etc/apache2/sites-available/webmail.example.org-https.conf file. Anywhere within the VirtualHost tags add:

<Location /rspamd>
  Require all granted   

RewriteEngine On
RewriteRule ^/rspamd$ /rspamd/ [R,L]
RewriteRule ^/rspamd/(.*) http://localhost:11334/$1 [P,L]

This piece of configuration will forward any requests to https://webmail.example.org/rspamd to localhost:11334 and thus give you access to the rspamd web interface.

The interface is password protected. Let’s generate a new access password:

pwgen 15 1

This gives you a password like “eiLi1lueTh9mia4”. You could put that password in an rspamd configuration file. But cleartext passwords in configuration files are not quite elegant. Let’s create a hash of the password:

rspamadm pw
Enter passphrase: …

Feed it the password that pwgen created for you and you will get a long hashed password. This procedure by the way is also documented on the rspamd pages.

Create a new configuration file /etc/rspamd/local.d/worker-controller.inc and put your hashed password in there:

password = "$2$icoahes75e7g9wxapnrbmqnpuzjoq7z…"

There is a nasty bug in the rspamd package in Debian Buster that breaks displaying of the charts. The Javascript D3 library that is used for that purpose was forgotten to get installed. Fortunately you just need to extract one Javascript file from the D3 release ZIP file to work around it:

wget https://github.com/d3/d3/releases/download/v5.15.0/d3.zip
unzip -d /usr/share/rspamd/www/js/lib d3.zip d3.min.js
rm d3.zip

That’s it for the configuration. Finally restart both rspamd and Apache to load your changed configuration:

systemctl restart rspamd
systemctl restart apache2

If everything went as expected you should now be able to access the rspamd web interface at https://webmail.example.org/rspamd

No graphs?

I could not get the (pie) charts working on the test server. Maybe that is a bug in rspamd 1.8.1. If anyone gets it working please leave a comment.

Anything else?

If you consider using rspamd for a larger environment then please take your time reading the good amount of documentation they provide. rspamd scales very well but requires additional setup like using Redis for faster spam analysis across multiple mail gateways.

Oh, and a…

Privacy Warning

rspamd “phones home” for its fuzzy checks. If that makes you feel bad you may want to disable that feature. Besides rspamd wants your money if you make money with your mail server and scan more than half a million emails per day.

© 2021 workaround.org - Proudly powered by theme Octo