May 6, 2021

Postfix mail server howto

Here we’ll walk through how to build a mail server that has a webmail client, bunches of authentication through a database backend (making it easy to add/delete/manage users) and a way to add malware/spam scanners if you want. There are lots of ways to do this, but this is down-and-dirty cut/paste howto. Chunks of it were taken from over here in case you want another reference or to support them (they’re good folks).

Understanding a “mail server” is non-trivial, because there are lots of little parts that allow mail to get to you, not just the “mail server”, which is really called a Mail Transfer Agent (MTA), which is what Postfix is. Still, without Postfix nothing would be trying to deliver anything, either locally or remotely. We also enable mail quotas, in case you need them.

Also, you have to have an idea of how DNS works, otherwise emails addressed to you@yourdomain.com would not know that you really get email on a server with an IP of 1.2.3.4. Oh, and this is normally a public, static IP (we just use the example 1.2.3.4, you should in this howto change it to what yours really is, it’s impossible that it will be 1.2.3.4) so you have to have a public static IP that doesn’t change often (or a way to tell dynamic dns that your home cable modem just changed to a different IP, which is sort of beyond the scope of this howto).

We’re using Debian Wheezy, though there are lots of successful mail servers which use other Linux/BSD variants. If you want to use them, you’ll have to adapt this to your target platform. First thing you do is login and get root. Okay, you could use sudo, and if you want just add sudo on the front of the commands.

su
dpkg-reconfigure dash
Use dash as the default system shell (/bin/sh)? <-- No
<pre lang="bash">
apt-get install postfix postfix-mysql postfix-doc mysql-client mysql-server courier-authdaemon courier-authlib-mysql courier-pop courier-pop-ssl courier-imap courier-imap-ssl libsasl2-2 libsasl2-modules libsasl2-modules-sql sasl2-bin libpam-mysql openssl phpmyadmin apache2 libapache2-mod-php5 php5 php5-mysql libpam-smbpass

Now it will ask you questions:

General type of mail configuration: <-- Internet Site
System mail name: <-- mail.yourdomain.com
New password for the MySQL "root" user: <-- yourrootsqlpassword
Repeat password for the MySQL "root" user: <-- yourrootsqlpassword
Create directories for web-based administration? <-- No
SSL certificate required <-- Ok
Web server to reconfigure automatically: <-- apache2
Configure database for phpmyadmin with dbconfig-common? <-- No

Now we rebuild postfix with a quota patch NOTE: IF YOU USE JESSIE OR POSTFIX > 2.10 THE PATCH ISN’T AVAILABLE, so skip this step:

apt-get build-dep postfix
cd /usr/src
apt-get source postfix
postconf -d | grep mail_version <-- mine said 2.9.6, use your number in the next commands for mail version
wget http://vda.sourceforge.net/VDA/postfix-vda-v11-2.9.6.patch
cd postfix-2.9.6
patch -p1 < ../postfix-vda-v11-2.9.6.patch
vi debian/rules
  export DEB_BUILD_HARDENING=1 <-- change to 0
dpkg-buildpackage
dpkg -i postfix_2.9.6-2_amd64.deb postfix-mysql_2.9.6-2_amd64.deb <-- change version to your real numbers

now create the databases and configure them. We create a table for domains (if you want to use this box for multiple domains) and a table for users, which would be where you’d add me@mydomain.com. Change the “password” fields to what password you really want:

mysqladmin -u root -p create mail
mysql -u root -p
GRANT SELECT, INSERT, UPDATE, DELETE ON mail.* TO 'mail_admin'@'localhost' IDENTIFIED BY 'mail_admin_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON mail.* TO 'mail_admin'@'localhost.localdomain' IDENTIFIED BY 'mail_admin_password';
FLUSH PRIVILEGES;
USE mail;
CREATE TABLE domains (
domain varchar(50) NOT NULL,
PRIMARY KEY (domain) )
ENGINE=MyISAM;
CREATE TABLE forwardings (
source varchar(80) NOT NULL,
destination TEXT NOT NULL,
PRIMARY KEY (source) )
ENGINE=MyISAM;
CREATE TABLE users (
email varchar(80) NOT NULL,
password varchar(20) NOT NULL,
quota INT(10) DEFAULT '10485760',
PRIMARY KEY (email)
) ENGINE=MyISAM;
CREATE TABLE transport (
domain varchar(128) NOT NULL default '',
transport varchar(128) NOT NULL default '',
UNIQUE KEY domain (domain)
) ENGINE=MyISAM;
quit;

