Table of Contents

Mail Server Setup

This is how I set up my mail server on

Supporting Packages to Install

Server configuration

vmail user configuration

groupadd -g 5000 vmail
useradd -d /home/vmail -g 5000 -m -s /bin/bash -u 5000 -p somepassword vmail
id vmail

uid=5000(vmail) gid=5000(vmail) groups=5000(vmail)

Postfix configuration

/etc/postfix/main.cf

main.cf
# See /usr/share/postfix/main.cf.dist for a commented, more complete version
 
 
# Debian specific:  Specifying a file name will cause the first
# line of that file to be used as the name.  The Debian default
# is /etc/mailname.
myorigin = /etc/mailname
 
smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
biff = no
 
# appending .domain is the MUAs job.
append_dot_mydomain = no
 
# Uncomment the next line to generate "delayed mail" warnings
delay_warning_time = 4h
 
readme_directory = no
 
# TLS parameters
smtpd_tls_cert_file = /etc/ssl/certs/ssl-mail.pem
smtpd_tls_key_file = /etc/ssl/private/ssl-mail.key
smtpd_tls_loglevel = 1
smtpd_tls_security_level = may
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
tls_random_source = dev:/dev/urandom
tls_random_bytes = 32
tls_random_reseed_period = 3600s
#
home_mailbox = Maildir/
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/dovecot-auth
smtpd_sasl_authenticated_header = yes
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = $myhostname
broken_sasl_auth_clients = yes
smtpd_use_tls = yes
smtpd_tls_received_header = yes
smtpd_tls_mandatory_protocols = SSLv3, TLSv1
smtpd_tls_mandatory_ciphers = medium
#smtpd_tls_auth_only = yes
#
smtp_tls_cert_file=/etc/postfix/ssl/smtpd.pem
smtp_tls_key_file=$smtp_tls_cert_file
smtp_tls_loglevel = 1
smtp_tls_security_level = may
smtp_tls_note_starttls_offer = yes
smtp_use_tls = yes
#
 
# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
# information on enabling SSL in the smtp client.
 
myhostname = example.org
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
#mydestination = example.org, www.example.org, example.cc, www.example.cc, myserverhostname, localhost.localdomain, localhost
mydestination =
relayhost = smtp-server.example.com
# This was commented out as it gives a "unused parameter" warning on Precise - works on Hardy
#relay_domain = $mydestination
mynetworks_style = subnet
#mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_command = /usr/lib/dovecot/deliver -c /etc/dovecot/conf.d/01-mail-stack-delivery.conf -m "${EXTENSION}"
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
owner_request_special = no
message_size_limit = 32768000
#
# Virtual Mailbox Domain Settings
virtual_alias_maps = mysql:/etc/postfix/mysql/alias_maps.cf, mysql:/etc/postfix/mysql/alias_alias_maps.cf
virtual_mailbox_domains = mysql:/etc/postfix/mysql/domains_maps.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql/mailbox_maps.cf, mysql:/etc/postfix/mysql/mailbox_alias_maps.cf
virtual_mailbox_limit = 51200000
virtual_minimum_uid = 5000
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
virtual_mailbox_base = /home/vmail
virtual_transport = virtual
# Additional for quota support
# This was commented out as it gives a "unused parameter" warning on Precise - works on Hardy
#virtual_create_maildirsize = yes
#virtual_mailbox_extended = yes
#virtual_mailbox_limit_maps = mysql:/etc/postfix/mysql/mailbox_limit_maps.cf
#virtual_mailbox_limit_override = yes
#virtual_maildir_limit_message = Sorry, the your maildir has overdrawn your diskspace quota, please free up some of spaces of your mailbox try again.
#virtual_overquota_bounce = yes
#
# Spam reduction parameters. May be aggresive for some, but seems to work well.
access_map_reject_code = 554
invalid_hostname_reject_code = 554
maps_rbl_reject_code = 554
multi_recipient_bounce_reject_code = 554
non_fqdn_reject_code = 554
plaintext_reject_code = 554
reject_code = 554
relay_domains_reject_code = 554
unknown_local_recipient_reject_code = 550
unknown_address_reject_code = 550
unknown_client_reject_code = 550
unknown_hostname_reject_code = 550
unknown_relay_recipient_reject_code = 550
unknown_virtual_alias_reject_code = 550
unknown_virtual_mailbox_reject_code = 550
unverified_recipient_reject_code = 550
unverified_sender_reject_code = 550
#
default_process_limit = 20
smtpd_client_connection_count_limit = 10
# Value of 60 should translate to 1 per second limit
smtpd_client_connection_rate_limit = 60
smtpd_client_message_rate_limit = 60
smtpd_client_new_tls_session_rate_limit = 60
#
smtpd_helo_required = yes
smtpd_delay_reject = yes
address_verify_map = btree:${data_directory}/verify_cache
smtpd_reject_unlisted_sender=yes
#
smtpd_recipient_restrictions =
    check_client_access hash:/etc/postfix/white_lists,
    permit_sasl_authenticated,
    permit_mynetworks,
