Mail server setup with postfix

This article provides steps to install complete mail server environment.

Install CentOS6 with minimal installation

Disable selinux

sed -i -e 's/enforcing/disabled/' /etc/sysconfig/selinux

Import GPG keys

rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY*
Enable RPMforge
rpm --import http://dag.wieers.com/rpm/packages/RPM-GPG-KEY.dag.txt
wget http://pkgs.repoforge.org/rpmforge-release/rpmforge-release-0.5.2-2.el6.rf.x86_64.rpm
rpm -ivh rpmforge-release-0.5.2-2.el6.rf.x86_64.rpm
Enable EPEL
rpm --import https://fedoraproject.org/static/0608B895.txt
wget http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
rpm -ivh epel-release-6-8.noarch.rpm
Install yum-porities (Important to overcome package conflict)
yum install yum-priorities
Set EPEL to priority 10
[epel]
name=Extra Packages for Enterprise Linux 6 - $basearch
#baseurl=http://download.fedoraproject.org/pub/epel/6/$basearch
mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=epel-6&arch=$basearch
failovermethod=priority
enabled=1
priority=10
gpgcheck=1

Build Courier-IMAP (Normally build server should separate one)

yum groupinstall 'Development Tools'
yum install ntp httpd mysql-server php php-mysql php-mbstring rpm-build gcc mysql-devel openssl-devel cyrus-sasl-devel pkgconfig zlib-devel phpMyAdmin pcre-devel openldap-devel postgresql-devel expect libtool-ltdl-devel openldap-servers libtool gdbm-devel pam-devel gamin-devel libidn-devel db4-devel mod_ssl telnet sqlite-devel
Create build user
useradd builder
Add user to sudoers
builder   ALL=(ALL)       ALL
Create build directory
su builder
mkdir $HOME/rpm
mkdir $HOME/rpm/SOURCES
mkdir $HOME/rpm/SPECS
mkdir $HOME/rpm/BUILD
mkdir $HOME/rpm/BUILDROOT
mkdir $HOME/rpm/SRPMS
mkdir $HOME/rpm/RPMS
mkdir $HOME/rpm/RPMS/i386
mkdir $HOME/rpm/RPMS/x86_64


echo "%_topdir $HOME/rpm" >> $HOME/.rpmmacros

mkdir $HOME/downloads
cd $HOME/downloads

wget --no-check-certificate https://sourceforge.net/projects/courier/files/authlib/0.65.0/courier-authlib-0.65.0.tar.bz2/download
wget --no-check-certificate https://sourceforge.net/projects/courier/files/imap/4.12.0/courier-imap-4.12.0.tar.bz2/download
wget --no-check-certificate https://sourceforge.net/projects/courier/files/maildrop/2.6.0/maildrop-2.6.0.tar.bz2/download


sudo rpmbuild -ta courier-authlib-0.65.0.tar.bz2

sudo ls -l /root/rpmbuild/RPMS/x86_64
Install authlib RPMs
sudo rpm -ivh /root/rpmbuild/RPMS/x86_64/courier-authlib-0.65.0-1.el6.x86_64.rpm /root/rpmbuild/RPMS/x86_64/courier-authlib-mysql-0.65.0-1.el6.x86_64.rpm /root/rpmbuild/RPMS/x86_64/courier-authlib-devel-0.65.0-1.el6.x86_64.rpm
Build courier-imap
cd $HOME/downloads

sudo mkdir -p /var/cache/ccache/tmp
sudo chmod o+rwx /var/cache/ccache/
sudo chmod 777 /var/cache/ccache/tmp


rpmbuild -ta courier-imap-4.12.0.tar.bz2

cd $HOME/rpm/RPMS/x86_64
Install courier imap
rpm -ivh courier-imap-4.12.0-1.x86_64.rpm
Build maildrop
cd $HOME/downloads

sudo rpmbuild -ta maildrop-2.6.0.tar.bz2

