FreeBSD Mail Server Administration Guide
Running your own mail server on FreeBSD gives you complete control over your email infrastructure -- no third-party scanning your messages, no storage limits, no monthly fees per mailbox. The trade-off is operational complexity. Email has more moving parts than almost any other server role: MTA, IMAP, spam filtering, TLS, DNS records, authentication protocols, and deliverability reputation.
This guide builds a production mail server on FreeBSD using Postfix (MTA), Dovecot (IMAP/POP3), and rspamd (spam filtering and DKIM signing). It covers virtual domain hosting, TLS encryption, DKIM/SPF/DMARC, and webmail.
Architecture Overview
The mail stack:
| Component | Software | Role |
|---|---|---|
| MTA | Postfix | Sends and receives email via SMTP |
| IMAP/POP3 | Dovecot | Serves mailboxes to clients |
| Spam filter | rspamd | Filters spam, signs DKIM, checks SPF |
| Webmail | Roundcube | Browser-based email access |
| TLS | Let's Encrypt | Certificates for all services |
| DNS | Your DNS | MX, SPF, DKIM, DMARC records |
Mail flow:
- Incoming: Internet -> Postfix (port 25) -> rspamd -> Dovecot (delivery)
- Outgoing: Client -> Postfix (port 587) -> rspamd (DKIM sign) -> Internet
- Reading: Client -> Dovecot (port 993 IMAP / 995 POP3)
Prerequisites
Before installing anything, set up DNS records for your mail domain. These are essential for deliverability.
DNS Records
shell; MX record -- tells the world where to deliver mail example.com. IN MX 10 mail.example.com. ; A record for the mail server mail.example.com. IN A 203.0.113.10 ; AAAA record (if you have IPv6) mail.example.com. IN AAAA 2001:db8::10 ; SPF record -- authorizes your server to send mail example.com. IN TXT "v=spf1 mx -all" ; DMARC record -- policy for failed authentication _dmarc.example.com. IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@example.com" ; PTR record (reverse DNS) -- set this with your hosting provider ; 10.113.0.203.in-addr.arpa. IN PTR mail.example.com.
The PTR record is critical. Many mail servers reject or flag email from IPs without a matching PTR record. Contact your hosting provider to set this.
DKIM records will be added after generating keys.
Installing the Stack
shpkg install postfix dovecot rspamd redis roundcube-php83 \ php83-imap php83-mbstring php83-intl php83-pdo_pgsql \ nginx acme.sh
Disable sendmail (FreeBSD's default MTA):
shsysrc sendmail_enable="NONE" sysrc sendmail_submit_enable="NO" sysrc sendmail_outbound_enable="NO" sysrc sendmail_msp_queue_enable="NO" service sendmail onestop
Enable services:
shsysrc postfix_enable="YES" sysrc dovecot_enable="YES" sysrc rspamd_enable="YES" sysrc redis_enable="YES" sysrc nginx_enable="YES" sysrc php_fpm_enable="YES"
Set Postfix as the system mailer:
shcat >> /etc/mail/mailer.conf << 'EOF' sendmail /usr/local/sbin/sendmail mailq /usr/local/bin/mailq newaliases /usr/local/bin/newaliases EOF
TLS Certificates
Get certificates from Let's Encrypt before configuring services:
shacme.sh --issue -d mail.example.com --standalone # Install certificates where services can find them mkdir -p /usr/local/etc/ssl/mail acme.sh --install-cert -d mail.example.com \ --key-file /usr/local/etc/ssl/mail/key.pem \ --fullchain-file /usr/local/etc/ssl/mail/fullchain.pem \ --reloadcmd "service postfix reload; service dovecot reload; service nginx reload"
Postfix Configuration
Main Configuration
shcat > /usr/local/etc/postfix/main.cf << 'EOF' # Basic settings myhostname = mail.example.com mydomain = example.com myorigin = $mydomain mydestination = localhost mynetworks = 127.0.0.0/8 [::1]/128 # Virtual domains virtual_mailbox_domains = example.com, example.org virtual_mailbox_base = /var/mail/vhosts virtual_mailbox_maps = hash:/usr/local/etc/postfix/vmailbox virtual_alias_maps = hash:/usr/local/etc/postfix/valias virtual_minimum_uid = 1000 virtual_uid_maps = static:1000 virtual_gid_maps = static:1000 # TLS (incoming) smtpd_tls_cert_file = /usr/local/etc/ssl/mail/fullchain.pem smtpd_tls_key_file = /usr/local/etc/ssl/mail/key.pem smtpd_tls_security_level = may smtpd_tls_auth_only = yes smtpd_tls_mandatory_protocols = >=TLSv1.2 smtpd_tls_mandatory_ciphers = high smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache # TLS (outgoing) smtp_tls_security_level = may smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache smtp_tls_mandatory_protocols = >=TLSv1.2 # SASL authentication (via Dovecot) smtpd_sasl_type = dovecot smtpd_sasl_path = private/auth smtpd_sasl_auth_enable = yes smtpd_sasl_security_options = noanonymous smtpd_sasl_local_domain = $myhostname # Restrictions smtpd_helo_required = yes smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, reject_invalid_hostname, reject_non_fqdn_sender, reject_non_fqdn_recipient, reject_unknown_sender_domain, reject_unknown_recipient_domain # rspamd milter smtpd_milters = inet:127.0.0.1:11332 non_smtpd_milters = inet:127.0.0.1:11332 milter_protocol = 6 milter_default_action = accept milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} # Limits message_size_limit = 52428800 mailbox_size_limit = 0 recipient_delimiter = + # Delivery via Dovecot LMTP virtual_transport = lmtp:unix:private/dovecot-lmtp EOF
Virtual Mailbox Map
shcat > /usr/local/etc/postfix/vmailbox << 'EOF' user@example.com example.com/user/ admin@example.com example.com/admin/ info@example.com example.com/info/ user@example.org example.org/user/ EOF postmap /usr/local/etc/postfix/vmailbox
Virtual Alias Map
shcat > /usr/local/etc/postfix/valias << 'EOF' postmaster@example.com admin@example.com abuse@example.com admin@example.com webmaster@example.com admin@example.com @example.com admin@example.com EOF postmap /usr/local/etc/postfix/valias
Submission Port (587)
Edit /usr/local/etc/postfix/master.cf to enable the submission port for authenticated sending:
shcat >> /usr/local/etc/postfix/master.cf << 'EOF' submission inet n - n - - smtpd -o syslog_name=postfix/submission -o smtpd_tls_security_level=encrypt -o smtpd_sasl_auth_enable=yes -o smtpd_tls_auth_only=yes -o smtpd_reject_unlisted_recipient=no -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject -o milter_macro_daemon_name=ORIGINATING EOF
Create Mail Directory Structure
shmkdir -p /var/mail/vhosts/example.com mkdir -p /var/mail/vhosts/example.org pw groupadd vmail -g 1000 pw useradd vmail -u 1000 -g vmail -d /var/mail/vhosts -s /usr/sbin/nologin chown -R vmail:vmail /var/mail/vhosts
Dovecot Configuration
Main Configuration
shcat > /usr/local/etc/dovecot/dovecot.conf << 'EOF' protocols = imap lmtp # Listen listen = *, :: # Mail location mail_location = maildir:/var/mail/vhosts/%d/%n/Maildir mail_uid = vmail mail_gid = vmail mail_privileged_group = vmail first_valid_uid = 1000 # Namespace namespace inbox { inbox = yes separator = / mailbox Drafts { auto = subscribe special_use = \Drafts } mailbox Sent { auto = subscribe special_use = \Sent } mailbox Trash { auto = subscribe special_use = \Trash } mailbox Junk { auto = subscribe special_use = \Junk } mailbox Archive { auto = subscribe special_use = \Archive } } # SSL/TLS ssl = required ssl_cert = </usr/local/etc/ssl/mail/fullchain.pem ssl_key = </usr/local/etc/ssl/mail/key.pem ssl_min_protocol = TLSv1.2 # Authentication auth_mechanisms = plain login disable_plaintext_auth = yes passdb { driver = passwd-file args = scheme=BLF-CRYPT username_format=%u /usr/local/etc/dovecot/users } userdb { driver = static args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n } # LMTP for Postfix delivery service lmtp { unix_listener /var/spool/postfix/private/dovecot-lmtp { mode = 0600 user = postfix group = postfix } } # SASL for Postfix authentication service auth { unix_listener /var/spool/postfix/private/auth { mode = 0660 user = postfix group = postfix } } # Logging log_path = /var/log/dovecot.log info_log_path = /var/log/dovecot-info.log # Sieve filtering (optional) protocol lmtp { mail_plugins = $mail_plugins sieve } plugin { sieve = /var/mail/vhosts/%d/%n/.dovecot.sieve sieve_dir = /var/mail/vhosts/%d/%n/sieve } EOF
Creating User Accounts
Dovecot uses a passwd-file for virtual users:
sh# Generate password hash doveadm pw -s BLF-CRYPT # Add user to password file cat > /usr/local/etc/dovecot/users << 'EOF' user@example.com:{BLF-CRYPT}$2b$05$abcdef...hash...here admin@example.com:{BLF-CRYPT}$2b$05$abcdef...hash...here EOF chmod 600 /usr/local/etc/dovecot/users chown dovecot:dovecot /usr/local/etc/dovecot/users
To add a new user:
shecho "newuser@example.com:$(doveadm pw -s BLF-CRYPT)" >> /usr/local/etc/dovecot/users
rspamd Configuration
rspamd handles spam filtering, DKIM signing, SPF checking, and DMARC enforcement.
Basic Configuration
sh# rspamd default configuration is in /usr/local/etc/rspamd/ # Override with local files in /usr/local/etc/rspamd/local.d/ mkdir -p /usr/local/etc/rspamd/local.d
Enable the Web UI
shcat > /usr/local/etc/rspamd/local.d/worker-controller.inc << 'EOF' password = "$2$xyz..."; # Generate with: rspamadm pw bind_socket = "127.0.0.1:11334"; EOF
Generate the password:
shrspamadm pw
DKIM Signing
Generate DKIM keys:
shmkdir -p /var/db/rspamd/dkim rspamadm dkim_keygen -d example.com -s 20260409 \ -k /var/db/rspamd/dkim/example.com.20260409.key > /var/db/rspamd/dkim/example.com.20260409.txt chown -R rspamd:rspamd /var/db/rspamd/dkim chmod 600 /var/db/rspamd/dkim/*.key
The output file contains the DNS TXT record. Add it to your DNS:
shell20260409._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3..."
Configure rspamd to sign outgoing mail:
shcat > /usr/local/etc/rspamd/local.d/dkim_signing.conf << 'EOF' path = "/var/db/rspamd/dkim/$domain.$selector.key"; selector = "20260409"; sign_authenticated = true; sign_local = true; domain { example.com { selector = "20260409"; path = "/var/db/rspamd/dkim/example.com.20260409.key"; } } EOF
Milter Configuration
shcat > /usr/local/etc/rspamd/local.d/worker-proxy.inc << 'EOF' bind_socket = "127.0.0.1:11332"; milter = yes; timeout = 120s; upstream "local" { default = yes; self_scan = yes; } EOF
Redis Backend
rspamd uses Redis for statistics and rate limiting:
shcat > /usr/local/etc/rspamd/local.d/redis.conf << 'EOF' servers = "127.0.0.1"; EOF service redis start
Start rspamd
shrspamadm configtest service rspamd start
Test DKIM:
sh# Send a test email and check headers # The rspamd web UI at http://127.0.0.1:11334 shows scanning results
Starting the Mail Stack
shservice redis start service rspamd start service dovecot start service postfix start
Testing
sh# Test SMTP connection openssl s_client -connect mail.example.com:587 -starttls smtp # Test IMAP connection openssl s_client -connect mail.example.com:993 # Send a test email echo "Test body" | mail -s "Test subject" user@example.com # Check mail queue mailq # Check Postfix logs tail -f /var/log/maillog
Verify DKIM/SPF/DMARC
Send an email to a Gmail or other major provider. Check the received headers for:
shellAuthentication-Results: mx.google.com; dkim=pass header.d=example.com; spf=pass; dmarc=pass
Or use online tools like mail-tester.com to verify your setup.
Webmail with Roundcube
Configure Roundcube
Create a database for Roundcube:
shservice postgresql start su - postgres -c "createuser -P roundcube" su - postgres -c "createdb -O roundcube roundcube"
Initialize the database:
shpsql -U roundcube roundcube < /usr/local/www/roundcube/SQL/postgres.initial.sql
Configure Roundcube:
shcat > /usr/local/www/roundcube/config/config.inc.php << 'RCEOF' <?php $config['db_dsnw'] = 'pgsql://roundcube:password@localhost/roundcube'; $config['imap_host'] = 'ssl://mail.example.com:993'; $config['smtp_host'] = 'tls://mail.example.com:587'; $config['smtp_user'] = '%u'; $config['smtp_pass'] = '%p'; $config['support_url'] = ''; $config['product_name'] = 'Webmail'; $config['des_key'] = 'CHANGE_THIS_TO_24_CHARS!'; $config['plugins'] = ['archive', 'zipdownload', 'managesieve']; $config['language'] = 'en_US'; $config['skin'] = 'elastic'; RCEOF
NGINX for Webmail
shcat > /usr/local/etc/nginx/nginx.conf << 'EOF' worker_processes auto; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; server { listen 443 ssl; server_name mail.example.com; ssl_certificate /usr/local/etc/ssl/mail/fullchain.pem; ssl_certificate_key /usr/local/etc/ssl/mail/key.pem; ssl_protocols TLSv1.2 TLSv1.3; root /usr/local/www/roundcube; index index.php; location / { try_files $uri $uri/ /index.php?$args; } location ~ \.php$ { fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~ /\. { deny all; } } server { listen 80; server_name mail.example.com; return 301 https://$host$request_uri; } } EOF service nginx start
Production Hardening
Postfix Hardening
Add to /usr/local/etc/postfix/main.cf:
shell# Rate limiting smtpd_client_connection_rate_limit = 30 smtpd_client_message_rate_limit = 60 smtpd_client_recipient_rate_limit = 120 anvil_rate_time_unit = 60s # Header checks (remove internal IPs from outgoing headers) header_checks = regexp:/usr/local/etc/postfix/header_checks
shcat > /usr/local/etc/postfix/header_checks << 'EOF' /^Received:.*\[10\./ IGNORE /^Received:.*\[192\.168\./ IGNORE /^Received:.*\[172\.(1[6-9]|2[0-9]|3[01])\./ IGNORE EOF
Firewall Rules
shcat > /etc/pf.conf << 'EOF' ext_if = "em0" # Mail ports mail_ports = "{ 25, 587, 993, 995 }" web_ports = "{ 80, 443 }" set block-policy drop set skip on lo0 block in all pass out all keep state # SSH pass in on $ext_if proto tcp to port 22 # Mail pass in on $ext_if proto tcp to port $mail_ports pass in on $ext_if proto tcp to port $web_ports EOF sysrc pf_enable="YES" service pf start
Monitoring
Check mail queue regularly:
sh# Add to crontab echo '*/5 * * * * root mailq | tail -1 | grep -q "Mail queue is empty" || echo "Mail queue has messages" | mail -s "Queue Alert" admin@example.com' >> /etc/crontab
Monitor logs for authentication failures:
shgrep "authentication failed" /var/log/maillog | tail -20
FAQ
Is running your own mail server worth the effort?
It depends on your priorities. If you value privacy, control, and independence from Big Tech mail providers, yes. If you want zero maintenance and guaranteed deliverability, use a hosted service. Self-hosting email is more complex than any other server role.
Why does my mail go to spam?
The most common causes: missing or incorrect PTR record, no DKIM signature, no SPF record, sending from a shared IP with bad reputation, or a new IP with no sending history. Verify all authentication records, then send small volumes and gradually increase to build reputation.
Can I host mail for multiple domains?
Yes. Add domains to virtual_mailbox_domains in Postfix, add mailbox mappings in vmailbox, generate DKIM keys for each domain, and add DNS records for each domain. The configuration scales to hundreds of domains.
How do I back up the mail server?
Back up these directories: /var/mail/vhosts/ (all mail), /usr/local/etc/postfix/ (Postfix config), /usr/local/etc/dovecot/ (Dovecot config and user database), /var/db/rspamd/dkim/ (DKIM keys), /usr/local/etc/rspamd/ (rspamd config). For ZFS, take snapshots: zfs snapshot zroot/var/mail@daily.
How do I add a new email account?
sh# Generate password hash doveadm pw -s BLF-CRYPT # Add to Dovecot users file echo "newuser@example.com:{BLF-CRYPT}$2b$05$hash" >> /usr/local/etc/dovecot/users # Add to Postfix virtual mailbox map echo "newuser@example.com example.com/newuser/" >> /usr/local/etc/postfix/vmailbox postmap /usr/local/etc/postfix/vmailbox # Create maildir mkdir -p /var/mail/vhosts/example.com/newuser chown -R vmail:vmail /var/mail/vhosts/example.com/newuser
Do I need IPv6 for a mail server?
Not strictly, but it helps deliverability. Some mail providers prefer servers with proper IPv6 connectivity. If you enable IPv6, make sure your AAAA record, PTR record, and SPF record all include the IPv6 address.
How do I migrate mail from another server?
Use imapsync to migrate mail between IMAP servers:
shpkg install p5-Mail-IMAPClient p5-IO-Socket-SSL imapsync --host1 old-server.example.com --user1 user@example.com --password1 'oldpass' \ --host2 mail.example.com --user2 user@example.com --password2 'newpass' --ssl1 --ssl2