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