Email Server - Part 2
More Raspberry Pi Projects
Project 3 — Email Server Part 2: TLS, SPF, DKIM, DMARC & Spam Filtering
The Email Authentication Problem
When you sent a test email from your Pi in Part 1, it probably arrived in the recipient's spam folder. That's not a configuration error — it's because email servers have no reason to trust you yet. The internet's email authentication system works through three interlocking standards, each solving a different part of the problem:
none (monitor only), quarantine (put in spam), or reject (bounce the email). Also requests reports back to you about authentication failures.These three work together. SPF confirms the sending server. DKIM confirms the message content. DMARC ties them together and tells receiving servers what to do when checks fail — and asks them to send you reports so you can monitor your deliverability.
Step 1 — Enable TLS for Postfix (SMTP)
Part 1 installed Postfix without TLS — passwords and email content sent on port 587 were unencrypted. Let's Encrypt gives you a free certificate, and you may already have one if you completed the Nextcloud or Roundcube setup.
Obtain or reuse a certificate
# If you already have a cert for mail.yourdomain.com, skip this step pi@raspberrypi:~$ sudo certbot certonly --standalone -d mail.yourdomain.com # Note: Certbot standalone uses port 80 — stop Apache briefly if needed: pi@raspberrypi:~$ sudo systemctl stop apache2 pi@raspberrypi:~$ sudo certbot certonly --standalone -d mail.yourdomain.com pi@raspberrypi:~$ sudo systemctl start apache2
If you already have Apache running with Certbot, use the webroot method instead (no downtime):
pi@raspberrypi:~$ sudo certbot certonly --webroot -w /var/www/html -d mail.yourdomain.com
Configure Postfix to use TLS
pi@raspberrypi:~$ sudo nano /etc/postfix/main.cf
Add these lines at the end of the file:
# ── TLS for incoming connections (other servers sending to you) ── smtpd_tls_cert_file = /etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem smtpd_tls_key_file = /etc/letsencrypt/live/mail.yourdomain.com/privkey.pem smtpd_tls_security_level = may # offer TLS but don't require it (for compatibility) smtpd_tls_loglevel = 1 smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache # ── TLS for outgoing connections (you sending to other servers) ── smtp_tls_cert_file = /etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem smtp_tls_key_file = /etc/letsencrypt/live/mail.yourdomain.com/privkey.pem smtp_tls_security_level = may # use TLS if the receiving server supports it smtp_tls_loglevel = 1 smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
pi@raspberrypi:~$ sudo systemctl restart postfix
Allow Postfix to read the Let's Encrypt certificates
Let's Encrypt certificates are only readable by root by default. Postfix runs as the postfix user, so you need to grant access:
pi@raspberrypi:~$ sudo chmod 750 /etc/letsencrypt/{live,archive} pi@raspberrypi:~$ sudo chmod 640 /etc/letsencrypt/live/mail.yourdomain.com/privkey.pem pi@raspberrypi:~$ sudo chgrp -R ssl-cert /etc/letsencrypt/{live,archive} pi@raspberrypi:~$ sudo usermod -aG ssl-cert postfix
Step 2 — Enable TLS for Dovecot (IMAP)
pi@raspberrypi:~$ sudo nano /etc/dovecot/conf.d/10-ssl.conf
ssl = required # require TLS for all IMAP connections ssl_cert = </etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem ssl_key = </etc/letsencrypt/live/mail.yourdomain.com/privkey.pem ssl_min_protocol = TLSv1.2
pi@raspberrypi:~$ sudo nano /etc/dovecot/conf.d/10-auth.conf
Now that TLS is required, switch this back on:
disable_plaintext_auth = yes # safe now — connections are encrypted
pi@raspberrypi:~$ sudo systemctl restart dovecot
Update email client settings
In your email clients (Outlook, Thunderbird, iPhone), update the IMAP settings to use SSL/TLS on port 993 instead of the unencrypted port 143 you used in Part 1:
| Setting | Old (Part 1) | New (Part 2) |
|---|---|---|
| IMAP Server | mail.yourdomain.com | mail.yourdomain.com |
| IMAP Port | 143 (unencrypted) | 993 (SSL/TLS) |
| SMTP Port | 587 (STARTTLS) | 587 (STARTTLS) — unchanged |
| Security | None / STARTTLS | SSL/TLS for IMAP |
Update Roundcube to use IMAPS
pi@raspberrypi:~$ sudo nano /etc/roundcube/config.inc.php
$config['imap_host'] = 'ssl://localhost:993'; // was tls://localhost:143
Step 3 — Add an SPF Record
SPF is the simplest of the three standards — it's just a DNS TXT record. Log in to your domain registrar and add:
Replace YOUR.PUBLIC.IP.ADDRESS with the public IP address of your Pi (the same IP as your mail.yourdomain.com A record).
If you ever send mail via another service (e.g. a relay), include it too:
The mechanism at the end matters:
-all— hard fail: any IP not listed is definitely not authorised (recommended)~all— soft fail: any unlisted IP is probably not authorised (treated as spam, not rejected)?all— neutral: no opinion (rarely useful)
~all (soft fail) while you're getting set up, then tighten to -all once you've confirmed everything is working. A wrong SPF record with -all will bounce legitimate email from your domain.
Verify SPF
pi@raspberrypi:~$ dig TXT yourdomain.com +short "v=spf1 ip4:203.0.113.42 -all"
Step 4 — Set Up DKIM with OpenDKIM
DKIM requires more work than SPF, but it's the most important part — Gmail and Outlook both heavily weight DKIM when deciding whether email is legitimate.
pi@raspberrypi:~$ sudo apt install -y opendkim opendkim-tools
Configure OpenDKIM
pi@raspberrypi:~$ sudo nano /etc/opendkim.conf
Replace the contents with:
AutoRestart Yes AutoRestartRate 10/1h UMask 002 Syslog yes SyslogSuccess Yes LogWhy Yes Canonicalization relaxed/simple ExternalIgnoreList refile:/etc/opendkim/TrustedHosts InternalHosts refile:/etc/opendkim/TrustedHosts KeyTable refile:/etc/opendkim/KeyTable SigningTable refile:/etc/opendkim/SigningTable Mode sv # sign outgoing, verify incoming PidFile /run/opendkim/opendkim.pid SignatureAlgorithm rsa-sha256 UserID opendkim:opendkim Socket inet:12301@localhost # Postfix will connect here
Create the DKIM directory structure
pi@raspberrypi:~$ sudo mkdir -p /etc/opendkim/keys/yourdomain.com
Create trusted hosts list
pi@raspberrypi:~$ sudo nano /etc/opendkim/TrustedHosts
127.0.0.1 localhost yourdomain.com mail.yourdomain.com
Generate the DKIM key pair
The selector name (mail below) can be anything — it's just a label in DNS that links to the key. You can have multiple selectors if you ever need to rotate keys.
pi@raspberrypi:~$ sudo opendkim-genkey -t -s mail -d yourdomain.com -D /etc/opendkim/keys/yourdomain.com/ pi@raspberrypi:~$ sudo chown -R opendkim:opendkim /etc/opendkim/keys/ pi@raspberrypi:~$ sudo chmod 600 /etc/opendkim/keys/yourdomain.com/mail.private
Create the key table and signing table
pi@raspberrypi:~$ sudo nano /etc/opendkim/KeyTable
mail._domainkey.yourdomain.com yourdomain.com:mail:/etc/opendkim/keys/yourdomain.com/mail.private
pi@raspberrypi:~$ sudo nano /etc/opendkim/SigningTable
*@yourdomain.com mail._domainkey.yourdomain.com
Connect OpenDKIM to Postfix
pi@raspberrypi:~$ sudo nano /etc/postfix/main.cf
Add at the end:
# ── DKIM signing via OpenDKIM milter ──────────────────────────── milter_protocol = 2 milter_default_action = accept smtpd_milters = inet:localhost:12301 non_smtpd_milters = inet:localhost:12301
pi@raspberrypi:~$ sudo systemctl restart opendkim pi@raspberrypi:~$ sudo systemctl restart postfix
Get the DKIM public key and add it to DNS
pi@raspberrypi:~$ sudo cat /etc/opendkim/keys/yourdomain.com/mail.txt mail._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; t=y; " "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..." "...more key data..." )
Copy the value inside the parentheses and add it to DNS as a TXT record. Some registrars want it on one line without quotes — combine the string parts:
t=y flag in the key record marks DKIM as being in test mode — receiving servers will check the signature but won't act on failures. Once you've verified everything is working, remove t=y; from the DNS record to go live.
Step 5 — Add a DMARC Record
DMARC ties SPF and DKIM together. Start with a monitoring-only policy (p=none) so you can review reports before enforcing anything:
The tags explained:
| Tag | Value | Meaning |
|---|---|---|
v | DMARC1 | Protocol version — always this value |
p | none | Policy: do nothing on failure (monitor only). Change to quarantine or reject later. |
rua | mailto:… | Aggregate reports — daily XML digest of all DMARC results (good for overview) |
ruf | mailto:… | Forensic reports — individual failure reports (can be verbose) |
fo | 1 | Generate a forensic report if SPF or DKIM fails (not just when both fail) |
Progression path
Once you've been running on p=none for a week or two and DMARC reports show clean results, tighten the policy progressively:
- Start:
p=none— monitor, no enforcement. Review the daily reports sent todmarc@yourdomain.com. - After a week:
p=quarantine; pct=25— quarantine (spam) 25% of failing emails. Gradually raisepct. - When confident:
p=reject— reject all emails that fail SPF and DKIM. Maximum protection.
Step 6 — Install SpamAssassin
SpamAssassin scores incoming email on hundreds of heuristic rules and adds headers that Postfix (and Roundcube) can act on. It won't catch everything, but it catches most obvious spam.
pi@raspberrypi:~$ sudo apt install -y spamassassin spamc
Enable and configure SpamAssassin
pi@raspberrypi:~$ sudo nano /etc/default/spamassassin
ENABLED=1 CRON=1 # automatically update spam rules daily
pi@raspberrypi:~$ sudo nano /etc/spamassassin/local.cf
# ── SpamAssassin local config ──────────────────────────────────── rewrite_header Subject **SPAM** # prepend subject on spam report_safe 0 # attach spam as attachment, don't modify required_score 5.0 # score threshold for spam classification use_bayes 1 # use Bayesian classifier bayes_auto_learn 1 # automatically learn from new mail skip_rbl_checks 0 # check DNS blocklists use_razor2 0 # disable razor (requires extra install) use_dcc 0 use_pyzor 0
Connect SpamAssassin to Postfix
Postfix passes mail to SpamAssassin via the spamc wrapper in its SMTP chain. Edit the master.cf to add a content filter:
pi@raspberrypi:~$ sudo nano /etc/postfix/master.cf
Add after the smtp service line:
smtp inet n - y - - smtpd -o content_filter=spamassassin # Add this new service entry at the bottom of the file: spamassassin unix - n n - - pipe user=debian-spamd argv=/usr/bin/spamc -f -e /usr/sbin/sendmail -oi -f ${sender} ${recipient}
pi@raspberrypi:~$ sudo systemctl start spamassassin pi@raspberrypi:~$ sudo systemctl enable spamassassin pi@raspberrypi:~$ sudo systemctl restart postfix
Update SpamAssassin rules
pi@raspberrypi:~$ sudo sa-update pi@raspberrypi:~$ sudo systemctl restart spamassassin
Step 7 — Verify Everything
Test your email authentication score
Send a test email to mail-tester.com — it gives you a score out of 10 and tells you exactly what's passing and failing. A fully configured server should score 9–10.
You can also use mxtoolbox.com/emailhealth or dmarcian.com to inspect individual records.
Check your score chips
The two amber items — PTR (reverse DNS) and IP blocklisting — are often outside your control on a residential connection. SPF + DKIM + DMARC will make a significant difference on their own.
Check DKIM is signing outgoing mail
pi@raspberrypi:~$ echo "DKIM test" | mail -s "DKIM test" your.gmail@gmail.com
In Gmail, open the received email → click the three dots → Show original. You should see:
ARC-Authentication-Results: ... dkim=pass header.i=@yourdomain.com header.s=mail spf=pass (google.com: domain of philip@yourdomain.com designates 203.0.113.42 as permitted sender)
Check TLS on Postfix
pi@raspberrypi:~$ openssl s_client -connect mail.yourdomain.com:587 -starttls smtp ... SSL handshake has read 3072 bytes and written 415 bytes Verification: OK --- New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Step 8 — Auto-Renew Certificates
Let's Encrypt certificates expire after 90 days. Certbot installs a renewal timer, but you need to ensure Postfix and Dovecot reload their certificates after each renewal. Add a deploy hook:
pi@raspberrypi:~$ sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-mail.sh
#!/bin/bash systemctl reload postfix systemctl reload dovecot
pi@raspberrypi:~$ sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-mail.sh # Test the renewal process (dry run — doesn't actually renew): pi@raspberrypi:~$ sudo certbot renew --dry-run Congratulations, all renewals succeeded.
Troubleshooting Deliverability Problems
t=y (test mode) and some servers treat test-mode failures differently; (2) the DNS record was added incorrectly — extra quotes, missing the p= value, or the name was mail._domainkey.yourdomain.com instead of mail._domainkey (with the domain automatically appended). Verify the record with: dig TXT mail._domainkey.yourdomain.com +short, then run sudo opendkim-testkey -d yourdomain.com -s mail -vvv.From: header. If your server is sending as @yourdomain.com but the return-path (envelope sender) uses a subdomain like @mail.yourdomain.com, SPF passes but alignment fails. Fix: set myorigin = $mydomain in Postfix main.cf (not $myhostname) so the envelope sender matches the From domain.ls -la /etc/letsencrypt/live/mail.yourdomain.com/; (2) Postfix's user is in the ssl-cert group: groups postfix — if not, re-run the usermod command and reboot; (3) check the exact error: sudo journalctl -u postfix -n 30.local.cf: required_score 6.5. Alternatively, whitelist trusted senders: add whitelist_from trusted@example.com to local.cf. You can also train the Bayesian filter by running sa-learn --spam /path/to/spam/folder and sa-learn --ham /path/to/ham/folder.< prefix in ssl_cert = </path/to/cert is intentional — it tells Dovecot to read the file (don't remove it). Check file permissions: sudo ls -la /etc/letsencrypt/live/mail.yourdomain.com/. Add Dovecot to the ssl-cert group: sudo usermod -aG ssl-cert dovecot, then restart.rua address once per day, usually in the early morning UTC. Wait 24–48 hours after setting up the DMARC record before expecting reports. Also ensure that dmarc@yourdomain.com is a valid mailbox (create a Linux user named dmarc or alias it to an existing user in /etc/aliases with newaliases). Consider using a free DMARC analysis service like dmarcian.com for easier report reading.sudo chmod 600 /etc/opendkim/keys/yourdomain.com/mail.private and sudo chown opendkim:opendkim /etc/opendkim/keys/yourdomain.com/mail.private. OpenDKIM refuses to use keys that could be read by other users.Your Complete Mail Server DNS Records
Here's a summary of all DNS records your mail server needs:
Quick Reference
| Task | Command / Location |
|---|---|
| Check Postfix TLS status | openssl s_client -connect mail.yourdomain.com:587 -starttls smtp |
| Check Dovecot TLS status | openssl s_client -connect mail.yourdomain.com:993 |
| Verify DKIM key matches DNS | sudo opendkim-testkey -d yourdomain.com -s mail -vvv |
| Check SPF DNS record | dig TXT yourdomain.com +short |
| Check DKIM DNS record | dig TXT mail._domainkey.yourdomain.com +short |
| Check DMARC DNS record | dig TXT _dmarc.yourdomain.com +short |
| View OpenDKIM log | sudo journalctl -u opendkim -f |
| Update SpamAssassin rules | sudo sa-update && sudo systemctl restart spamassassin |
| Train spam filter (spam) | sudo sa-learn --spam /path/to/spam |
| Train spam filter (ham) | sudo sa-learn --ham /path/to/ham |
| Test full email score | mail-tester.com |
| Check IP blocklist status | mxtoolbox.com/blacklists |
| Analyse DMARC reports | dmarcian.com |
| OpenDKIM config | /etc/opendkim.conf |
| DKIM private key | /etc/opendkim/keys/yourdomain.com/mail.private |
| SpamAssassin local rules | /etc/spamassassin/local.cf |
| Certbot deploy hook (reload) | /etc/letsencrypt/renewal-hooks/deploy/reload-mail.sh |