Now we have to add the authentication config/mapping files, so create these new files with the contents shown:

vi /etc/postfix/mysql-virtual_domains.cf
  user = mail_admin
  password = mail_admin_password
  dbname = mail
  query = SELECT domain AS virtual FROM domains WHERE domain='%s'
  hosts = 127.0.0.1
vi /etc/postfix/mysql-virtual_forwardings.cf
  user = mail_admin
  password = mail_admin_password
  dbname = mail
  query = SELECT destination FROM forwardings WHERE source='%s'
  hosts = 127.0.0.1
vi /etc/postfix/mysql-virtual_mailboxes.cf
  user = mail_admin
  password = mail_admin_password
  dbname = mail
  query = SELECT CONCAT(SUBSTRING_INDEX(email,'@',-1),'/',SUBSTRING_INDEX(email,'@',1),'/') FROM users WHERE email='%s'
  hosts = 127.0.0.1
vi /etc/postfix/mysql-virtual_email2email.cf
  user = mail_admin
  password = mail_admin_password
  dbname = mail
  query = SELECT email FROM users WHERE email='%s'
  hosts = 127.0.0.1
vi /etc/postfix/mysql-virtual_transports.cf
  user = mail_admin
  password = mail_admin_password
  dbname = mail
  query = SELECT transport FROM transport WHERE domain='%s'
  hosts = 127.0.0.1
vi /etc/postfix/mysql-virtual_mailbox_limit_maps.cf
  user = mail_admin
  password = mail_admin_password
  dbname = mail
  query = SELECT quota FROM users WHERE email='%s'
  hosts = 127.0.0.1
chmod o= /etc/postfix/mysql-virtual_*.cf
chgrp postfix /etc/postfix/mysql-virtual_*.cf
groupadd -g 5000 vmail
useradd -g vmail -u 5000 vmail -d /home/vmail -m

Now you have to modify some of Postfix’s settings:

postconf -e 'myhostname = mail.example.com' <-- change the domain to yours
vi /etc/postfix/main.cf <-- change stuff to look like (add to end of file)
  virtual_alias_domains =
  virtual_alias_maps = proxy:mysql:/etc/postfix/mysql-virtual_forwardings.cf, mysql:/etc/postfix/mysql-virtual_email2email.cf
  virtual_mailbox_domains = proxy:mysql:/etc/postfix/mysql-virtual_domains.cf
  virtual_mailbox_maps = proxy:mysql:/etc/postfix/mysql-virtual_mailboxes.cf
  virtual_mailbox_base = /home/vmail
  virtual_uid_maps = static:5000
  virtual_gid_maps = static:5000
  smtpd_sasl_auth_enable = yes
  broken_sasl_auth_clients = yes
  smtpd_sasl_authenticated_header = yes
  smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
  smtpd_use_tls = yes
  smtpd_tls_cert_file = /etc/postfix/smtpd.cert
  smtpd_tls_key_file = /etc/postfix/smtpd.key
  transport_maps = proxy:mysql:/etc/postfix/mysql-virtual_transports.cf
  virtual_maildir_extended = yes
  virtual_mailbox_limit_maps = proxy:mysql:/etc/postfix/mysql-virtual_mailbox_limit_maps.cf
  virtual_mailbox_limit_override = yes
  virtual_maildir_limit_message = "The user you are trying to reach is over quota."
  virtual_overquota_bounce = yes
  proxy_read_maps = $local_recipient_maps $mydestination $virtual_alias_maps $virtual_alias_domains $virtual_mailbox_maps $virtual_mailbox_domains  $relay_recipient_maps $relay_domains $canonical_maps $sender_canonical_maps $recipient_canonical_maps $relocated_maps $transport_maps $mynetworks $virtual_mailbox_limit_maps