#   permit_tls_clientcerts,
    reject_invalid_hostname,
    reject_non_fqdn_hostname,
    reject_non_fqdn_sender,
    reject_non_fqdn_recipient,
    reject_unknown_sender_domain,
    reject_unknown_recipient_domain,
    reject_unverified_sender,
    reject_unauth_destination,
    reject_rbl_client zen.spamhaus.org,
    reject_rbl_client cbl.abuseat.org,
    check_client_access hash:/etc/postfix/white_lists,
    permit
smtpd_data_restrictions =
    permit_sasl_authenticated,
    permit_mynetworks,
    reject_unauth_pipelining,
    permit
# Added for trying to send email from PDA
smtpd_client_restrictions =
    permit_sasl_authenticated
smtpd_helo_restrictions =
    check_helo_access hash:/etc/postfix/check_helo_access,
    permit_mynetworks,
    permit_sasl_authenticated,
    reject_non_fqdn_hostname,
    reject_invalid_hostname,
    permit
#

/etc/postfix/mailname

mailname
example.org

Client TLS Support

Follow this for Postfix Client TLS Support setup

Postfix MySql (maps) configuration

mysql_uid.cf
user=<mysql_db_user>
password=<mysql_db_password>
dbname=<mysql_db_name>
table=mailbox
select_field=username
where_field=username
hosts=127.0.0.1:<mysql_db_port>
mysql_gid.cf
user=<mysql_db_user>
password=<mysql_db_password>
dbname=<mysql_db_name>
table=mailbox
select_field=username
where_field=username
hosts=127.0.0.1:<mysql_db_port>
mailbox_maps.cf
user=<mysql_db_user>
password=<mysql_db_password>
dbname=<mysql_db_name>
table=mailbox
select_field=maildir
where_field=username
hosts=127.0.0.1:<mysql_db_port>
additional_conditions = and active = 1
mailbox_limit_maps.cf
user=<mysql_db_user>
password=<mysql_db_password>
dbname=<mysql_db_name>
table=mailbox
select_field=quota
where_field=username
hosts=127.0.0.1:<mysql_db_port>
additional_conditions = and active = 1
alias_maps.cf
user=<mysql_db_user>
password=<mysql_db_password>
dbname=<mysql_db_name>
table=alias
select_field=goto
where_field=address
hosts=127.0.0.1:<mysql_db_port>
additional_conditions = and active = 1
domains_maps.cf
user=<mysql_db_user>
password=<mysql_db_password>
dbname=<mysql_db_name>
table=domain
select_field=domain
where_field=domain
hosts=127.0.0.1:<mysql_db_port>
additional_conditions = and backupmx = 0 and active = 1 and transport = 'virtual' and domain = '%s'
relay_domains.cf
user=<mysql_db_user>
password=<mysql_db_password>
dbname=<mysql_db_name>
table=domain
select_field=domain
where_field=domain
hosts=127.0.0.1:<mysql_db_port>
additional_conditions = and active = 1 and backupmx = 0 and transport = 'relay' and domain = '%s'

