Setup Email Server From Scratch On FreeBSD #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