Now make a SSL cert. When it prompts you, add your details like city, state, etc.

openssl req -new -outform PEM -out smtpd.cert -newkey rsa:2048 -nodes -keyout smtpd.key -keyform PEM -days 365 -x509
chmod o= /etc/postfix/smtpd.key

Now configure authentication:

mkdir -p /var/spool/postfix/var/run/saslauthd
vi /etc/default/saslauthd
  START=yes <-- change to yes, default=no
  OPTIONS="-c -m /var/spool/postfix/var/run/saslauthd -r" <-- comment the line above it, and then add this one
vi /etc/pam.d/smtp <-- add the next lines:
  auth    required   pam_mysql.so user=mail_admin passwd=mail_admin_password host=127.0.0.1 db=mail table=users usercolumn=email passwdcolumn=password crypt=1
  account sufficient pam_mysql.so user=mail_admin passwd=mail_admin_password host=127.0.0.1 db=mail table=users usercolumn=email passwdcolumn=password crypt=1
vi /etc/postfix/sasl/smtpd.conf <-- create this file and put this stuff in it:
  pwcheck_method: saslauthd
  mech_list: plain login
  allow_plaintext: true
  auxprop_plugin: sql
  sql_engine: mysql
  sql_hostnames: 127.0.0.1
  sql_user: mail_admin
  sql_passwd: mail_admin_password
  sql_database: mail
  sql_select: select password from users where email = '%u@%r'
adduser postfix sasl
/etc/init.d/postfix restart
/etc/init.d/saslauthd restart

Now we get Courier (mail delivery thing) to authenticate:

vi /etc/courier/authdaemonrc
  authmodulelist="authmysql"
cp /etc/courier/authmysqlrc /etc/courier/authmysqlrc_orig
cat /dev/null > /etc/courier/authmysqlrc
vi /etc/courier/authmysqlrc
  MYSQL_SERVER localhost
  MYSQL_USERNAME mail_admin
  MYSQL_PASSWORD mail_admin_password
  MYSQL_PORT 0
  MYSQL_DATABASE mail
  MYSQL_USER_TABLE users
  MYSQL_CRYPT_PWFIELD password
  #MYSQL_CLEAR_PWFIELD password
  MYSQL_UID_FIELD 5000
  MYSQL_GID_FIELD 5000
  MYSQL_LOGIN_FIELD email
  MYSQL_HOME_FIELD "/home/vmail"
  MYSQL_MAILDIR_FIELD CONCAT(SUBSTRING_INDEX(email,'@',-1),'/',SUBSTRING_INDEX(email,'@',1),'/')
  #MYSQL_NAME_FIELD
  MYSQL_QUOTA_FIELD quota
cd /etc/courier
rm -f /etc/courier/imapd.pem
rm -f /etc/courier/pop3d.pem
vi /etc/courier/imapd.cnf
  CN=mail.example.com  <-- change to your real domain
vi /etc/courier/pop3d.cnf 
  CN=mail.example.com  <-- change to your real domain
mkimapdcert
mkpop3dcert
/etc/init.d/courier-authdaemon restart
/etc/init.d/courier-imap restart
/etc/init.d/courier-imap-ssl restart
/etc/init.d/courier-pop restart
/etc/init.d/courier-pop-ssl restart

Now see if your courier is working by doing:

telnet localhost pop3
  Trying ::1...
  Connected to localhost.
  Escape character is '^]'.
  +OK Hello there.
  quit <-- type this
  +OK Better luck next time.
  Connection closed by foreign host.

Now set up your notification emails, change the email to whatever email account you want to get mail server health notifications on:

vi /etc/aliases
  root: you@yourdomain.com