ls -l /root/rpmbuild/RPMS/x86_64
Install maildrop
rpm -ivh /root/rpmbuild/RPMS/x86_64/maildrop-2.6.0-1.x86_64.rpm
Install postfix
yum install postfix
Install MySQL
yum install mysql-server
Create DB mail
mysqladmin -u root -p create mail

mysql -u root -p
Create mail db user and access
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;
Create tables
USE mail;

CREATE TABLE domains (
domain varchar(50) NOT NULL,
PRIMARY KEY (domain) )
ENGINE=MyISAM;

+--------+-------------+------+-----+---------+-------+
| Field  | Type        | Null | Key | Default | Extra |
+--------+-------------+------+-----+---------+-------+
| domain | varchar(50) | NO   | PRI | NULL    |       |
+--------+-------------+------+-----+---------+-------+

CREATE TABLE forwardings (
source varchar(80) NOT NULL,
destination TEXT NOT NULL,
PRIMARY KEY (source) )
ENGINE=MyISAM;

+-------------+-------------+------+-----+---------+-------+
| Field       | Type        | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+-------+
| source      | varchar(80) | NO   | PRI | NULL    |       |
| destination | text        | NO   |     | NULL    |       |
+-------------+-------------+------+-----+---------+-------+

CREATE TABLE users (
email varchar(80) NOT NULL,
password varchar(20) NOT NULL,
quota bigint(20) DEFAULT '10485760',
PRIMARY KEY (email)
) ENGINE=MyISAM;

+----------+-------------+------+-----+----------+-------+
| Field    | Type        | Null | Key | Default  | Extra |
+----------+-------------+------+-----+----------+-------+
| email    | varchar(80) | NO   | PRI | NULL     |       |
| password | varchar(20) | NO   |     | NULL     |       |
| quota    | bigint(20)  | YES  |     | 10485760 |       |
+----------+-------------+------+-----+----------+-------+

CREATE TABLE transport (
domain varchar(128) NOT NULL default '',
transport varchar(128) NOT NULL default '',
UNIQUE KEY domain (domain)
) ENGINE=MyISAM;

+-----------+--------------+------+-----+---------+-------+
| Field     | Type         | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+-------+
| domain    | varchar(128) | NO   | PRI |         |       |
| transport | varchar(128) | NO   |     |         |       |
+-----------+--------------+------+-----+---------+-------+
Configure postfix to work with MySQL tables
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
Create account
groupadd -g 5000 vmail
useradd -g vmail -u 5000 vmail -d /home/vmail -m
Change Postfix configuration
postconf -e 'myhostname = server1.example.com'
postconf -e 'mydestination = server1.example.com, localhost, localhost.localdomain'
postconf -e 'mynetworks = 127.0.0.0/8'
postconf -e 'virtual_alias_domains ='
postconf -e ' virtual_alias_maps = proxy:mysql:/etc/postfix/mysql-virtual_forwardings.cf, mysql:/etc/postfix/mysql-virtual_email2email.cf'
postconf -e 'virtual_mailbox_domains = proxy:mysql:/etc/postfix/mysql-virtual_domains.cf'
postconf -e 'virtual_mailbox_maps = proxy:mysql:/etc/postfix/mysql-virtual_mailboxes.cf'
postconf -e 'virtual_mailbox_base = /home/vmail'
postconf -e 'virtual_uid_maps = static:5000'
postconf -e 'virtual_gid_maps = static:5000'
postconf -e 'smtpd_sasl_auth_enable = yes'
postconf -e 'broken_sasl_auth_clients = yes'
postconf -e 'smtpd_sasl_authenticated_header = yes'
postconf -e 'smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination'
postconf -e 'smtpd_use_tls = yes'
postconf -e 'smtpd_tls_cert_file = /etc/postfix/smtpd.cert'
postconf -e 'smtpd_tls_key_file = /etc/postfix/smtpd.key'
postconf -e 'transport_maps = proxy:mysql:/etc/postfix/mysql-virtual_transports.cf'
postconf -e 'virtual_create_maildirsize = yes'
postconf -e 'virtual_maildir_extended = yes'
postconf -e 'virtual_mailbox_limit_maps = proxy:mysql:/etc/postfix/mysql-virtual_mailbox_limit_maps.cf'
postconf -e 'virtual_mailbox_limit_override = yes'
postconf -e 'virtual_maildir_limit_message = "The user you are trying to reach is over quota."'
postconf -e 'virtual_overquota_bounce = yes'
postconf -e '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'
postconf -e 'inet_interfaces = all'



