Setup Email Server From Scratch On FreeBSD #2 - 11 SpamAssassin
10 Blocking Spam With Postfix <- Intro -> 12 Amavis Clam AntiVirus
We believe in data independence, and support others who want data independence.
This tutorial is complete 2025-08-14 except there is no page for setting up postscreen.
This is version 2 and everthing works.
################################################### # Blocking Spam With Postfix PCRE and SpamAssasin # ###################################################
Discard or Reject mails by matching regular expressions in message header or body
nano /usr/local/etc/postfix/main.cf
--- add to end of file ---
header_checks = pcre:/usr/local/etc/postfix/header_checks
body_checks = pcre:/usr/local/etc/postfix/body_checks
Discard silently messages matching regular expression use DISCARD
Reject messages matching regular expression use REJECT
Spamhaus does client testing and if you discard the server will report delivered, and this may not be what is desired with doing a configuration check with Spamhaus. For production a production server, in some cases discard may be preferred, as the client doesn't know the email has been discarded. Giving clients too much information just tells spammers how to improve thier mail bot.
Filter emails with empty From: and To: and 'free morgage quote' and 'repair your credit' .. pcre is case insensitive by default.
nano /usr/local/etc/postfix/header_checks
/To:.*<>/ REJECT "5.7.1 Rejected, invalid To: header."
/From:.*<>/ REJECT "5.7.1 Rejected, invalid From: header."
/free mortgage quote/ DISCARD
/repair your credit/ DISCARD
postmap /usr/local/etc/postfix/header_checks
Filter emails with matching body
nano /usr/local/etc/postfix/body_checks
/free mortgage quote/ DISCARD
/repair your credit/ DISCARD
postmap /usr/local/etc/postfix/body_checks
Setup NO-REPLY addresses if needed, you can add domain in the regular expression
nano /usr/local/etc/postfix/noreply_recipients
/^no-?reply\@okbsd\.com/ REJECT "5.7.1 This address does not accept replies. Please do not reply."
/^do-?not-?reply\@/ REJECT "5.7.1 This address does not accept replies. Please do not reply."
/no-?reply\@/ REJECT "5.7.1 This address does not accept replies. Please do not reply."
/dev-?null\@/ REJECT "5.7.1 This address does not accept replies. Please do not reply."
postmap /usr/local/etc/postfix/noreply_recipients
service postfix restart
SpamAssassin
pkg install spamassassin spamass-milter
nano rc.conf
spamd_enable="YES"
spamd_flags="--create-prefs --max-children 5 --helper-home-dir --nouser-config --virtual-config-dir=/var/vmail/%d/%l/spamassassin --username=vmail"
spamass_milter_enable="YES"
spamass_milter_user="spamd"
spamass_milter_group="spamd"
spamass_milter_socket="/var/spool/postfix/spamass/spamass.sock"
spamass_milter_socket_owner="postfix"
spamass_milter_socket_group="postfix"
spamass_milter_socket_mode="660"
spamass_milter_localflags="-e okbsd.com -u spamd -i 127.0.0.1 -R REJECTED_AS_SPAM -r 8 -- --max-size=5120000"
mkdir -p /var/spool/postfix/spamass
chown spamd:wheel /var/spool/postfix/spamass
chmod 770 /var/spool/postfix/spamass
pw groupmod spamd -m postfix
sa-update
service sa-spamd start
service spamass-milter start
Change this later and run Spamassassin from Amavis virus filter, otherwise Spamassasin will be run twice.
nano /usr/local/etc/postfix/main.cf
--- change this line ---
smtpd_milters = unix:opendkim/opendkim.sock,unix:opendmarc/opendmarc.sock,unix:spamass/spamass.sock
Configure blacklists and whitelists
nano /usr/local/etc/postfix/main.cf
--- edit smtpd_recipient_restrictions ---
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination,
check_recipient_access pcre:/usr/local/etc/postfix/noreply_recipients,
check_policy_service unix:private/policyd-spf
check_policy_service inet:127.0.0.1:10023
check_client_access hash:/usr/local/etc/postfix/rbl_override,
reject_rhsbl_helo dbl.spamhaus.org,
reject_rhsbl_reverse_client dbl.spamhaus.org,
reject_rhsbl_sender dbl.spamhaus.org,
permit_dnswl_client list.dnswl.org=127.0.[0..255].[1..3],
permit_dnswl_client swl.spamhaus.org,
reject_rbl_client zen.spamhaus.org
service postfix restart
Spamhaus DQS - didn't work for me so we'll skip that.
At this point I got bounce mails from my first mail server saying the domain okbsd.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@okbsd.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.okbsd.com which has to match what is is the smtp_banner which should be myhostname variable. My first hostname in /etc/hosts is okbsd.com but the value in the banner comes from myhostname set in /usr/local/etc/postfix/main.cf which is mx.okbsd.com. After this I submitted a request to remove my domain from spamhaus RBL and used the postmaster@okbsd.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 /usr/local/etc/postfix/rbl_override
domain1.com OK // ignore rbl for domain1.com
domain2.com OK // ignore rbl for domain2.com
postmap /usr/local/etc/postfix/rbl_override
service postfix restart
You may see DNSSEC failures related to spamhaus in the logs. So turned off DNSSEC in your nameserver and possibly white listed spamhaus in /usr/local/etc/postfix/rbl_override. Some troubleshooting will be needed since you want DNSSEC enabled for opendkim keys.
grep URIBL /var/log/maillog
URIBL_BLOCKED hit, (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 to install a full recursive caching resolver that suppports DNSSEC, eg. setup bind nameserver, covered previously.
Check Email Headers and Body with SpamAssassin
The following files has rules for things like
* MISSING_HEADERS
* MISSING_DATE
* MISSING_FROM
Default spamassassin files are in /var/db/spamassassin/4.000001/updates_spamassassin_org
nano /var/db/spamassassin/4.000001/updates_spamassassin_org/20_head_tests.cf
no changes
SpamAssassin's Builtin Whitelist
There are several files under /var/db/spamassassin/4.000001/updates_spamassassin_org/ which contain builtin whitelists among other things.
cat /var/db/spamassassin/4.000001/updates_spamassassin_org/60_whitelist_spf.cf
Edit Local Scoring Rules - whitelist your own domains.
nano /usr/local/etc/mail/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 *@okbsd.com
whitelist_from *@coragarden.com
# whitelist_from jack@coragarden.com
# whitelist_from *@gooddomain.com
# blacklist_from spammer@example.com
# blacklist_from *@baddomain.org
# Show X-Spam-Status for all scores (default is 5)
required_score 5
Check the rules syntax and restart sa-spamd
spamassassin --lint
service spamass-milter restart
service sa-spamd restart
Schedule update of spamassassin rules using cron at 1 am daily
crontab -e
--- add to your crontab ---
0 1 * * * /usr/local/bin/sa-update && service sa-spamd restart
Moving spam to the junk folder.
We previously added sieve using dovecot-pigeonhole from ports.
nano /usr/local/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 /usr/local/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 = $mail_plugins quota sieve
}
nano /usr/local/etc/dovecot/conf.d/10-mail.conf
mail_home = /var/vmail/%d/%n
nano /usr/local/etc/dovecot/conf.d/90-plugin.conf
--- change sieve_before and enable by removeing hash ---
plugin {
#setting_name = value
sieve = file:~/sieve;active=~/.dovecot.sieve
sieve_default = /usr/local/etc/dovecot/sieve/default.sieve
sieve_global = /usr/local/etc/dovecot/sieve/global/
sieve_before = /usr/local/etc/dovecot/sieve/before-global.sieve
}
mkdir /usr/local/etc/dovecot/sieve
nano /usr/local/etc/dovecot/sieve/before-global.sieve
require "fileinto";
if header :contains "X-Spam-Flag" "YES"
{
fileinto "Junk";
stop;
}
sievec /usr/local/etc/dovecot/sieve/before-global.sieve
service dovecot restart
Reject emails with spamassassin scores > 8 using '-r 8' flags.
Set max-size ' -- --max-size=5120000'
nano /etc/rc.conf
spamd_enable="YES"
spamd_flags="--create-prefs --max-children 5 --helper-home-dir --nouser-config --virtual-config-dir=/var/vmail/%d/%l/spamassassin --username=vmail"
spamass_milter_enable="YES"
spamass_milter_user="spamd"
spamass_milter_group="spamd"
spamass_milter_socket="/var/spool/postfix/spamass/spamass.sock"
spamass_milter_socket_owner="spamd"
spamass_milter_socket_group="postfix"
spamass_milter_socket_mode="660"
spamass_milter_localflags="-e okbsd.com -r 8 -i 127.0.0.1 -u spamd -R REJECTED_AS_SPAM -- --max-size=5120000"
service spamass-milter restart
Configure Individual User Preferences
nano /usr/local/etc/mail/spamassassin/local.cf
# USER RULES ENABLED
allow_user_rules 1
service sa-spamd restart
service spamass-milter restart
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
service sa-spamd restart
service spamass-milter restart
# 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 *@okbsd.com
whitelist_from myfriend@gmail.com
blacklist_from *
spamassassin --lint
service sa-spamd restart
service spamass-milter restart
# Check URIBL_BLOCKED
# Send and email to postmaster@okbsd.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
grep DNS /var/log/maillog
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
options {
############################################# # Install Bind as a DNS caching name server # #############################################apt install bind920 cd /usr/local/etc/namedb sysrc named_enable="YES" service named start service named status rndc reload # Check bind setup, you want to enable recursion for the server itself and any networks you trust. Also enable DNSSEC. nano /usr/local/etc/namedb/named.conf
// All file and path names are relative to the chroot directory,
// if any, and should be fully qualified.
directory "/usr/local/etc/namedb/working";
pid-file "/var/run/named/pid";
dump-file "/var/dump/named_dump.db";
statistics-file "/var/stats/named.stats";
// DNSSEC
dnssec-validation auto;
// RECURSION
recursion yes;
allow-recursion {
127.0.0.1;
::1;
// okbsd external
147.135.37.135;
2604:2dc0:200:187::1;
// okdeb slave
15.204.113.148;
2604:2dc0:202:300::3645;
// my trusted home network(s)
trusted_net/cidr;
};
allow-query { any; };
// ...
listen-on {
127.0.0.1;
147.135.37.135;
};
listen-on-v6 {
::1;
2604:2dc0:200:187::1;
};
// if this is master, define slaves here
//allow-notify { 15.204.113.148; };
//allow-transfer { localhost; 15.204.113.148; };
//notify yes;
};
# Do not define any master zones unless you plan to run your own DNS, it's not needed for mail server setup.
rndc reload
# The root hints is enabled by
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 okbsd.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
nano /usr/local/etc/mail/spamassassin/local.cf
dns_server 127.0.0.1
spamassassin --lint
service sa-spamd restart
service spamass-milter restart
# If /etc/resolv.conf is a symbolic link, remove it and remake the file.
rm /etc/resolv.conf
nano /etc/resolv.conf
nameserver 127.0.0.1
options edns0 trust-ad
search .
dig okbsd.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)
# Block Outgoing Mail - Prevent server sending emails to certain addresses.
nano /usr/local/etc/postfix/header_checks
/^Received:/ IGNORE
/To:.*<>/ REJECT REJECT "5.7.1 Rejected, invalid To: header."
/From:.*<>/ REJECT REJECT "5.7.1 Rejected, invalid From: header."
/free mortgage quote/ REJECT
/repair your credit/ REJECT
/^To:.*badrecipientd.*/ DISCARD
# When using Thunderbird the outgoing mail headers show the external IP address of the client or the client's router which I found undesirable. Recieved IGNORE removes the initial Recieved line with the IP address.
nano /usr/local/etc/postfix/main.cf
header_checks = regexp:/usr/local/etc/postfix/header_checks
postmap /usr/local/etc/postfix/header_checks
service postfix restart
# Delete Outgoing headers
nano /usr/local/etc/postfix/smtp_header_checks
/^User-Agent.*Roundcube Webmail/ IGNORE
nano /usr/local/etc/postfix/main.cf
smtp_header_checks = pcre:/usr/local/etc/postfix/smtp_header_checks
header_checks = regexp:/usr/local/etc/postfix/header_checks
body_checks = pcre:/usr/local/etc/postfix/body_checks
postmap /usr/local/etc/postfix/smtp_header_checks
service postfix restart
# 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 /usr/local/etc/mail/spamassassin/local.cf. It went through because I had whitelisted the domain 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
10 Blocking Spam With Postfix <- Intro -> 12 Amavis Clam AntiVirus
########################################################## # Next Up Installing Amavis and ClamAV Antivirus Scanner # ##########################################################