User Tools

Site Tools


computing:mailserver

This is an old revision of the document!



  • mailserver
  • Jonathan Haack
  • Haack's Networking
  • webmaster@haacksnetworking.org

mailserver


This tutorial is for users of Debian GNU/Linux who want to set up a proper email server.. This tutorial assumes you know how to set up A, AAAA, SPF, DKIM, DMARC, MX, and PTR records. Set an A record for example.org and mail.example.org. If you don't know how, then learn up, and do not proceed. Thanks to LinuxBabe for a great jumping off point. Let's begin by editing our hosts file sudo nano /etc/hosts as follows:

127.0.1.1 example.org example
127.0.0.1 mail.example.org localhost

Install postfix and mailutils sudo apt-get install mailutils postfix -y picking Internet Site and set your domain to example.org.

Install firewall, open common ports for front facing website, and for imap/smtp:

sudo apt install ufw
sudo ufw allow 22/tcp
sudo ufw allow 53/tcp
sudo ufw allow 25/tcp
sudo ufw allow 587/tcp
sudo ufw allow 143/tcp
sudo ufw allow 993/tcp
sudo ufw allow 80
sudo ufw allow 443

Increase quota / message size:

sudo postconf -e message_size_limit=52428800

Set hostname and aliases in sudo nano /etc/postfix/main.cf and make sure that the hostname, origin, destination, mailbox size, and quota are set. Also, in my case, I only have ipv4 support, so I explicitly set that as well.

myhostname = mail.example.com
myorigin = /etc/mailname
mydestination = example.com, $myhostname, localhost.$mydomain, localhost
mailbox_size_limit = 0
inet_protocols = ipv4
message_size_limit = 52428800

Let's also make sure that system emails are sent to the user we created above instead of root by sudo nano /etc/aliases and then:

postmaster: root
root: user

Now, set up the server block for your mail server's website:

sudo nano /etc/nginx/conf.d/mail.example.com.conf
sudo mkdir -p /usr/share/nginx/html/

The contents looking something like:

server {
    listen 80;
    #listen [::]:80;
    server_name mail.example.com;
    root /usr/share/nginx/html/;
    location ~ /.well-known/acme-challenge {
      allow all;
   }
}

Once that is done, restart the service sudo systemctl reload nginx and then let's generate a cert:

sudo apt install certbot
sudo apt install python3-certbot-nginx
sudo certbot certonly -a nginx --agree-tos --no-eff-email --staple-ocsp --email email@email.com -d mail.example.com

Now, let's configure postfix to work together with Dovecot/submission on 587 and 465 and to use TLS by editing sudo nano /etc/postfix/master.cf as follows:

submission     inet     n    -    y    -    -    smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_tls_wrappermode=no
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth
smtps     inet  n       -       y       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth

It's now time to configure postfix sudo nano /etc/postfix/main.cf to use TLS:

#Enable TLS Encryption when Postfix receives incoming emails
smtpd_tls_cert_file=/etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_key_file=/etc/letsencrypt/live/mail.example.com/privkey.pem
smtpd_tls_security_level=may 
smtpd_tls_loglevel = 1
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
#Enable TLS Encryption when Postfix sends outgoing emails
smtp_tls_security_level = may
smtp_tls_loglevel = 1
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
#Enforce TLSv1.3 or TLSv1.2
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1

Now, let's configure and enable SASL support. Open /etc/postfix/main.cf and enter:

# SASL Authentication with Dovecot
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_security_options = noanonymous

Now, we can install dovecot and configure it to use IMAP and lmtp. Install the packages with sudo apt install dovecot-core dovecot-imapd dovecot-lmtpd and then edit sudo nano /etc/dovecot/dovecot.conf:

<protocols = imap lmtp>

After that, open sudo nano /etc/dovecot/conf.d/10-mail.conf and change the default mail director location as follows:

<mail_location = maildir:~/Maildir>

Let's make sure dovecot is part of the mail group with sudo adduser dovecot mail and now we can configure dovecot with sudo nano /etc/dovecot/conf.d/10-master.conf in order to be able to leverage lmtp:

service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
   mode = 0600
   user = postfix
   group = postfix
  }
 }

Similarly, we need to edit postfix for lmtp as well with sudo nano /etc/postfix/main.cf and then specifying:

mailbox_transport = lmtp:unix:private/dovecot-lmtp
smtputf8_enable = no

Next, let's configure dovecot authorization with sudo nano /etc/dovecot/conf.d/10-auth.conf plain login as follows:

disable_plaintext_auth = yes
auth_username_format = %n
auth_mechanisms = plain login

