Setup Email Server From Scratch Debian #2 - 11 SpamAssassin
We believe in data independence, and support others who want data independence.
Debian Email From Scratch version 2 finished 2025-07-30.
We are still adding to it but it all works!
##################################
# Blocking Spam With SpamAssasin #
##################################
apt install postfix-pcre
# Discard mails with headers or body matching regular expressions
nano /etc/postfix/main.cf
# --- add to end of file
header_checks = pcre:/etc/postfix/header_checks
body_checks = pcre:/etc/postfix/body_checks
# ---
# Discard silently messages matching regular expression on the right with DISCARD
# Discard blank email addresses
# Filter emails with empty From: and To: and 'free morgage quote' and 'repair
# your credit' .. pcre is case insensitive by default.
nano /etc/postfix/header_checks
# ---
/To:.*<>/ DISCARD
/From:.*<>/ DISCARD
/free mortgage quote/ DISCARD
/repair your credit/ DISCARD
# ---
postmap /etc/postfix/header_checks
nano /etc/postfix/body_checks
# ---
/free mortgage quote/ DISCARD
/repair your credit/ DISCARD
# ---
postmap /etc/postfix/body_checks
# Setup NO-REPLY addresses if needed, you can add domain in the regular expression
nano /etc/postfix/noreply_recipients
# ---
/^no-?reply\@okdeb\.com/ REJECT "This noreply@okdeb address does not accept replies. Please do not reply."
/^do-?not-?reply\@/ REJECT "This address does not accept replies. Please do not reply."
/no-?reply\@/ REJECT "This address does not accept replies. Please do not reply."
/dev-?null\@/ REJECT "This address does not accept replies. Please do not reply."
# ---
postmap /etc/postfix/noreply_recipients
systemctl restart postfix
# SpamAssassin
apt install spamassassin spamc
systemctl enable spamd
systemctl start spamd
# Add SpamAssassin to Postfix
apt install spamass-milter
# Change this later and run Spamassassin from Amavis virus filter, otherwise
# Spamassasin will be run twice.
nano /etc/postfix/main.cf
# --- change this line
smtpd_milters = unix:opendkim/opendkim.sock,unix:opendmarc/opendmarc.sock,unix:spamass/spamass.sock
# ---
# Remove the hash/pound mark to enable and change to -r 8, do not change the first
# line. This changes rejection of mail with a score above 15 to a score above 8.
nano /etc/default/spamass-milter
# ---
OPTIONS="-u spamass-milter -i 127.0.0.1 -R REJECTED_AS_SPAM"
OPTIONS="${OPTIONS} -r 8"
# ---
systemctl restart postfix spamass-milter spamd
# This may cause mails to be rejected need to get and DQS key from spamhaus and
# configure in postfix...
# Go to spamhaus , create an account, and create a DQS_key
https://www.spamhaus.com/resource-center/if-you-query-spamhaus-projects-dnsbls-via-cloudflares-dns-move-to-the-free-data-query-service/
# Replace your_DQS_key with your own key in the configuration below.
nano /etc/postfix/main.cf
# --- edit smtpd_recipient_restrictions ---
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination,
check_recipient_access pcre:/etc/postfix/noreply_recipients,
check_policy_service unix:private/policyd-spf,
check_policy_service inet:127.0.0.1:10023,
check_client_access hash:/etc/postfix/rbl_override,
reject_rbl_client your_DQS_key.zen.dq.spamhaus.net=127.0.0.[2..11]
reject_rhsbl_sender your_DQS_key.dbl.dq.spamhaus.net=127.0.1.[2..99]
reject_rhsbl_helo your_DQS_key.dbl.dq.spamhaus.net=127.0.1.[2..99]
reject_rhsbl_reverse_client your_DQS_key.dbl.dq.spamhaus.net=127.0.1.[2..99]
reject_rhsbl_sender your_DQS_key.zrd.dq.spamhaus.net=127.0.2.[2..24]
reject_rhsbl_helo your_DQS_key.zrd.dq.spamhaus.net=127.0.2.[2..24]
reject_rhsbl_reverse_client your_DQS_key.zrd.dq.spamhaus.net=127.0.2.[2..24]
permit_dnswl_client list.dnswl.org=127.0.[0..255].[1..3],
permit_dnswl_client swl.spamhaus.org,
rbl_reply_maps = hash:$config_directory/dnsbl-reply-map
# ---
nano /etc/postfix/dnsbl-reply-map
# ---
your_DQS_key.zen.dq.spamhaus.net=127.0.0.[2..11] 554 $rbl_class $rbl_what blocked using ZEN - see https://www.spamhaus.org/query/ip/$client_address for details
your_DQS_key.dbl.dq.spamhaus.net=127.0.1.[2..99] 554 $rbl_class $rbl_what blocked using DBL - see $rbl_txt for details
your_DQS_key.zrd.dq.spamhaus.net=127.0.2.[2..24] 554 $rbl_class $rbl_what blocked using ZRD - domain too young
your_DQS_key.zen.dq.spamhaus.net 554 $rbl_class $rbl_what blocked using ZEN - see https://www.spamhaus.org/query/ip/$client_address for details
your_DQS_key.dbl.dq.spamhaus.net 554 $rbl_class $rbl_what blocked using DBL - see $rbl_txt for details
your_DQS_key.zrd.dq.spamhaus.net 554 $rbl_class $rbl_what blocked using ZRD - domain too young
#---
postmap /etc/postfix/dnsbl-reply-map
systemctl restart postfix
# At this point I got bounce mails from my first mail server saying the domain
# okdeb.com was on a blacklist. Even though it was brand new.
https://check.spamhaus.org/
https://www.spamsources.fabel.dk/delist
https://www.mail-tester.com
https://mxtoolbox.com
# I created a postmaster@okdeb.com email address, not an alias, to submit a
# ticket. Checked my email credibility which was saying no reverse DNS. In OVH
# Cloud control customers can set a reverse DNS easily if you have the forward
# pointer set, so I set mine for both ipv4 and ipv6 to mx.okdeb.com which has to
# match what is is the smtp_banner which should be myhostname variable. My first
# hostname in /etc/hosts is okdeb.com but the value in the banner comes from
# myhostname set in /etc/postfix/main.cf which is mx.okdeb.com. After this I
# submitted a request to remove my domain from spamhaus RBL and used the
# postmaster@okdeb.com email address, they send a confirmation mail, and I
# replied and the ticket was submitted. There were many DNS messages in the
# logs. The main issues were probably due to non-matching RDNS pointer and it
# being a brand new domain.
nano /etc/postfix/rbl_override
# ---
domain1.com OK // ignore rbl for domain1.com
domain2.com OK // ignore rbl for domain2.com
spamhaus.org OK
spamhaus.net OK
# ---
postmap /etc/postfix/rbl_override
systemctl restart postfix
# I also saw a number of DNSSEC failures related to spamhaus in the logs. So
# turned off DNSSEC in systemd-resolved and white listed spamhaus in
# /etc/postfix/rbl_override.
journalctl -e -g DNS
Jul 29 07:31:46 okdeb.com systemd-resolved[420]: [🡕] DNSSEC validation failed
for question spamhaus.org IN DS: no-signature
Jul 29 07:31:46 okdeb.com systemd-resolved[420]: [🡕] DNSSEC validation failed
for question mail-vk1-xa4a.google.com.dbl.spamhaus.org IN TXT: no-signature
Jul 29 07:32:01 okdeb.com systemd-resolved[420]: [🡕] DNSSEC validation failed
for question ip6.arpa IN DS: no-signature
Jul 29 10:14:21 okdeb.com spamd[1070]: check: dns_block_rule URIBL_BLOCKED hit,
creating /root/.spamassassin/dnsblock_multi.uribl.com (This means DNSBL blocked
you due to too many queries.>
# After a few DNS queries to spamhaus they will be blocked and a local cache
# will solve the problem. The full resolution of this is to stop using
# systemd-resolved which is stub resolver not a full recursive caching resolver
# and to setup bind nameserver. See how to set up bind farther down.o
# After this I still got the bounce message but my other zimbra mail server
# accepted the but still got a bounce message. Unexpected behavior.
# Check Email Headers and Body with SpamAssassin
# The following files has rules for things like
* MISSING_HEADERS
* MISSING_DATE
* MISSING_FROM
nano /usr/share/spamassassin/20_head_tests.cf
#---
no changes
# ---
# Update spamassassin rules daily using systemd
systemctl enable --now spamassassin-maintenance.timer
# Or edit CRON value in this file...
nano /etc/cron.daily/spamassassin
# ---
CRON=1
# ---
# Edit Scoring Rules
# Modified from linuxbabe.com. Whitelist your own domains.
nano /etc/spamassassin/local.cf
# --- add these to the bottom of the file --
score MISSING_FROM 5.0
score MISSING_DATE 5.0
score MISSING_HEADERS 3.0
score PDS_FROM_2_EMAILS 3.0
score EMPTY_MESSAGE 5.0
score FREEMAIL_DISPTO 2.0
score FREEMAIL_FORGED_REPLYTO 3.5
score DKIM_ADSP_NXDOMAIN 5.0
score FORGED_GMAIL_RCVD 2.5
header FROM_SAME_AS_TO ALL=~/\nFrom: ([^\n]+)\nTo: \1/sm
describe FROM_SAME_AS_TO From address is the same as To address.
score FROM_SAME_AS_TO 2.0
header EMPTY_RETURN_PATH ALL =~ /<>/i
describe EMPTY_RETURN_PATH empty address in the Return Path header.
score EMPTY_RETURN_PATH 3.0
header CUSTOM_DMARC_FAIL Authentication-Results =~ /dmarc=fail/
describe CUSTOM_DMARC_FAIL This email failed DMARC check
score CUSTOM_DMARC_FAIL 3.0
# good email rules
body GOOD_EMAIL /(debian|ubuntu|linux mint|centos|red hat|RHEL|OpenSUSE|Fedora|Arch Linux|Raspberry Pi|Kali Linux)/i
describe GOOD_EMAIL I don't think spammer would include these words in the email body.
score GOOD_EMAIL -4.0
body BOUNCE_MSG /(Undelivered Mail Returned to Sender|Undeliverable|Auto-Reply|Automatic reply)/i
describe BOUNCE_MSG Undelivered mail notifications or auto-reply messages
score BOUNCE_MSG -1.5
body __RESUME /(C.V|Resume)/i
meta RESUME_VIRUS (__RESUME && __MIME_BASE64)
describe RESUME_VIRUS The attachment contains virus.
score RESUME_VIRUS 5.5
header __AT_IN_FROM From =~ /\@/
meta NO_AT_IN_FROM !__AT_IN_FROM
score NO_AT_IN_FROM 4.0
header __DOT_IN_FROM From =~ /\./
meta NO_DOT_IN_FROM !__DOT_IN_FROM
score NO_DOT_IN_FROM 4.0
whitelist_from *@okdeb.com
whitelist_from *@coragarden.com
# whitelist_from jack@coragarden.com
# whitelist_from *@gooddomain.com
# blacklist_from spammer@example.com
# blacklist_from *@baddomain.org
# ---
# Check the rules syntax and restart spamd
spamassassin --lint
systemctl restart spamass-milter spamd
# SpamAssassin's Builtin Whitelist
# There are several files under /usr/share/spamassassin/ which contain builtin whitelists among other things.
cat /usr/share/spamassassin/60_whitelist_spf.cf
# Moving spam to the junk folder.
apt install dovecot-sieve
nano /etc/dovecot/conf.d/15-lda.conf
# ---
protocol lda {
# Space separated list of plugins to load (default is global mail_plugins).
mail_plugins = $mail_plugins sieve
}
# ---
nano /etc/dovecot/conf.d/20-lmtp.conf
# ---
protocol lmtp {
# Space separated list of plugins to load (default is global mail_plugins).
#mail_plugins = $mail_plugins quota
mail_plugins = quota sieve
}
# ---
nano /etc/dovecot/conf.d/10-mail.conf
# ---
mail_home = /var/vmail/%d/%n
# ---
nano /etc/dovecot/conf.d/90-sieve.conf
# --- change sieve_before and enable by removeing hash
sieve_before = /etc/dovecot/sieve_before/SpamToJunk.sieve
# ---
mkdir /etc/dovecot/sieve_before
nano /etc/dovecot/sieve_before/SpamToJunk.sieve
# ---
require "fileinto";
if header :contains "X-Spam-Flag" "YES"
{
fileinto "Junk";
stop;
}
# ---
sievec /etc/dovecot/sieve_before/SpamToJunk.sieve
systemctl restart dovecot
nano /etc/default/spamass-milter
# ---
# Default, use the spamass-milter user as the default user, ignore
# messages from localhost
OPTIONS="-u spamass-milter -i 127.0.0.1 -R REJECTED_AS_SPAM"
# Reject emails with spamassassin scores > 15.
#OPTIONS="${OPTIONS} -r 15"
OPTIONS="${OPTIONS} -r 8"
#Spamc options
OPTIONS="${OPTIONS} -- --max-size=5120000"
# ---
systemctl restart spamass-milter
# Configure Individual User Preferences
nano /etc/spamassassin/local.cf
# ---
# USER RULES ENABLED
allow_user_rules 1
# ---
nano /etc/default/spamd
# ---
OPTIONS="--create-prefs --max-children 5 --helper-home-dir --nouser-config --virtual-config-dir=/var/vmail/%d/%l/spamassassin --username=vmail"
# ---
systemctl restart spamd
nano /etc/default/spamass-milter
# ---
OPTIONS="-e okdeb.com -u spamass-milter -i 127.0.0.1 -R REJECTED_AS_SPAM"
# ---
systemctl restart spamass-milter
# Send and email to jack@coragarden.com to create the custom user rules directory
cd /var/vmail/coragarden.com/jack/spamassassin/
# Add custom rules to user_prefs, besides these you can add many other custom rules as required
nano /var/vmail/coragarden.com/jack/spamassassin/user_prefs
# -- these rules increase spam score for unsubscibe emails if you never subscribe with this email
body SUBSCRIPTION_SPAM /(unsubscribe|u n s u b s c r i b e|Un-subscribe)/i
describe SUBSCRIPTION_SPAM I didn't subscribe to your spam.
score SUBSCRIPTION_SPAM 3.0
header LIST_UNSUBSCRIBE ALL =~ /List-Unsubscribe/i
describe LIST_UNSUBSCRIBE I didn't join your mailing list.
score LIST_UNSUBSCRIBE 2.0
# ---
spamassassin --lint
systemctl restart spamd
# Whitelist & Blacklisting - to allow only whitelist emails and block all others. Whitelist
# decreases spam score by -100 and blacklist increases spam score by +100.
nano /var/vmail/coragarden.com/jack/spamassassin/user_prefs
# ---
whitelist_from *@okdeb.com
whitelist_from myfriend@gmail.com
blacklist_from *
# ---
spamassassin --lint
systemctl restart spamd
# Check URIBL_BLOCKED
# Send and email to postmaster@okdeb.com and view message source. If your header contains
# URIBL_BLOCKED, URIBL_DBL_BLOCKED_OPENDNS like this...
X-Spam-Status: No, score=-8.9 required=5.0 tests=DKIM_SIGNED ...
...
SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED, URIBL_DBL_BLOCKED_OPENDNS
journalctl -g DNS
Jul 29 16:08:18 okdeb.com spamd[8740]: check: dns_block_rule
RCVD_IN_ZEN_BLOCKED_OPENDNS hit, creating
/nonexistent/.spamassassin/dnsblock_zen.spamhaus.org (This means DNSBL blocked
you due to many queries
# ... install a local caching dns resolver. We can leave systemd-resolved alone since it listens on
# address 127.0.0.53 and won't interfere with bind. We can later use this as a master or slave DNS
# server. In this case we just set it up to cache requests. Too many DNS requests will cause the BL
# to block connections so having a DNS cache solves the issue. You could also disable systemd-resolved
# and change /etc/resolv.conf to nameserver 127.0.0.1. Make sure resolvconf is disabled .conf
# Install bind9 as a DNS caching name server
apt install bind9 dnsutils
cd /etc/bind
systemctl enable named
systemctl start named
rndc reload
/etc/bind/named.conf.options
# ---
include "/etc/bind/rndc.key";
controls {
inet 127.0.0.1 allow { localhost; } keys { "rndc-key"; };
// slave server // inet slave_ip allow { master_ip; } keys { "rndc-key"; };
};
# ---
systemctl restart named
rndc reload
/etc/bind/named.conf.options
# ---
options {
directory "/var/cache/bind";
dnssec-validation auto
recursion yes;
// only allow recursion for our own networks, we don't want others using
// our DNS - unless we setup as a master in which case it's not recursion
allow-recursion {
127.0.0.1;
::1;
// master_ipaddr;
// master_ipaddr6;
// trusted_ip;
// trusted_network;
};
allow-query { any; };
listen-on { 127.0.0.1; 15.204.113.148; };
listen-on-v6 { any; };
};
# ---
# The root hints is enabled by an include in /etc/bind/named.conf.default-zones
nslookup google.com 127.0.0.1
Server: 127.0.0.1
Address: 127.0.0.1#53
Non-authoritative answer:
Name: google.com
Address: 142.251.33.78
Name: google.com
Address: 2607:f8b0:400a:806::200e
dig @127.0.0.1 okdeb.com +dnssec +multiline
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
# Flags with ad means DNSSEC is enabled.
# Configure SpamAssassin to use local bind
nano /etc/spamassassin/65_dns.cf
# ---
dns_server 127.0.0.1
# ---
systemctl restart spamd
# Disable systemd-resolved and just use bind
systemctl stop systemd-resolved
systemctl disable systemd-resolved
rm /etc/resolv.conf
nano /etc/resolv.conf
# ---
nameserver 127.0.0.1
options edns0 trust-ad
search .
# ---
dig okdeb.com +dnssec +multiline
...
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
...
;; SERVER: 127.0.0.1#53(127.0.0.1) (UDP)
journalctl -g DNS
Jul 29 18:07:07 okdeb.com named[529]: generating session key for dynamic DNS Jul
Jul 29 23:34:59 okdeb.com spamd[1086]: spamd: result: . 0 - ARC_SIGNED,ARC_VALID,
BASE64_LENGTH_78_79,BASE64_LENGTH_79_INF,DKIMWL_WL_MED,DKIM_SIGNED,DKIM_VALID,DMARC_PASS
# Block Outgoing Mail - Prevent server sending emails to certain addresses.
nano /etc/postfix/header_checks
# --- block sending to any email address or domain matching 'badrecipient'
/^To:.*badrecipientd.*/ DISCARD
# ---
nano /etc/postfix/main.cf
# ---
header_checks = regexp:/etc/postfix/header_checks
# ---
postmap /etc/postfix/header_checks
systemctl reload postfix
# Delete Outgoing headers
nano /etc/postfix/smtp_header_checks
# ---
/^User-Agent.*Roundcube Webmail/ IGNORE
/^X-Spam-Status:/ IGNORE
/^X-Spam-Checker-Version:/ IGNORE
# ---
nano /etc/postfix/main.cf
# --- make sure you have these lines
header_checks = regexp:/etc/postfix/header_checks
smtp_header_checks = pcre:/etc/postfix/smtp_header_checks
body_checks = pcre:/etc/postfix/body_checks
# ---
postmap /etc/postfix/smtp_header_checks
systemctl reload postfix
# When I tested Outlook 2019 with Autodiscover, Outlook sent a test mail and I recieved
# a bounce saying missing Date header. Outlook is not RFC compliant.
# The spam score was -95.9 and message accepted. Look in /etc/mail/spamassassin/local.cf
# or /usr/share/spamassassin files. It went through because it was whitelisted which
# adds -100 to the score. Removed the whitelist and it was still delivered with a
# score of 4.1 which seems like more reasonable behavior if it was an external mail.
# This just confirms everything is working as expected and it is a good idea to whitelist
# your own domains.
whitelist_from jack@coragarden.com
# Test to make sure you can still send mail and make backups of your configurations
# Next Up Installing Amavis and ClamAV Antivirus Scanner