The below two settings (alias_alias_maps.cf & mailbox_alias_maps.cf) are for alias domains:

alias_alias_maps.cf
user=<mysql_db_user>
password=<mysql_db_password>
dbname=<mysql_db_name>
hosts=127.0.0.1:<mysql_db_port>
query = SELECT goto FROM alias,alias_domain
  WHERE alias_domain.alias_domain = '%d'
  AND alias.address=concat('%u', '@', alias_domain.target_domain)
  AND alias.active = 1 AND alias_domain.active = 1
mailbox_alias_maps.cf
user=<mysql_db_user>
password=<mysql_db_password>
dbname=<mysql_db_name>
hosts=127.0.0.1:<mysql_db_port>
query = SELECT maildir FROM mailbox, alias_domain
  WHERE alias_domain.alias_domain = '%d'
  AND mailbox.username=concat('%u', '@', alias_domain.target_domain )
  AND mailbox.active = 1 AND alias_domain.active = 1

Dovecot Configuration

Change the following files at /etc/dovecot & /etc/dovecot/conf.d

The files in /etc/dovecot need NO changes from default

File(s) requiring change

dovecot-sql.conf.ext
driver = mysql
connect = host=127.0.0.1 port=3306 user=root password=rootpassword dbname=postfixadmindbname
default_pass_scheme = CRAM-MD5
user_query = \
 SELECT \
        concat('/home/vmail/', maildir) as home, \
        5000 as uid, 5000 as gid \
        FROM mailbox \
        WHERE username = '%u' AND active = '1'
password_query = \
 SELECT username as user, password, \
        concat('/home/vmail/', maildir) as userdb_home, \
        concat('maildir:/home/vmail/', maildir) as userdb_mail, \
        5000 as userdb_uid, 5000 as userdb_gid \
        FROM mailbox \
        WHERE username = '%u' AND active = '1'

The following files in /etc/dovecot/conf.d do NOT require change as well

File(s) requiring change

10-logging.conf
auth_verbose = yes
auth_debug = yes
plugin {
  # Events to log. Also available: flag_change append
  #mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
  # Available fields: uid, box, msgid, from, subject, size, vsize, flags
  # size and vsize are available only for expunge and copy events.
  #mail_log_fields = uid box msgid size
}
10-auth.conf
auth_mechanisms = plain cram-md5
!include auth-system.conf.ext
auth-system.conf.ext
passdb {
  driver = sql
  args = /etc/dovecot/dovecot-sql.conf.ext
  # [session=yes] [setcred=yes] [failure_show_msg=yes] [max_requests=<n>]
  # [cache_key=<key>] [<service name>]
  #args = dovecot
}
userdb {
  # <doc/wiki/AuthDatabase.Passwd.txt>
  driver = sql
  args = /etc/dovecot/dovecot-sql.conf.ext
  # [blocking=no]
  #args =
}
10-mail.conf
mail_location = maildir:/home/vmail/%d/%u
mail_uid = vmail
mail_gid = vmail
maildir_copy_with_hardlinks = yes

Postfix Admin

All changes from the default go in config.local.php

config.local.php
$CONF['postfix_admin_url'] = 'http://pfa.example.org';
 
$CONF['domain_path'] = 'YES';
 
$CONF['database_type'] = 'mysql';
$CONF['database_host'] = 'localhost';
$CONF['database_user'] = 'upostfixadmin';
$CONF['database_password'] = 'somepassword';
$CONF['database_name'] = 'postfixadmin';
 
$CONF['admin_email'] = 'postmaster@example.com';
 