Now, configure SSL/TLS encryption in dovecot using your website/domain certs from earlier with sudo nano /etc/dovecot/conf.d/10-ssl.conf:

ssl = required
ssl_cert = </etc/letsencrypt/live/mail.example.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.example.com/privkey.pem
ssl_prefer_server_ciphers = yes
ssl_min_protocol = TLSv1.2

Set up the SASL listener by editing sudo nano /etc/dovecot/conf.d/10-master.conf and adding this block:

service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
}

If you have errors or can't connect your email client at this point, you can test your handshakes as follows:

openssl s_client -connect mail.example.com:465
openssl s_client -starttls smtp -connect mail.example.com:25

Now it is time to setup an spf policy agent so that the incoming email that is received checks for validity of spf records. Do not confuse this with creating an spf TXT record for your outgoing email. Let's install spf policy with sudo apt install postfix-policyd-spf-pythonand then edit sudo nano /etc/postfix/master.cf as follows:

policyd-spf  unix  -       n       n       -       0       spawn
user=policyd-spf argv=/usr/bin/policyd-spf

After that, let's set up sudo nano /etc/postfix/main.cf as follows:

policyd-spf_time_limit = 3600
smtpd_recipient_restrictions =
 permit_mynetworks,
 permit_sasl_authenticated,
 reject_unauth_destination,
 check_policy_service unix:private/policyd-spf

Now, it is time to set up DKIM on your server. After creating the DKIM record/key on your server, you will need to create a corresponding TXT record for it to establish that anything over smtp with that signature is, in fact, you/your server. Let's install opendkim with sudo apt install opendkim opendkim-tools and add postfix to its group with sudo adduser postfix opendkim and then adjust the configuration in sudo nano /etc/opendkim.conf as follows:

Canonicalization   relaxed/simple
Mode               sv
SubDomains         no
Nameservers     8.8.8.8,1.1.1.1
KeyTable           refile:/etc/opendkim/key.table
SigningTable       refile:/etc/opendkim/signing.table
ExternalIgnoreList  /etc/opendkim/trusted.hosts
InternalHosts       /etc/opendkim/trusted.hosts

Now that the configuration for DKIM is ready, let's create the keys and content for the locations specified above:

sudo mkdir -p /etc/opendkim/keys
sudo chown -R opendkim:opendkim /etc/opendkim
sudo chmod 711 /etc/opendkim/keys

Once all the directories and key locations are created, let's open the signing table with sudo nano /etc/opendkim/signing.table and enter the following:

  • @example.com default._domainkey.example.com
  • @*.example.com default._domainkey.example.com

Now that the signing table is setup, we need to edit the key table with sudo nano /etc/opendkim/key.table and enter the following:

default._domainkey.example.com     example.com:default:/etc/opendkim/keys/example.com/default.private

The trusted hosts is next, over in sudo nano /etc/opendkim/trusted.hosts which we simply enter:

.domain.com

We now need to cut the DKIM keys (and make sure to add TXT records on your DNS host later) as follows:

sudo mkdir /etc/opendkim/keys/example.com
sudo opendkim-genkey -b 2048 -d example.com -D /etc/opendkim/keys/example.com -s default -v
sudo chown opendkim:opendkim /etc/opendkim/keys/example.com/default.private
sudo chmod 600 /etc/opendkim/keys/example.com/default.private

To get the information you need for the DNS record, you can run sudo cat /etc/opendkim/keys/example.com/default.txt and then copy everything between the parentheses into your TXT record with default._domainkey as the host. After the DKIM TXT record caches, test it as follows:

sudo opendkim-testkey -d example.com -s default -vvv

Note that that output will display “key not secure” unless you configure DNSSEC, which this tutorial has not done. It's now time to configure postfix to leverage this DKIM key. Let's make a directory for the socket and set up the configuration:

sudo mkdir /var/spool/postfix/opendkim
sudo chown opendkim:postfix /var/spool/postfix/opendkim
sudo nano /etc/opendkim.conf

In the dkim config file, enter:

Socket    local:/var/spool/postfix/opendkim/opendkim.sock

After establishing the socket directory and location, let's configure our dkim defaults in sudo nano /etc/default/opendkim and editing:

SOCKET="local:/var/spool/postfix/opendkim/opendkim.sock"

Lastly, we need to configure postfix to be able to use opendkim in sudo nano /etc/postfix/main.cf by editing:

milter_default_action = accept
milter_protocol = 6
smtpd_milters = local:opendkim/opendkim.sock
non_smtpd_milters = $smtpd_milters

