Email Server - Part 2

More Raspberry Pi Projects

Project 3 — Email Server Part 2: TLS, SPF, DKIM, DMARC & Spam Filtering

2/2
Part 2 — Securing and hardening your mail server Assumes Part 1 is complete — Postfix, Dovecot, and Roundcube are running
What you will add TLS encryption for SMTP/IMAP, SPF + DKIM + DMARC email authentication, SpamAssassin spam filtering
Why it matters Without this, your email lands in spam and your passwords travel in plaintext
Time to complete 2 – 3 hours (plus DNS propagation time)
Prerequisite Project 2 (Email Server Part 1) fully working — Postfix, Dovecot, Roundcube all sending and receiving

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:

How receiving servers check your email
SPFSender Policy Framework
Is this IP allowed to send from your domain?
You publish a DNS TXT record listing which IP addresses are authorised to send email for your domain. The receiving server checks your DNS record against the sender IP.
→ pass / fail
DKIMDomainKeys Identified Mail
Has this email been tampered with in transit?
Your mail server signs every outgoing email with a private key. The public key is published in DNS. The receiving server uses it to verify the signature — if the email was modified in transit, the signature breaks.
→ pass / fail
DMARCDomain-based Message Authentication, Reporting & Conformance
What should receiving servers do if SPF or DKIM fails?
A DNS record that tells receivers your policy: none (monitor only), quarantine (put in spam), or reject (bounce the email). Also requests reports back to you about authentication failures.
→ policy

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:

SettingOld (Part 1)New (Part 2)
IMAP Servermail.yourdomain.commail.yourdomain.com
IMAP Port143 (unencrypted)993 (SSL/TLS)
SMTP Port587 (STARTTLS)587 (STARTTLS) — unchanged
SecurityNone / STARTTLSSSL/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:

DNS TXT record — add at your registrar TXT @ "v=spf1 ip4:YOUR.PUBLIC.IP.ADDRESS -all"

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:

SPF with additional sending service "v=spf1 ip4:203.0.113.42 include:sendgrid.net -all"

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)
Start with ~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:

DNS TXT record — DKIM public key TXT mail._domainkey "v=DKIM1; h=sha256; k=rsa; t=y; p=MIIBIjANBgkqhk..."
The 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:

DNS TXT record — DMARC policy (start here) TXT _dmarc "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com; ruf=mailto:dmarc@yourdomain.com; fo=1"

The tags explained:

TagValueMeaning
vDMARC1Protocol version — always this value
pnonePolicy: do nothing on failure (monitor only). Change to quarantine or reject later.
ruamailto:…Aggregate reports — daily XML digest of all DMARC results (good for overview)
rufmailto:…Forensic reports — individual failure reports (can be verbose)
fo1Generate 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 to dmarc@yourdomain.com.
  • After a week: p=quarantine; pct=25 — quarantine (spam) 25% of failing emails. Gradually raise pct.
  • 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

✓ SPF pass ✓ DKIM pass ✓ DMARC pass ✓ TLS on 587 ✓ IMAPS on 993 ? PTR record ? IP not blocklisted

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

⚠ Email still goes to spam despite SPF, DKIM, and DMARC passing
This is almost always a reputation issue, not a configuration issue. Your IP is either new (no sending history), on a blocklist, or is a residential IP with no PTR record. Check blocklists at mxtoolbox.com/blacklists.aspx. If you're on a list, contact the list operator — they all have delisting forms. IP reputation takes weeks or months to build. Consider sending a small, consistent volume of legitimate email to gradually build trust.
⚠ "DKIM verification failed" in receiving server headers
Check that the DKIM DNS record matches the key that OpenDKIM is using. Common causes: (1) the key in DNS still has 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.
⚠ SPF passes but DMARC shows "SPF alignment failure"
DMARC requires "alignment" — the domain in the SPF pass must match the domain in the 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.
⚠ Postfix won't start after adding TLS config — "cannot load certificate"
The certificate path is wrong, the file doesn't exist yet, or Postfix can't read it. Check: (1) the path in main.cf is exactly right: 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.
⚠ SpamAssassin is marking legitimate email as spam
The default threshold of 5.0 may be too aggressive for some legitimate senders. Raise it to 6.0 or 7.0 in 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.
⚠ Dovecot won't connect after enabling TLS — "SSL_CTX_use_certificate_file() failed"
Dovecot can't read the certificate. The < 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.
⚠ DMARC reports are not arriving
Receiving servers send DMARC reports to the 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.
⚠ OpenDKIM fails to start — "Key is not secure"
The private key file has permissions that are too open. Fix: 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:

A record — points mail.yourdomain.com to your Pi A mail 203.0.113.42
MX record — tells other servers where to deliver your email MX @ 10 mail.yourdomain.com.
SPF TXT record — lists authorised sending IPs TXT @ "v=spf1 ip4:203.0.113.42 -all"
DKIM TXT record — public key for signature verification TXT mail._domainkey "v=DKIM1; h=sha256; k=rsa; p=MIIBIjAN..."
DMARC TXT record — authentication policy and reporting TXT _dmarc "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com; fo=1"

Quick Reference

TaskCommand / Location
Check Postfix TLS statusopenssl s_client -connect mail.yourdomain.com:587 -starttls smtp
Check Dovecot TLS statusopenssl s_client -connect mail.yourdomain.com:993
Verify DKIM key matches DNSsudo opendkim-testkey -d yourdomain.com -s mail -vvv
Check SPF DNS recorddig TXT yourdomain.com +short
Check DKIM DNS recorddig TXT mail._domainkey.yourdomain.com +short
Check DMARC DNS recorddig TXT _dmarc.yourdomain.com +short
View OpenDKIM logsudo journalctl -u opendkim -f
Update SpamAssassin rulessudo 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 scoremail-tester.com
Check IP blocklist statusmxtoolbox.com/blacklists
Analyse DMARC reportsdmarcian.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