Setup Email Server From Scratch Debian #2 - 11 SpamAssassin

10 Blocking Spam <- Intro -> 12 Amavis Clam AntiVirus

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

10 Blocking Spam <- Intro -> 12 Amavis Clam AntiVirus