In addition to spf and dkim policies, it's also best practice to have a dmarc policy. Let's install opendmarc with sudo apt install opendmarc and make sure to say no to db configure in the ncurses prompt, after which you should open sudo nano /etc/opendmarc.conf and enter something like:

AuthservID OpenDMARC
TrustedAuthservIDs mail.yourdomain.com
RejectFailures true
IgnoreAuthenticatedClients true
SPFSelfValidate true
Socket local:/var/spool/postfix/opendmarc/opendmarc.sock

The opendmarc socket also needs its directory created, similar to dkim. So let's create a directory for it sudo mkdir -p /var/spool/postfix/opendmarc and then set permissions and restart the service:

sudo chown opendmarc:opendmarc /var/spool/postfix/opendmarc -R
sudo chmod 750 /var/spool/postfix/opendmarc/ -R
sudo adduser postfix opendmarc
sudo systemctl restart opendmarc

Now, we need to configure postfix to work with openDMARC. Expand the milter to include openDMARC's socket by opening sudo nano /etc/postfix/main.cf and adjusting the following block:

milter_default_action = accept
milter_protocol = 6
smtpd_milters = local:opendkim/opendkim.sock,local:opendmarc/opendmarc.sock
non_smtpd_milters = $smtpd_milters

It's about time to setup spam controls, but before we do that, we need to make sure mission critical IMAP folders populate on clients. Let's open sudo nano /etc/dovecot/conf.d/15-mailboxes.conf and enter the auto-create line on folders that you need:

mailbox Drafts {
  **auto = create**
  special_use = \Drafts
}

Since I use lmtp, which is case-sensitive, and sends incoming mail to dovecot, which is also case-sensitive, I created virtual aliases in order to avoid emails being rejected when auto-mailers capitalize or otherwise punctuate the email user name. Let's open sudo nano /etc/postfix/main.cf and add another block:

virtual_alias_maps = regexp:/etc/postfix/virtual_alias

After you enter that block, let's open the file sudo nano /etc/postfix/virtual_alias and edit it with every spelling combination. Here's an example:

/^[Jj][Oo][Nn][Aa][Tt][Hh][Aa][Nn]@haacksnetworking.org/                jonathan
/^[Ww][Ee][Bb][Mm][Aa][Ss][Tt][Ee][Rr]@haacksnetworking.org/            webmaster

It's super important to include the domain above after the regex or all emails you send to that begin with your user name will wind up in that user's inbox. When finished configuring, run postmap /etc/postfix/virtual_alias to honor the changes. So long as everything is working, we can now setup a very reasonable and appropriate way to manage spam, without playing wack-a-mole and without rejecting or discarding emails. Let's first discuss what we can do with postfix itself. It can be configured to reject or discard very poor health emails, such as but not exclusive to setting PTR rules or other fundamentals in sudo nano /etc/postfix/main.cf as follows:

smtpd_sender_restrictions =
    permit_mynetworks,
    permit_sasl_authenticated,
    #reject_unknown_reverse_client_hostname,
    #reject_unknown_client_hostname,
    #reject_unknown_sender_domain,
    #reject_unauthenticated_sender_login_mismatch,
    reject_sender_login_mismatch
    permit
  

Personally, I disable most of the above rejects by design. I include them here more for others' reference. Just remember, there's nothing worse than never getting an email. You can also have postfix do expression checking with sudo apt install postfix-pcre and then edit sudo nano /etc/postfix/main.cf with expression checking, for example:

header_checks = pcre:/etc/postfix/header_checks
body_checks = pcre:/etc/postfix/body_checks

In these locations, sudo nano /etc/postfix/header_checks and sudo nano /etc/postfix/body_checks, setup expression checks as follows:

/free mortgage quote/    REJECT  
/free mortgage quote/    DISCARD

Once they are to your liking, make sure to compile them with sudo postmap /etc/postfix/body_checks and sudo postmap /etc/postfix/header_checks. Again, in my personal setup, I keep postfix out of the spam-management game. Instead, I prefer to accept ALL email and let spam assassin do the lifting. Let's install spamassasin and integrate it with dovecot's managesieve plugin. Let's do sudo apt install dovecot-sieve dovecot-managesieved spamassassin spamc spamass-milter and then edit sudo nano /etc/dovecot/dovecot.conf as follows:

protocols = imap lmtp sieve

In sudo nano /etc/dovecot/conf.d/15-lda.conf please adjust the local delivery agent as follows:

protocol lda {
  mail_plugins = $mail_plugins sieve
}