newaliases
/etc/init.d/postfix restart

Now install your scanners and spam filters. Change these to reflect whatever scanners you happen to have/want. Also, you can add scanners later and just change the Amavisd-new files to point to them, or remove old scanners you don’t want to use anymore.

apt-get install amavisd-new spamassassin clamav clamav-daemon zoo unzip bzip2 libnet-ph-perl libnet-snpp-perl libnet-telnet-perl nomarch lzop pax

Now we configure the scanners:

vi /etc/amavis/conf.d/15-content_filter_mode <-- uncomment the next lines:
  @bypass_spam_checks_maps = (
   \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);
vi /etc/amavis/conf.d/50-user <-- add the next line in the middle of the file:
  $pax='pax';
adduser clamav amavis
/etc/init.d/amavis restart
/etc/init.d/clamav-freshclam restart
/etc/init.d/clamav-daemon restart
postconf -e 'content_filter = amavis:[127.0.0.1]:10024'
postconf -e 'receive_override_options = no_address_mappings'
vi /etc/postfix/master.cf <-- add these next lines to the end of the file:
  amavis unix - - - - 2 smtp
        -o smtp_data_done_timeout=1200
        -o smtp_send_xforward_command=yes
  127.0.0.1:10025 inet n - - - - smtpd
        -o content_filter=
        -o local_recipient_maps=
        -o relay_recipient_maps=
        -o smtpd_restriction_classes=
        -o smtpd_client_restrictions=
        -o smtpd_helo_restrictions=
        -o smtpd_sender_restrictions=
        -o smtpd_recipient_restrictions=permit_mynetworks,reject
        -o mynetworks=127.0.0.0/8
        -o strict_rfc821_envelopes=yes
        -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks
/etc/init.d/posfix restart

Now postfix should be listening on port 25, and Amavisd-new should be listening on port 10024. Check this by running (your should see at least these two lines in the output):

