Thank you for visiting!
My little window on internet allowing me to share several of my passions
Categories:
- OpenBSD
- FreeBSD
- fapws
- Nvim
- Firewall
- got
- PEKwm
- Zsh
- VM
- High Availability
- vdcron
- My Sysupgrade
- Nas
- VPN
- DragonflyBSD
- Alpine Linux
- Openbox
- Desktop
- Security
- yabitrot
- nmctl
- Tint2
- Project Management
- Hifi
- Alarm
Most Popular Articles:
Last Articles:
Create a local email server with strong spam filter
Posted on 2026-04-05 17:52:00 from Vincent in OpenBSD

This blog provides the precise configuration required to build a localized, self-learning mail server on OpenBSD. We assume your mail is hosted on a remote IMAP service and you want to filter it locally before your best email client (K-9 Mail in my case) ever rings.
This is an intermediary setup where I keep my provider as MX records, but I'm allowed to perform my own spam filtering.
Then by using imap server, I can publish cleaned inbox.
Introduction
In an era of bloated webmail and aggressive tracking, there is a certain quiet satisfaction in pulling your digital life back onto your own hardware. This guide explains how to build a localized mail processing pipeline using three Unix-centric tools: mbsync, Dovecot, and Bogofilter.
The Architecture
Our "Mail Stack" functions as a three-stage pipeline:
1. mbsync (The Retriever): Replicates from remote IMAP mailbox (dns provider, email provider, ...) to my local disk.
2. Dovecot (The Librarian): Serves those local files back to your devices (like K-9 Mail on Android) via a local IMAP server.
3. Bogofilter (The Sentry): A Bayesian filter that "learns" from your habits to automatically sort spam.
1. The Retriever: mbsync
mbsync (part of the isync package) is faster and more robust than the older offlineimap. It treats your remote mail and local mail as two "Stores" that must remain identical.
Install the isync package. mbsync is our synchronization engine.
$ doas pkg_add isync
$ mkdir -p ~/Mail/myfolder
Create a file ~/.mbsyncrc like this
# isync/mbsync 1.5.1 config for my email's provider
# Replace the placeholders with your actual values.
##############################################
# Global settings
##############################################
# Store your password in a GPG-encrypted file
# or use a PassCmd. See the "Passwords" section below.
##############################################
# IMAP Account
##############################################
IMAPAccount cloud
Host imap.cloud.com
Port 993
User user@yourdomain.com
PassCmd "gpg2 --quiet --for-your-eyes-only --no-tty --decrypt ~/.config/mbsync/cloud.gpg"
TLSType IMAPS
CertificateFile /etc/ssl/cert.pem
##############################################
# Remote store (IMAP side)
##############################################
IMAPStore cloud-remote
Account cloud
##############################################
# Local store (Maildir on disk)
##############################################
MaildirStore cloud-local
Path ~/Mail/myfolder/
Inbox ~/Mailmyfolder/
##############################################
# Channel: ties remote and local together
##############################################
Channel cloud
Far :cloud-remote:
Near :cloud-local:
Patterns *
Create Both
Expunge Both
SyncState *
You can heck it by doing:
$ mbsync -a
You should not see errors
2. The Librarian: Dovecot
Why run a local server? It allows you to process mail (filter spam) on my OpenBSD box before my phone ever sees it.
Dovecot serves my local ~/Mail.
File: /etc/dovecot/local.conf (Or equivalent in conf.d) must regroup those parameters
those are spread into different configuration file located at /etc/dovecot/conf.d
# in 10-mail.conf makes sure you have at least this
first_valid_uid = 1000
mail_location = maildir:~/Mail/myfolder:LAYOUT=fs
# in 10-ssl.conf
ssl_cert = </etc/ssl/server.fullchain.pem
ssl_key = </etc/ssl/private/server.key
# in 10-auth.conf
#!include auth-system.conf.ext #commented to avoid unix like auth
!include auth-passwdfile.conf.ext #uncomment to force /etc/dovecot/users
# auth-passwdfile.conf.ext must looks like this
passdb {
driver = passwd-file
args = scheme=BLF-CRYPT username_format=%u /etc/dovecot/users
}
userdb {
driver = passwd-file
args = username_format=%u /etc/dovecot/users
}
# in 10-logging.conf makes sure you have
log_path = /var/log/dovecot.log
In this case, I'm using a dedicated user file called /etc/dovecot/users.
This file has the same structure of /etc/passwd file.
You must have it like this:
<user>:{BLF-CRYPT}$2y$05$K...:1000:1000::/home/<user>
Where the hash key is create by this command:
$ doas doveadm pw -s BLF-CRYPT -p password >> /etc/dovecot/users
To avoid limitation errors, add this to the end of the /etc/login.conf file:
_dovecot:\
:openfiles-cur=1024:\
:openfiles-max=2048:\
:tc=daemon:
Then you an enable and start dovecot:
$ doas rcctl enable dovecot
$ doas rcctl start dovecot
You can follow details in /var/log/dovecot.log
In my specific case public key is managed by acme-client.
So, do not forget to reload dovecot after each recreation. This is either in cron or in /etc/daily.local or any other file triggering the certificate renewal.
acme-client -v mydomain.come && rcctl reload dovecot
3. The Brain: Bogofilter
Bogofilter doesn't use static rules. It calculates the "Spamicity" of an email based on words it has seen before.
On OpenBSD, use the LMDB version for high-speed database access.
$ doas pkg_add bogofilter-lmdb
Training Logic: Bogofilter is "Bayesian." It needs to know what Spam looks like AND what Good (Ham) mail looks like. Since you send mail via your phone's SMTP, your best "Good" data is your existing Inbox.
This can be done by doing bogofilter -s -B /path/to/spam or bogofilter -n -B /path/to/good/mails
So to simplify, here a small script doing all in once:
File: ~/.local/bin/process-mail.sh
#!/bin/sh
# Paths
MAILDIR="$HOME/Mail/myfolder"
SPAMDIR="$MAILDIR/Spam"
BOGOFILTER="/usr/local/bin/bogofilter"
mv_to_spam()
{
msg=$1
# mktemp creates a unique file; we then rename it with a .spam suffix
dest=$(mktemp "$SPAMDIR/new/$(date +%s).XXXXXX.spam")
mv "$msg" "$dest"
}
# 1. Sync Remote to Local
/usr/local/bin/mbsync -a -q
# 2. Learn from what you manually moved to Spam (only new/unprocessed ones)
for f in "$SPAMDIR/new/"*; do
[ -f "$f" ] || continue
$BOGOFILTER -s < "$f" 2>/dev/null
mv "$f" "$SPAMDIR/cur/$(basename "$f")"
done
# 3. Filtering Phase (Processing New Mail)
if [ -d "$MAILDIR/new" ]; then
for msg in "$MAILDIR/new/"*; do
[ -f "$msg" ] || continue
# -u: unsupervised learning (auto-updates wordlist)
# -e: exit code reflects spam/ham/unsure
# -p: passthrough, appending X-Bogosity header
RESULT=$($BOGOFILTER -u -e -p < "$msg" 2>/dev/null)
EXIT_CODE=$?
# Exit code 3 = error (e.g. malformed/Base64 crash)
# Treat as spam to be safe
if [ $EXIT_CODE -eq 3 ]; then
mv_to_spam "$msg"
continue
fi
if echo "$RESULT" | grep -q "X-Bogosity: Spam"; then
echo "$RESULT" > "$msg"
mv_to_spam "$msg"
else
# Ham or Unsure: keep in new, update header in place
echo "$RESULT" > "$msg"
fi
done
fi
To keep your inbox clean, run this every 5 minutes.
Command: crontab -e
*/5 * * * * $HOME/.local/bin/process-mail.sh
If you have lot of email already "read", you can train bogofilter by doing this:
# Learn from what you have in Spam
$BOGOFILTER -s -B "$SPAMDIR/cur/"* 2>/dev/null
# Learn good emails
$BOGOFILTER -n -B "$MAILDIR/cur/"* 2>/dev/null
Why This Setup is Superior
- The Error Catcher: The
EXIT_CODEcheck is vital on OpenBSD. Malformed phishing (massive Base64 blocks) can trigger memory faults in many parsers. By treating a crash as "Spam," you prevent your script from hanging. - The "Ham" Baseline: By training on your
cur/inbox after you've manually cleared out the junk, you provide Bogofilter with a high-quality "Good" baseline. - Notifications: Because the script runs on the server, K-9 Mail on your phone only triggers a notification for emails that passed the Bogofilter test.
How to verify your "Brain"
Run this command to see your message counts:
$ bogoutil -w ~/.bogofilter/wordlist.lmdb .MSG_COUNT
spam good
.MSG_COUNT 1533 31
A healthy setup will show hundreds of spams and dozens (or hundreds) of good emails. As those numbers grow, the "Unsure" results will vanish.
On the k9-mail client app
You can just provide this new machine name/ip with the user and password as defined in /etc/dovecot/users
It should display your emails without Spam. In case you have some, you can move those emails to the "Spam" folder.
And every 5 minutes Bogofilter will take it into account and increase his spam Bayesian algorithm.
Conclusion
My OpenBSD mail stack acts as a "Sovereign Filter," using mbsync to create a bidirectional mirror between the cloud and my local disk. By running a script every five minutes, Bogofilter inspects new arrivals and "rebirths" spam into the Spam folder.
Because mbsync synchronizes these changes back to my email's provider, my remote inbox is automatically scrubbed of spams. While I could point my phone directly at my email provider for an "auto-cleaning" experience, routing K-9 Mail through a local Dovecot instance ensures a truly silent inbox where spam is intercepted before a notification ever pings. This architecture transforms a passive email account into a private, self-learning system controlled entirely by my own hardware.