Let's also adjust the lmtp agent in sudo nano /etc/dovecot/conf.d/20-lmtp.conf as follows:

protocol lmtp {
  mail_plugins = quota sieve
}

Now it's time to setup spamassassin in postfix with the other milters. Let's open sudo nano /etc/postfix/main.cf and adjust the milter block once again:

milter_default_action = accept
milter_protocol = 6
smtpd_milters = local:opendkim/opendkim.sock,local:opendmarc/opendmarc.sock,local:spamass/spamass.sock
non_smtpd_milters = $smtpd_milters

It's now time to edit spam assassin's default configuration file in sudo nano /etc/default/spamass-milter. As with elsewhere, I make sure the REJECT line is commented out, in order to ensure that false positives won't happen. If you understand the risks and want to reject emails at the gate, go ahead and edit sudo nano /etc/default/spamass-milter and uncomment these lines:

#optional custom message
#OPTIONS="-u spamass-milter -i 127.0.0.1 -R SPAM_ARE_NOT_ALLOWED_HERE"
OPTIONS="-u spamass-milter -i 127.0.0.1"
#Reject emails with spamassassin scores > 15 [or desired score]
OPTIONS="${OPTIONS} -r 15"

Again, I do not use the reject or discard options but rather leverage spam assassin's scoring and header assessing together with dovecot, which can move emails to locations fitting their scores. There are two fundamental ways to configure the scoring and sieve rules, either with the CLI or with Roundcube. When I first began, I used the CLI and created a simple rule in sudo nano /etc/dovecot/conf.d/90-sieve.conf and entered this block:

sieve_before = /var/mail/SpamToJunk.sieve

The sieve_before rule ensures spam assassin assesses the email right after they arrive and before sending them to dovecot and its configured delivery agents. Let's open sudo nano /var/mail/SpamToJunk.sieve and enter the following:

require "fileinto";
if header :contains "X-Spam-Flag" "YES"
{
  fileinto "Junk";
  stop;
}

After creating the sieve rule, compile it.

sudo sievec /var/mail/SpamToJunk.sieve

RESUMEEEEEEEEE!!!!!

We can now configure some custom scores and white/black lists as needed so sudo nano /etc/spamassassin/local.cf. I've included the default and explicit header lines as well, which can help with debugging. Specifically, SA will not give a score breakdown on ham unless those are specified.

report_contact webmaster@domain.com
required_score 5.0
#rewrite_header Subject **Possible Spam**
report_safe 0
always_add_headers = 1
#add_header all Flag _YESNO_
#add_header all Status "_YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_"
#add_header all Report _REPORT_
#add_header all Level _STARS_
#add_header all Checker-Version "SpamAssassin _VERSION_ (_DATE_) on _HOSTNAME_"

Before we enter the custom scores and white and black lists, let's install and enable unbound, so that spam-assassin can query the RBLs. RBLs do not allow upstream DNS queries, thus we are required to run our own recursive resolver here. Take care not to expose this publicly. I prefer unbound, but one can also use bind9 or others. Specify the DNS in the local.cf and create your rules/lists:

sudo apt install unbound
sudo nano /etc/spamassassin/local.cf
<dns_server 127.0.0.1>
<score MISSING_FROM                      5.0>
<score MISSING_DATE                      5.0>
<score MISSING_HEADERS                   3.0>
<score PDS_FROM_2_EMAILS                 3.0>
<score FREEMAIL_FORGED_REPLYTO           3.5>
<score DKIM_ADSP_NXDOMAIN                5.0>
<score FORGED_GMAIL_RCVD                 2.5>
<score FREEMAIL_FORGED_FROMDOMAIN        3.0>
<score HEADER_FROM_DIFFERENT_DOMAINS     3.0>
<score FREEMAIL_FROM                     3.0>
<score ACCT_PHISHING                     3.0>
<score AD_PREFS                          3.0>
<score ADMAIL                            3.0>
<score ADMITS_SPAM                       3.0>
<score CONFIRMED_FORGED                  3.0>
<score FROM_PAYPAL_SPOOF                 3.0>
<score SPF_SOFTFAIL                      2.0>
<score SPF_FAIL                          5.0>
<whitelist_from *@statefarm.com>
<blacklist_from *@email.freethinkerdaily.com>