netstat -tap
tcp        0      0 *:smtp                  *:*                     LISTEN      23899/master    
tcp        0      0 localhost:10024         *:*                     LISTEN      27469/amavisd-new (

Now we install blacklisting doo-dads and link them into Amavis so your mail will be filtered through them:

apt-get install razor pyzor
cd /usr/src
wget http://www.dcc-servers.net/dcc/source/dcc-dccproc.tar.Z
tar xzvf dcc-dccproc.tar.Z
cd dcc-dccproc-1.3.147
./configure --with-uid=amavis
make
make install
chown -R amavis:amavis /var/dcc
ln -s /var/dcc/libexec/dccifd /usr/local/bin/dccifd
vi /etc/spamassassin/local.cf <-- add these lines to end of file:
  #dcc
  use_dcc 1
  dcc_path /usr/local/bin/dccproc
  #pyzor
  use_pyzor 1
  pyzor_path /usr/bin/pyzor
  #razor
  use_razor2 1
  razor_config /etc/razor/razor-agent.conf
  #bayes
  use_bayes 1
  use_bayes_rules 1
  bayes_auto_learn 1
vi /etc/spamassassin/v310.pre
  loadplugin Mail::SpamAssassin::Plugin::DCC <-- uncomment this line
spamassassin --lint <-- look for errors and fix them
/etc/init.d/amavis restart
sa-update --no-gpg
crontab -e <-- add the next line to the end of the file:
  47 2 */2 * * /usr/bin/sa-update --no-gpg &> /dev/null

now your spamassassin will update every other morning at 2:47 a.m. Now we set up quota (or you don’t have to):

cd /usr/local/sbin/
vi quota_notify

there’s a lot of cut/paste for this tasty bit of code (props to its creator jps@tntmax.com). Change the emails for postmaster to a real email address:

#!/usr/bin/perl -w
# Author <jps@tntmax.com>
#
# This script assumes that virtual_mailbox_base in defined
# in postfix's main.cf file. This directory is assumed to contain
# directories which themselves contain your virtual user's maildirs.
# For example:
#
# -----------/
#            |
#            |
#    home/vmail/domains/
#        |          |
#        |          |
#  example.com/  foo.com/
#                   |
#                   |
#           -----------------
#           |       |       |
#           |       |       |
#         user1/   user2/  user3/
#                           |
#                           |
#                        maildirsize
#
use strict;
my $POSTFIX_CF = "/etc/postfix/main.cf";
my $MAILPROG = "/usr/sbin/sendmail -t";
my $WARNPERCENT = 80;
my @POSTMASTERS = ('postmaster@domain.tld');
my $CONAME = 'My Company';
my $COADDR = 'postmaster@domain.tld';
my $SUADDR = 'postmaster@domain.tld';
my $MAIL_REPORT = 1;
my $MAIL_WARNING = 1;
#get virtual mailbox base from postfix config
open(PCF, "< $POSTFIX_CF") or die $!;
my $mboxBase;
while (<PCF>) {
   next unless /virtual_mailbox_base\s*=\s*(.*)\s*/;
   $mboxBase = $1;
}
close(PCF);
#assume one level of subdirectories for domain names
my @domains;
opendir(DIR, $mboxBase) or die $!;
while (defined(my $name = readdir(DIR))) {
   next if $name =~ /^\.\.?$/;        #skip '.' and '..'
   next unless (-d "$mboxBase/$name");
   push(@domains, $name);
}
closedir(DIR);
#iterate through domains for username/maildirsize files
my @users;
chdir($mboxBase);
foreach my $domain (@domains) {
        opendir(DIR, $domain) or die $!;
        while (defined(my $name = readdir(DIR))) {
           next if $name =~ /^\.\.?$/;        #skip '.' and '..'
           next unless (-d "$domain/$name");
      push(@users, {"$name\@$domain" => "$mboxBase/$domain/$name"});
        }
}
closedir(DIR);
#get user quotas and percent used
my (%lusers, $report);
foreach my $href (@users) {
   foreach my $user (keys %$href) {
      my $quotafile = "$href->{$user}/maildirsize";
      next unless (-f $quotafile);
      open(QF, "< $quotafile") or die $!;
      my ($firstln, $quota, $used);
      while (<QF>) {
         my $line = $_;
              if (! $firstln) {
                 $firstln = 1;
                 die "Error: corrupt quotafile $quotafile"
                    unless ($line =~ /^(\d+)S/);
                 $quota = $1;
            last if (! $quota);
            next;
         }
         die "Error: corrupt quotafile $quotafile"
            unless ($line =~ /\s*(-?\d+)/);
         $used += $1;
      }
      close(QF);
      next if (! $used);
      my $percent = int($used / $quota * 100);
      $lusers{$user} = $percent unless not $percent;
   }
}
#send a report to the postmasters
if ($MAIL_REPORT) {
   open(MAIL, "| $MAILPROG");
   select(MAIL);
   map {print "To: $_\n"} @POSTMASTERS;
   print "From: $COADDR\n";
   print "Subject: Daily Quota Report.\n";
   print "DAILY QUOTA REPORT:\n\n";
   print "----------------------------------------------\n";
   print "| % USAGE |            ACCOUNT NAME          |\n";
   print "----------------------------------------------\n";
   foreach my $luser ( sort { $lusers{$b} <=> $lusers{$a} } keys %lusers ) {
      printf("|   %3d   | %32s |\n", $lusers{$luser}, $luser);
      print "---------------------------------------------\n";
   }
        print "\n--\n";
        print "$CONAME\n";
        close(MAIL);
}
#email a warning to people over quota
if ($MAIL_WARNING) {
        foreach my $luser (keys (%lusers)) {
           next unless $lusers{$luser} >= $WARNPERCENT;       # skip those under quota
           open(MAIL, "| $MAILPROG");
           select(MAIL);
           print "To: $luser\n";
      map {print "BCC: $_\n"} @POSTMASTERS;
           print "From: $SUADDR\n";
           print "Subject: WARNING: Your mailbox is $lusers{$luser}% full.\n";
           print "Reply-to: $SUADDR\n";
           print "Your mailbox: $luser is $lusers{$luser}% full.\n\n";
           print "Once your e-mail box has exceeded your monthly storage quota\n";
      print "your monthly billing will be automatically adjusted.\n";
      print "Please consider deleting e-mail and emptying your trash folder to clear some space.\n\n";
           print "Contact <$SUADDR> for further assistance.\n\n";
           print "Thank You.\n\n";
           print "--\n";
           print "$CONAME\n";
           close(MAIL);
        }
}

Now we automate and make it executable:

chmod 755 quota_notify
crontab -e <-- add next line to end of file:
0 0 * * * /usr/local/sbin/quota_notify &> /dev/null

Okay, now let’s test Postfix by trying to use it from localhost. You’re really just looking to see if it tries to login and then after typing ehlo localhost gives you the 250-STARTTLS line:

telnet localhost 25
Trying ::1...
Connected to localhost.
Escape character is '^]'.
220 mail.setestbox.com ESMTP Postfix (Debian/GNU)
ehlo localhost <-- type this
250-mail.setestbox.com
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-STARTTLS
250-AUTH PLAIN LOGIN
250-AUTH=PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
quit
221 2.0.0 Bye
Connection closed by foreign host.

Now add a real email address and test:

mysql -u root -p
USE mail;
INSERT INTO `domains` (`domain`) VALUES ('example.com');
INSERT INTO `users` (`email`, `password`, `quota`) VALUES ('sales@example.com', ENCRYPT('secret'), 10485760);
quit;
postfix reload

Now let’s send an email to that user, which will create the mail directory that we’ll log into later:

apt-get install mailutils
mailx sales@example.com
Subject: Welcome <-- ENTER
Welcome! Have fun with your new mail account. <-- ENTER
<-- CTRL+D
Cc: <-- ENTER

Okay, now we install a webmail client called Roundcube. Of course you can test the server with some other email client like Outlook or whatever, but if you want webmail, this is a nice feature. Jessie doesn’t have roundcube in the repos, so you’ll have to download and configure it. If you have it in the repo, skip this step:

mkdir /usr/src/roundcube
cd /usr/src/roundcube
wget https://downloads.sourceforge.net/project/roundcubemail/roundcubemail/1.1.2/roundcubemail-1.1.2-complete.tar.gz
tar xfj roundcubemail-1.1.2-complete.tar.gz
cd roundcubemail-1.1.2
mkdir /var/www/mail
rsync -hauv ./ /var/www/mail/
cd /var/www
chown -R www-data.www-data mail
vi /etc/apache2/sites-available/000-default
  (add these next lines somewhere in the middle)
  Alias /mail/ /var/www/mail/
  <Directory "/var/www/mail">
        Options Indexes Includes FollowSymLinks Multiviews
        AllowOverride All
        Order allow,deny
        Allow from all
  </Directory>
/etc/init.d/apache2 reload
<pre lang="bash">
Now create a roudcube mysql database like:
<pre lang="bash">
mysql -u root -p
create database roundcube;
grant all privileges on roundcube.* TO 'roundcubemail'@'localhost' identified by 'some_password' with grant option;
quit;
cd /var/www/mail/SQL
mysql -u root -p roundcube < mysql.initial.sql

Now go do the rest of the install by opening a browser to:

http://mail.whateverservername.com/mail/installer/

Just follow prompts and enter your database password you set up. The rest of the settings should be fine. If you want to use en_US for the language, do that. Now remove the installer folder (after it succeeds with all the checks and visit the site:

cd /var/www/mail/
rm -rf installer
http://mail.whateversite.com/mail/
apt-get install roundcube
use db-common and type in the passwords
vi /etc/roundcube/apache.conf <-- uncomment the next line
  Alias /roundcube /var/lib/roundcube
/etc/init.d/apache2 reload

Now try to login to your webmail at:

Home

and try to login as that user@example.com and see if you got your email 🙂