$CONF['smtp_server'] = 'smtp-server.example.com';
 
$CONF['encrypt'] = 'dovecot:CRAM-MD5';
 
$CONF['dovecotpw'] = "/usr/bin/doveadm pw";
 
$CONF['page_size'] = '100';
 
$CONF['default_aliases'] = array (
    'abuse' => 'abuse@example.com',
    'hostmaster' => 'hostmaster@example.com',
    'postmaster' => 'postmaster@example.com',
    'webmaster' => 'webmaster@example.com'
);
 
$CONF['aliases'] = '10000';
$CONF['mailboxes'] = '10000';
$CONF['maxquota'] = '10000';
 
$CONF['transport'] = 'YES';
 
$CONF['vacation_domain'] = 'autoreply.example.com';
 
$CONF['user_footer_link'] = "http://example.com/";
$CONF['footer_text'] = 'Return to example.com'; 
$CONF['footer_link'] = 'http://example.com';
 
$CONF['create_mailbox_subdirs_prefix']='';
$CONF['new_quota_table'] = 'YES';

Migration from Courier to Postfix

If you are moving to a new machine make sure that you tar and untar files to preserver permissions and most importantly timestamps. Or all your old mail will have the same date/time!

Resource

Migration-Courier - Dovecot Wiki

Command

/home/vmail/courier-dovecot-migrate.pl --to-dovecot --recursive --convert --overwrite /home/vmail/example.org/

Sample output:

Converting to Dovecot format
Finding maildirs under /home/vmail/example.org/

Total: 70 mailboxes / 38 users
       0 errors
46 dovecot-uidlist files written

WARNING: Badly done migration will cause your IMAP and/or POP3 clients to re-download all mails. Read http://wiki.dovecot.org/Migration carefully.

Folder between courier and dovecot

One way to get the courier subfolders to show up in dovecot is to add this to the configuration:

10-mail.conf
namespace private {
  prefix = INBOX.
  separator = .
  inbox = yes
}

Alternatively, I created the subfolders (from squirrel mail) and then delete the newly created folders from the Linux file folders and renamed the existing file folders to the name I just deleted. So here goes for example:

mv .INBOX.SomeSubFolder/ junk
mv .SomeSubFolder .INBOX.SomeSubFolder

Here I moved the newly created folder to a “junk” location (an alternative to just deleting it). Then renamed the original folder to the new folder name.

Squirrel Mail configuration

Follow this link.

Some dovecot commands

dovecot --build-options
find /usr/lib/dovecot/modules/
doveconf -a

pflogsumm.pl - Produce Postfix MTA logfile summary

The pflogsumm.pl produces a daily summary of mail activity.

Install pflogsumm at /usr/local/bin/pflogsumm. Install the below script as a Daily cron. Make sure the /etc/cron.daily has the shell script above the daily log-rotate script and the Daily cron runs at midnight. To change the timing of the daily cron run, update the run time at /etc/crontab.

Daily cron job shell wrapper

#!/bin/bash
#
# Daily Postfix Log report
#
TS=$(date +%Y%m%d_%H%M%S);
LOGFILEDIR="/var/log/postfixrep"
LOGFILE="$LOGFILEDIR/pfrep_$TS.txt"
PFLOGSUMM="/usr/local/bin/pflogsumm"
PFMAILINF="/var/log/mail.info"
PFMAILINF="/var/log/mail.log"
REMAIL="report@example.org"
REPSUB="Postfix Report"
#
#$PFLOGSUMM $PFMAILINF > $LOGFILE
$PFLOGSUMM --detail 10 --problems_first --verbose_msg_detail $PFMAILINF > $LOGFILE
cat $LOGFILE | mailx -s "$REPSUB" $REMAIL
#
# Delete log files older than 40 days
/usr/bin/find $LOGFILEDIR/pfrep* -mtime +40 -exec rm {} \;
#
exit 0