cd /etc/postfix
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
========
Configure SASLauthd
vi /etc/sasl2/smtpd.conf

pwcheck_method: authdaemond
log_level: 3
mech_list: PLAIN LOGIN
authdaemond_path:/var/spool/authdaemon/socket

chmod 755 /var/spool/authdaemon
chkconfig --levels 235 courier-authlib on
/etc/init.d/courier-authlib start

chkconfig --levels 235 sendmail off
chkconfig --levels 235 postfix on
chkconfig --levels 235 saslauthd on
/etc/init.d/sendmail stop
/etc/init.d/postfix start
/etc/init.d/saslauthd start
Configure Courier
vi /etc/authlib/authdaemonrc

authmodulelist="authmysql"
#authmodulelist="authuserdb authpam authpgsql authldap authmysql authcustom authpipe"

cp /etc/authlib/authmysqlrc /etc/authlib/authmysqlrc_orig
cat /dev/null > /etc/authlib/authmysqlrc
vi /etc/authlib/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

chkconfig --levels 235 courier-imap on
/etc/init.d/courier-authlib restart
/etc/init.d/courier-imap restart

cd /usr/lib/courier-imap/share
rm -f imapd.pem
rm -f pop3d.pem

vi /usr/lib/courier-imap/etc/imapd.cnf

CN=server1.example.com

vi /usr/lib/courier-imap/etc/pop3d.cnf

CN=server1.example.com

./mkimapdcert
./mkpop3dcert

/etc/init.d/courier-authlib restart
/etc/init.d/courier-imap restart



vi /etc/aliases

postmaster: root
root: postmaster@yourdomain.tld
Install Amavisd-new, SpamAssassin And ClamAV
yum install amavisd-new spamassassin clamav clamd unzip bzip2 unrar perl-DBD-mysql
Edit /etc/amavisd/amavisd.conf
$mydomain = 'localhost';

$sa_tag_level_deflt  = 2.0;  # add spam info headers if at, or above that level
$sa_tag2_level_deflt = 4.0;  # add 'spam detected' headers at that level
$sa_kill_level_deflt = $sa_tag2_level_deflt;  # triggers spam evasive actions (e.g. blocks mail)
$sa_dsn_cutoff_level = 10;   # spam level beyond which a DSN is not sent


@lookup_sql_dsn =
( ['DBI:mysql:database=mail;host=127.0.0.1;port=3306', 'mail_admin', 'mail_admin_password'] );

$sql_select_policy = 'SELECT "Y" as local FROM domains WHERE CONCAT("@",domain) IN (%k)';

$sql_select_white_black_list = undef;  # undef disables SQL white/blacklisting

$recipient_delimiter = '+';                # (default is '+')

$replace_existing_extension = 1;        # (default is false)

$localpart_is_case_sensitive = 0;        # (default is false)


$recipient_delimiter = undef;  # undef disables address extensions altogether

$final_virus_destiny      = D_REJECT;
$final_banned_destiny     = D_REJECT;
$final_spam_destiny       = D_PASS;
$final_bad_header_destiny = D_PASS;


chkconfig --levels 235 amavisd on
chkconfig --levels 235 clamd.amavisd on
/usr/bin/freshclam
/etc/init.d/amavisd start
/etc/init.d/clamd.amavisd start
Configure postfix to use amavis
postconf -e 'content_filter = amavis:[127.0.0.1]:10024' 
postconf -e 'receive_override_options = no_address_mappings'
Edit /etc/postfix/master.cf
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
        -o smtpd_bind_address=127.0.0.1
        
/etc/init.d/postfix restart
Create schedule for Spamassassin
sa-update --no-gpg

crontab -e