Additionally, if you check the full headers, you will see that the RBLs can now be queried without issue. Note that whitelisting adds a -100 score and that blacklisting adds a +100 score. To understand how to tweak the symbolic headers better, one should review their spam and headers periodically and update rules. It is also possible to manage behaviors for whitelist, blacklist, and spam scores after dovecot receives them and from Roundcube. This would be instead of managing them with spam assassin at the CLI before dovecot receives them. To do that, comment out white and black list rules, and remove the sieve_before rule we created above. Navigate to roundcube > settings > filters > edit filter set. To replicate the same functionality as above, I created the following:

#open roundcube, click the Edit filter set
#copy paste this into the block, replace everything
require ["fileinto"];
# rule:[whitelist]
if anyof (
    header :contains "from" [
        "noreply@dmarc.yahoo.com",
        "noreply@dmarc.google.com",
        "Friend@protonmail.com"
    ]
) {
    keep;
    stop;
}
# rule:[blacklist]
if anyof (
    header :contains "from" [
        "awakening-minds.com",
        "porn@yahoo.com",
        "bounce-1.public.govdelivery.com"
    ]
) {
    fileinto "Junk";
    stop;
}
# rule:[spamcheck]
if anyof (
    header :contains "x-spam-status" "Yes",
    header :contains "x-spam-flag" "YES",
    header :contains "x-spam-level" "*****"
) {
    fileinto "Junk";
    stop;
}

Additionally, the best way to monitor your server is with pflogsum.

sudo apt install pflogsumm
sudo apt install rsyslog

Disable the /var/log/mail.log entries that are in rsyslog and any others that are located in /etc/logrotate.d/. Once that's done, create a new rule as follows:

sudo nano postfix-log

In that file, enter the following:

/var/log/mail.log {
    missingok
    daily
    rotate 7
    create
    compress
    start 0
}

Once that's done, let's create script and cronjob to send us daily reports:

sudo nano /usr/local/bin/pflog-run.sh

In that script, enter something like this:

#!/bin/sh
#/usr/sbin/logrotate -f /etc/logrotate.d/postfix-log
gunzip /var/log/mail.log.0.gz
/usr/sbin/pflogsumm /var/log/mail.log.0 --problems-first --rej-add-from --verbose-msg-detail -q |  mail -s "[pflog-lastlog]-$(hostname -f)-$(date)" email@email.com
gzip /var/log/mail.log.0
sleep 2s
systemctl restart rsyslog
systemctl restart postfix
systemctl restart dovecot
exit 0

The key here is that your script and zip and unzipping rules match the retention and naming conventions specified in logrotate. Since I floored the rotation at 0, the script always unzips the 0.gz log. This is why it is preferable and easier to remove the mail.log stanza from the other rotations. This allows one to easily customize it for email logs without messing with other rotations and settings. Once that's done, set up a cronjob and you are all set.

30 12 * * * /bin/bash /usr/local/bin/pflog-run.sh >> /home/logs/pflog-run.log

In extreme cases, directly block problem domains in postfix in /etc/postfix/main.cf by adding reject_sender example.com within the smtpd_sender_restrictions = block. Make sure to add all users to the mail group.

sudo adduser username mail

The SASL module packages should be brought in as dependencies of postfix and/or dovecot. However, on upgrades, etc., they might be removed during dependency resolution. If you get “no sasl” report on your logs suddenly, despite everything working prior, use:

sudo apt-get install libsasl2-modules

If/when things are going wrong, turn on your detailed debugging logs and study them:

nano /etc/dovecot/conf.d/10-logging.conf
<mail_debug yes>

To check record health after you set your DNS, you can do the following:

dig txt +short _dmarc.jonathanhaack.com
dig txt +short _dmarc.haacksnetworking.org
dig default._domainkey.jonathanhaack.com txt
dig default._domainkey.haacksnetworking.org txt
dig txt +short jonathanhaack.com
dig txt +short haacksnetworking.org
dig -x 8.28.86.130 +short
dig -x 8.28.86.125 +short
sudo opendkim-testkey -d jonathanhaack.com -s default -vvv
sudo opendkim-testkey -d haacksnetworking.org -s default -vvv

You should test email health with the CLI and/or use a service like mail-tester.com. I recommend using both CLI to send email and a common client. Both domain.com and mail.domain.com should work if you set everything right. Here's how to send a simple email at the CLI:

echo "Hi, I am testing the subdomain email health." | mail -s "CLI Email Test" oemb1905@jonathanhaack.com

Postfix has its own CLI control tools, such as but not exclusive to viewing email, deleting email, etc.:

mailq
postcat -q E900C4780073
postsuper -d E900C4780073
postsuper -d ALL

oemb1905 2025/04/12 22:10

computing/mailserver.1744498291.txt.gz · Last modified: 2025/04/12 22:51 by oemb1905