Fail2ban configuration

Fail2ban is optional but highly recommended to reduce thrashing of the servers from brute-force attempts

Notice the journalmatch = _SYSTEMD_UNIT=postfix@-.service line. The default journalmatch = _SYSTEMD_UNIT=postfix.service does not work. Not sure if this is an issue with Debian/Bookworm or fail2ban configuration.

SMTP Configuration

postfix.conf
# Fail2Ban filter for selected Postfix SMTP rejections
#
#
 
[INCLUDES]
 
# Read common prefixes. If any customizations available -- read them from
# common.local
before = common.conf
 
[Definition]
 
_daemon = postfix/smtpd
 
failregex = ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 554 5\.7\.1 .*$
            ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 554 5\.5\.2 .*$
            ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 450 4\.7\.1 : Helo command rejected: Host not found; from=<> to=<> proto=ESMTP helo= *$
            ^%(__prefix_line)s\S+\: reject: RCPT from \S+\[<HOST>\]: 550 5\.1\.1 <\S*>: Recipient address rejected:.*$
            ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 454 4\.7\.1 <\S*>: Relay access denied;.*$
            ^%(__prefix_line)swarning: \S+\[<HOST>\]: SASL LOGIN authentication failed: Invalid authentication mechanism$
            ^%(__prefix_line)swarning: Recipient address rate limit exceeded: \S+\ from unknown\[<HOST>\] for service smtp$
 
ignoreregex =
 
[Init]
 
journalmatch = _SYSTEMD_UNIT=postfix@-.service
postfix-sasl.conf
# Fail2Ban filter for postfix authentication failures
#
 
[INCLUDES]
 
before = common.conf
 
[Definition]
 
_daemon = postfix/smtpd
 
failregex = ^%(__prefix_line)swarning: [-._\w]+\[<HOST>\]: SASL (?:LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5) authentication failed(: [ A-Za-z0-9+/]*={0,2})?\s*$
 
ignoreregex =
 
[Init]
 
journalmatch = _SYSTEMD_UNIT=postfix@-.service

dovecot.conf did not require any changes from the default install

Dovecot/IMAP Configuration

Below are the list of failures that need to be checked