23 4 */2 * * /usr/bin/sa-update --no-gpg &> /dev/null
User quota notification
cd /usr/local/sbin/ Create quota_notify
#!/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);
        }
}
Schedule job
chmod 755 quota_notify

crontab -e

0 0 * * * /usr/local/sbin/quota_notify &> /dev/null
Create domain and accounts
INSERT INTO `domains` (`domain`) VALUES ('example.com');
INSERT INTO `users` (`email`, `password`, `quota`) VALUES ('sales@example.com', ENCRYPT('secret'), 10485760);

INSERT INTO `users` (`email`, `password`, `quota`) VALUES (‘noreply@cloudmail.infotheater.net’, ENCRYPT(‘demotest2014’), 10485760);
INSERT INTO `users` (`email`, `password`, `quota`) VALUES (‘postmaster@demomail.infotheater.net’, ENCRYPT(‘demotest2014’), 10485760);

INSERT INTO `forwardings` (`source`, `destination`) VALUES ('info@example.com', 'sales@example.com');
INSERT INTO `transport` (`domain`, `transport`) VALUES ('example.com', 'smtp:mail.example.com');
Create Maildir
yum install mailx

mailx sales@example.com

[root@server1 ~]# mailx sales@example.com
Subject: Welcome <-- ENTER
Welcome! Have fun with your new mail account. <-- ENTER
<-- CTRL+D
EOT
Install Webmail
yum install squirrelmail php-pear-DB
/etc/init.d/httpd restart

cd /usr/share/squirrelmail/plugins
wget http://www.squirrelmail.org/plugins/change_sqlpass-3.3-1.2.tar.gz
tar xvfz change_sqlpass-3.3-1.2.tar.gz
cd change_sqlpass
cp config.php.sample config.php
Edit config.php
[...]
$csp_dsn = 'mysql://mail_admin:mail_admin_password@localhost/mail';
[...]
$lookup_password_query = 'SELECT count(*) FROM users WHERE email = "%1" AND password = %4';
[...]
$password_update_queries = array('UPDATE users SET password = %4 WHERE email = "%1"');
[...]
$password_encryption = 'MYSQLENCRYPT';
[...]
$csp_salt_static = 'LEFT(password, 2)';
[...]
//$csp_salt_query = 'SELECT salt FROM users WHERE username = "%1"';
[...]
$csp_delimiter = '@';
[...]


cd /usr/share/squirrelmail/plugins
wget http://www.squirrelmail.org/countdl.php?fileurl=http%3A%2F%2Fwww.squirrelmail.org%2Fplugins%2Fcompatibility-2.0.16-1.0.tar.gz
tar xvfz compatibility-2.0.16-1.0.tar.gz
Run /usr/share/squirrelmail/config/conf.pl
SquirrelMail Configuration : Read: config.php (1.4.0)

Main Menu --
1.  Organization Preferences
2.  Server Settings
3.  Folder Defaults
4.  General Options
5.  Themes
6.  Address Books
7.  Message of the Day (MOTD)
8.  Plugins
9.  Database
10. Languages

D.  Set pre-defined settings for specific IMAP servers

C   Turn color off
S   Save data
Q   Quit

Command >> <-- D


Please select your IMAP server:
    bincimap    = Binc IMAP server
    courier     = Courier IMAP server
    cyrus       = Cyrus IMAP server
    dovecot     = Dovecot Secure IMAP server
    exchange    = Microsoft Exchange IMAP server
    hmailserver = hMailServer
    macosx      = Mac OS X Mailserver
    mercury32   = Mercury/32
    uw          = University of Washington's IMAP server
    gmail       = IMAP access to Google mail (Gmail) accounts

    quit        = Do not change anything
Command >> <-- courier

imap_server_type = courier
        default_folder_prefix = INBOX.
                trash_folder = Trash
                sent_folder = Sent
                draft_folder = Drafts
            show_prefix_option = false
        default_sub_of_inbox = false
show_contain_subfolders_option = false
            optional_delimiter = .
                delete_folder = true

Press enter to continue... <-- ENTER
Edit config_local.php
vi /etc/squirrelmail/config_local.php

//$default_folder_prefix                = '';

Comments