Table of Contents
This is a companion piece to articles I wrote about configuring postfix & dovecot 2.4 to handle system and virtual users on a Debian Trixie VPS (the config file details are also available here).
One of the things you really need to do when you run a mail server is address the presence of spam. There are two aspects to that: recognizing a spam email when you receive it, so you can ignore it or deliver it to a spam folder for further review, and not having your mail server be seen as a source of spam. This article is about the first aspect, recognizing a received email as spam.
There are a lot of tools available to do this. The two that I use are SpamAssassin, a rules-based, community-based learning algorithm, and spamhaus.org, an online service (actually, a wrapper for several online services).
SpamAssassin: Installation
I thought I already had SpamAssassin set up and running on my newly migrated VPS, but it turned out I was wrong. To reinstall it, I decided to use ChatGPT for guidance on the details. While that was a frustrating experience — AI models almost always speak authoritatively, even when they don’t have a fracking clue what they’re talking about 🙂 — I’m glad I did, because I ended up with a faster SpamAssassin setup.
The first step is to install the required packages:
sudo apt update
sudo apt install spamassassin spamc spamass-milter pyzor razor sa-compile
SpamAssassin operates in three parts:
- spamassassin is the core library
- spamd is a daemon that handles requests to analyze emails (as a daemon it runs continuously, avoiding the heavy startup cost of launching the core SpamAssassin code, which is written in Perl)
- spamc is the client app that mail servers call to analyze an email. It’s lightweight, and so can be called frequently by mail servers without unduly burdening your system.
After you’ve installed the packages, the first step is to configure the spamd daemon. Do so by creating it’s configuration file, /etc/default/spamassassin:
ENABLED=1
SAUPDATE=1
CRON=1
OPTIONS="--create-prefs --max-children 5 --helper-home-dir /var/lib/spamassassin -u debian-spamd"
Then configure the core spamassassin library, by modifying /etc/spamassassin/local.cf, which was created when you installed the packages. I’m showing here only the parameters where I didn’t accept the SpamAssassin defaults.
# This rewrites an email's subject line if it is identified as spam, to include the spam score.
# This is not the default behavior. I use it because my email server supports both system
# and virtual users, with system users' emails being delivered immediately by postfix,
# and virtual users' emails being delivered by dovecot.
#
# dovecot can be set to route spam emails automatically to a Junk mail folder
# (see below), but postfix can't do that, so you I wanted a way to identify
# spam messages visually in the system user inboxes (most email clients will
# also let you create a rule to route messages to folders based on text
# contained in their subject lines)
rewrite_header Subject [SPAM _SCORE_]
# I don't want to modify/report emails deemed to be okay
report_safe 0
# since I configure real-time block list checks outside of SpamAssassin,
# don't run them here as well
skip_rbl_checks 0
# these lines define the use of a compiled SpamAssassin, rather than
# interpreting it for every single email
use_pyzor 1
use_razor2 1
# these lines ensure the Bayesian learning algorithms are used
use_bayes 1
bayes_auto_learn 1
bayes_path /var/lib/spamassassin/bayes/bayes
It’s worth running SpamAssassin’s built-in configuration checker at this point to identify problems.
sudo spamassassin --lint
You then need to run the following commands to support Bayesian processing, and to compile the SpamAssassin code:
sudo mkdir -p /var/lib/spamassassin/bayes
sudo chown -R debian-spamd:debian-spamd /var/lib/spamassassin
sudo sa-update
sudo sa-compile || true
sudo systemctl enable --now spamassassin
sudo systemctl status spamassassin
You should then be able to run the following command line check to see that everything is working okay, and non-spam messages are not being tagged as spam:
echo "test" | spamc -c
You can also ensure spam messages are being tagged by running this guaranteed to get tagged as spam string through the system:
echo "XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X" | spamc -c
SpamAssassin: spamass-milter
The next step is to configure spamass-milter, the mail filter that postfix will call to check each incoming email. This was one of the points where ChatGPT fell flat on its face and wasted a lot of my time. Basically, what it initially had me try to do is to configure postfix to access spamass-milter via a Unix socket…which is either hard to do under Debian Trixie or flat out impossible.
In the end, ChatGPT found a solution by switching the postfix/spamass-milter interaction to using plain old ports. You start configuring this by modifying/creating /etc/default/spamass-milter to contain the following (again, I’m only showing you changes from the defaults; also, note the various options for reporting an email as spam):
# this line tells spamass-milter to monitor port 11332 on localhost for
# processing requests. We'll configure postfix to call that port
# when an email arrives
SOCKET="inet:11332@127.0.0.1"
# here are some options for reporting an email as spam
# this line would reject anything with a score greater than -1
#OPTIONS="-u spamass-milter -i 127.0.0.1 -m -r -1"
# this line delivers everything, but won't rewrite
# the subject line
#OPTIONS="-u spamass-milter -i 127.0.0.1 -m"
# this line delivers everything AND rewrites the subject
# line to include a spam score
OPTIONS="-u spamass-milter -i 127.0.0.1"
To teach postfix how to call spamass-milter, make the following change to /etc/postfix/main.cf:
smtpd_milters = inet:localhost:8891, inet:127.0.0.1:11332
non_smtpd_milters = inet:localhost:8891
The order in which you define the ports postfix will call to check an email for spam is important. The first clause — calling port 8891 — invokes the spamhaus.org real-time block list service (This was part of my earlier configuration discussion, but I’ll show you the details on setting this up in a separate post). It must come first. The second clause — calling port 11332 — is what bring spamass-milter, and SpamAsssassin, into play.
To check everything is working, run the following commands:
sudo systemctl restart spamass-milter.service
sudo ss -ltnp | grep 11332
sudo systemctl restart postfix
The second line — ss -ltnp… — should show that spamass-milter is listening on 127.0.0.1:11332. If it isn’t, something went wrong.
Adventures in sieving
At this point you should have SpamAssassin doing its thing, which you can check by sending emails containing that guaranteed to be spam text (XJSC4JDBQADN1.NSBN32IDNENGTUBE-STANDARD-ANTI-UBE-TEST-EMAILC.34X). But it’d be nice to configure dovecot so messages marked as spam ended up in a Junk folder instead of the inbox.
That’s simple to do…so long as you’re not using an AI that can’t remember you’re using dovecot 2.4, which introduced a whole bunch of breaking changes in how you set up configuration files. It also kept forgetting it’d told me to put all my configuration changes into /etc/dovecot/dovecot.conf, and not use the conf.d subdirectory files. Those blind spots cost me almost two hours of time. I finally figured out what I had to do using Google’s GeminiPro, and told ChatGPT in no uncertain terms how angry I was at it, and sharing the correct answer. It told me it would use what I’d learned going forward. We’ll see.
In any event, here’s how you set up sieving — the term used to describe routing emails to specific folders based on tests — in dovecot 2.4. First, install the required sieve packages and check their versions:
sudo apt install dovecot-sieve dovecot-lmtpd
sudo dpkg -l | grep dovecot
dovecot --version
dovecot and the dovecot sieve binaries must be at the exact same version level. Bad Things Will Happen to You if they’re not.
Next, create the sieving script in /etc/dovecot/sieve/default.sieve (you can call the file whatever you want):
require ["fileinto", "mailbox"];
if header :contains "X-Spam-Flag" "YES"
{
fileinto :create "Spam";
stop;
}
Now, update permissions and compile that sieve script:
sudo sievec /etc/dovecot/sieve/default.sieve
sudo chown -R dovecot:dovecot /etc/dovecot/sieve
ls -l /etc/dovecot/sieve/
If all went well, that last line should show you two files:
-rw-r--r-- 1 dovecot dovecot 113 Oct 4 18:12 default.sieve
-rw-r--r-- 1 dovecot dovecot 282 Oct 4 18:12 default.svbin
The second file is a compiled binary of the sieving source code.
Finally, make the following changes to /etc/dovecot/dovecot.conf:
sieve_script global_after {
type = after
driver = file
path = /etc/dovecot/sieve/default.sieve
}
protocol lmtp {
postmaster_address = postmaster@theboilingfrog.net
# this is the new part, on my system at least
mail_plugins {
sieve = yes
}
}
# this defines the layout of how a client displays
# IMAP emails when it connects for the first time
# I think most are default values, but...
namespace inbox {
inbox = yes
mailbox Drafts {
auto = subscribe
special_use = \Drafts
}
mailbox Junk {
auto = subscribe
special_use = \Junk
}
mailbox Trash {
auto = subscribe
special_use = \Trash
}
# For \Sent mailboxes there are two widely used names. We'll mark both of
# them as \Sent. User typically deletes one of them if duplicates are created.
mailbox Sent {
auto = subscribe
special_use = \Sent
}
mailbox "Sent Messages" {
auto = subscribe
special_use = \Sent
}
}
Restart the dovecot service and you should find emails tagged as spam ending up in the Junk folder.
BTW, if you’ve been following along with my tutorials you already have lines 16 to 48 in dovecot.conf. They define the template IMAP will use for creating folders in your email client when you first access a site dovecot handles. You don’t have to call the spam email folder Junk; that’s just my preference.
Using SpamHaus.org
SpamHaus.org offers both free and commercial accounts for using their lists of spam sites. If you qualify for the free account (which I believe is mostly based on usage), you can apply for an access key at their site.
Once you get a key, you modify /etc/postfix/main.cf to contain the following lines:
postscreen_dnsbl_sites = ltsiwdjteou6dbj7monsmeikxq.zen.dq.spamhaus.net*3,
ltsiwdjteou6dbj7monsmeikxq.dbl.dq.spamhaus.net*2,
ltsiwdjteou6dbj7monsmeikxq.zrd.dq.spamhaus.net*2
postscreen_dnsbl_reply_map = hash:/etc/postfix/dnsbl_reply
An important caveat: these lines must appear after you define your smtpd_relay_restrictions, smtpd_client_restrictions and smtpd_recipient_restrictions. If you’re utilizing the main.cf file I’ve been referring to in these articles, that’s where you’ll find the above lines.
Line 5 defines a separate hashmapped file used to provide that key to the various endpoints within spamhaus.org which check to see if an email is from a spammer. It looks like this (I’ve redacted my personal SpamHaus key; replace the word redacted with your key in your own setup):
# /etc/postfix/dnsbl_reply
# Replace YOUR_DQS_KEY with your exact alphanumeric key
redacted.zen.dq.spamhaus.net zen.spamhaus.org
redacted.dbl.dq.spamhaus.net dbl.spamhaus.org
redacted.zrd.dq.spamhaus.net zrd.spamhaus.org
You can call the file whatever you want; just be sure to use the correct name in that postscreen_dnsbl_reply_map entry in main.cf. Also, remember to hashmap the file using postfix’s utility:
sudo postmap <your file name here>