Jul  9 07:19:39 inthostname dovecot: imap-login: Disconnected: Too many invalid commands (no auth attempts in 0 secs): user=<>, rip=<hackerip>, lip=internalip, session=<MGpJ4QwAJgCn+IV+>
Jul  9 00:23:02 inthostname dovecot: imap-login: Aborted login (no auth attempts in 4 secs): user=<>, rip=<hackerip>, lip=internalip, TLS, session=<g2xYDwcAcwBX7LCo>
Jul  9 07:57:35 inthostname dovecot: imap-login: Disconnected (no auth attempts in 0 secs): user=<>, rip=<hackerip>, lip=internalip, TLS: Disconnected, session=<wqHyaA0AHgCrQ0bl>
Jul  9 11:15:15 inthostname dovecot: imap-login: Disconnected (auth failed, 1 attempts in 2 secs): user=<someone@example.org>, method=PLAIN, rip=<hackerip>, lip=internalip, TLS, session=<mN+5KxAACACr92MJ>
Jul  9 14:37:38 inthostname dovecot: imap-login: Disconnected (tried to use disallowed plaintext auth): user=<>, rip=<hackerip>, lip=internalip, session=<HXSc/xIAuAAl7S4R>
Oct  6 23:17:35 inthostname dovecot: imap-login: Disconnected (disconnected before auth was ready, waited 0 secs): user=<>, rip=<hackerip>, lip=internalip, TLS handshaking: SSL_accept() failed: error:140760FC:SSL routines:SSL23_GET_CLIENT_HELLO:unknown protocol, session=<HTcFoxgHmgBrquET>
Oct  5 04:33:07 inthostname dovecot: imap-login: Disconnected (tried to use unsupported auth mechanism): user=<>, method=NTLM, rip=<hackerip>, lip=internalip, session=<MK/Dz/QGMAClmnge>
Sep 25 04:55:41 inthostname dovecot: imap-login: Disconnected (client didn't finish SASL auth, waited 19 secs): user=<>, method=PLAIN, rip=<wanip>, lip=internalip, TLS: SSL_read() syscall failed: Connection reset by peer, session=<9yvt9CsGOQAYNzGR>


hackerip=x.x.x.x
internalip=y.y.y.y

Testing Fail2ban configuration

fail2ban-regex systemd-journal /etc/fail2ban/filter.d/postfix.conf
fail2ban-regex systemd-journal /etc/fail2ban/filter.d/postfix-sasl.conf

rsyslog filters duplicate messages as last message repeated X times. This causes Fail2ban to not get the actual count of failed attempts. To change the configuration:

Edit /etc/rsyslog.conf to update $RepeatedMsgReduction from on to off

$RepeatedMsgReduction off

Restart rsyslog

service rsyslog restart
Currently banned IPs

Run as root:

fail2ban-client status | grep "Jail list:" | sed "s/ //g" | awk '{split($2,a,",");for(i in a) system("fail2ban-client status " a[i])}'

Permanently ban

Refer here to permanently ban!

Other Fail2ban resources

https://www.the-art-of-web.com/system/fail2ban-howto/

Post Configuration Steps

white_lists file

To avoid error that there is no white list file.

The check_helo_access allows helo specific white lists. For e.g. use when you an error such as Helo command rejected: Host not found;.

cd /etc/postfix
touch white_lists
postmap white_lists
#
touch check_helo_access
postmap check_helo_access

Restart

service postfix restart
service dovecot restart

Dovecot debug

To turn on auth debug in dovecot. Update dovecot/conf.d/10-logging.conf file with

auth_verbose = no
auth_debug = no

Remember to turn if off once done.

/etc/aliases

Optionally update aliases so mail delivered to root (or someuser) at localhost can be forwarded to a remote mail user.

# See man 5 aliases for format
postmaster:    root
someuser:someuser@example.org
root:someuser@example.org

Run the below after changing /etc/aliases

postalias /etc/aliases
# Alternative to postalias you can also run 'newaliases' command
service postfix restart

Logs

Update logrotate configuration at /etc/logrotate.d/rsyslog to increase the retention period. Typically mail logs have a 4 week retention period (parameter rotate). Increase it to 13 weeks or more as desired.

Satellite hosts

The above setup is for Postfix smart host. For the rest of the servers in the network, it is preferred to set them up as a Satellite host and route mail through the local host. The Postfix main.cf configuration for Postfix Satellite host aka Send-only, Relay, Forwarded host is below. Note:

/etc/postfix/main.cf

main.cf
# See /usr/share/postfix/main.cf.dist for a commented, more complete version
 
# Debian specific:  Specifying a file name will cause the first
# line of that file to be used as the name.  The Debian default
# is /etc/mailname.
myorigin = /etc/mailname
 
smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
biff = no
 
# appending .domain is the MUA's job.
append_dot_mydomain = no
 
# Uncomment the next line to generate "delayed mail" warnings
#delay_warning_time = 4h
 
readme_directory = no
 
# TLS parameters
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
smtpd_use_tls=yes
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
 
# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
# information on enabling SSL in the smtp client.
 
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
myhostname = your-host-name
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
mydomain = example.org
mydestination = $myhostname.$mydomain, localdomain, $myhostname, localhost.localdomain, localhost
relayhost = mainsmtp
smtp_host_lookup = native
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = loopback-only
# Address rewriting
#smtp_generic_maps = hash:/etc/postfix/generic

/etc/postfix/mailname

mailname
your-host-name.example.org

/etc/aliases

aliases
# See man 5 aliases for format
postmaster:    root
user:system@example.org
root:system@example.org