CentOS + Postfix + MySQL + TLS + SASL + Maildrop + SQLgrey + Amavisd + SpamAssassin + ClamAV + Courier-IMAP + Courier-POP3d + SqWebMail + Horde IMP

Original of this document available at http://www.bowe.id.au/michael/isp/postfix-server.htm


NAVIGATION LINKS

MAIN CHAPTERS :

OPTIONAL CHAPTERS ( FOR LARGER / MORE-COMPLEX SERVERS )

"UNDER CONSTRUCTION" CHAPTERS

OTHER


CentOS is an Enterprise-class Linux Distribution derived from Redhat Enterprise Linux - pretty much the only difference is the Redhat branding has been removed. As such CentOS is a great choice of O/S because you can be assured it will be stable and well supported for drivers.

Fedora is another O/S that is very similar to CentOS. This guide will work fine on Fedora as well, with no changes required to the installation steps. If you are going to use Fedora there are a few things you need to be aware of :

If you are going to host more than few hundred mailboxes, I would suggest you have at least 2 disk drives in the server. IDE drives are OK, but SCSI ( possibly running in RAID configuration) will give better results when there are high disk loads

It is possible to build large servers using this design. Examples built include :

With these servers, the system load values on the mailserver are typically around 5 peaking at about 10. The Amavis boxes have similar readings. POP3 and SqWebMail used mainly, with a little bit of IMAP and IMP. Common to see around 100 POP3 sessions active. SMTP-MX concurrency set around 100, Maildrop concurrency set around 20.


INSTALL CENTOS

http://www.centos.org

Boot the CentOS 4.4 disk

Language selection :English (English)

Keyboard Configuration : US English

Upgrade Examine : Install CentOS

Installation Type : Server

Disk Partition Setup : Manually partition with Disk Druid

Your 1st disk doesn't need to be very large.

Your 2nd disk should be large. If its an IDE drive, you should put it on a separate controller to the 1st disk:

If your server is going to be busy, and you have a 3rd disk you can make further enhancements. The disk doesn't need to be very large. Note, that if you have IDE drives, then this 3rd drive wont really help much unless you have a 3rd IDE controller available

Boot Loader Configuration : press next

Network Configuration :

Firewall Configuration :

Additional Language Support :

Timezone Selection :

Set root Password : ChooseSomethingGood!

Package Group Selection :


TWEAK THE CENTOS INSTALL

Configure the internationalisation settings. By default CentOS will set UTF8 ( Unicode ) character encoding schemes. but I find this causes problems with the console display in my SSH client. Also some perl programs are known to have problems with the UTF8

cp /etc/sysconfig/i18n /etc/sysconfig/i18n.original
vi /etc/sysconfig/i18n
# Remove any UTF-8 entries from the LANG line
# ie change it from LANG="en_US.UTF-8" to LANG="en_US"
LANG="en_US"

Import the GPG keys for software packages

rpm --import /usr/share/rhn/RPM-GPG-KEY*

Configure the log rotation scheme, to rotate daily, for 30 days, compressing the old logs

 vi /etc/logrotate.conf
#weekly
daily
#rotate 4
rotate 30
#compress
compress

Configure the NTP clock sync ( very important that mail servers have correct clock! )

vi /etc/ntp.conf
#server 0.pool.ntp.org
#server 1.pool.ntp.org
#server 2.pool.ntp.org
server ntp.yourdomain.com

Tweak the firewall rules. Need to add some extra ports

TCP 20  : ftp-data
TCP 21  : ftp
TCP 110 : pop3
TCP 143 : imap
TCP 443 : https
TCP 465 : smtps
TCP 993 : imaps
TCP 995 : pop3s
UDP 161 : snmp
vi /etc/sysconfig/iptables
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 110 -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 143 -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 443 -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 465 -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 993 -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 995 -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state NEW -m udp -p udp --dport 161 -j ACCEPT

Configure which services are starting at boot time. The aim is to disable any unneeded services.

chkconfig apmd off
chkconfig bluetooth off
chkconfig cpuspeed off
chkconfig cups off
chkconfig httpd on
chkconfig isdn off
chkconfig mysqld on
chkconfig netfs off
chkconfig nfslock off
chkconfig ntpd on
chkconfig pcmcia off
chkconfig portmap off
chkconfig rpcgssd off
chkconfig rpcidmapd off
chkconfig saslauthd off
chkconfig sendmail off
# these two are in Fedora, but not CentOS
chkconfig mDNSResponder off
chkconfig nifd off

For your spool and mailbox partitions, set the noatime flag. This is an important performance tweak which works by preventing the need for writing any updates to the disk when processes are reading files ( eg when Postfix's qmgr process scans mail in the queue ). I had a busy server where loads dropped from a steady 20 to sub 10's by making this simple change! Also, as a security precaution, tweak the fstab, to disable /tmp from permitting SUID or exec functionality.

vi /etc/fstab
LABEL=/tmp /tmp ext3 defaults,nosuid,noexec 1 2
LABEL=/var/vmail /var/vmail ext3 defaults,noatime 1 2
# and if you made a dedicated partition for the postfix mail queue...
LABEL=/var/spool/postf /var/spool/postfix ext3 defaults,noatime 1 2

That noexec fstab tweak has an unfortunate side-effect of causing the "logrotate" script to break. A workaround for this is :

mkdir /var/logrotate.tmp
vi /etc/cron.daily/logrotate
#!/bin/sh
export TMPDIR=/var/logrotate.tmp
/usr/sbin/logrotate /etc/logrotate.conf

Take advantage of the colors and other advanced features of the vim editor, compared with basic vi editor

# only required on Fedora, a CentOS install appears to already default to vim
echo "alias vi='vim'" >> /root/.bashrc

If you are using CentOS, you can grab the "fastestmirror plugin" for yum, as this should allow your rpm downloads to run quicker

yum install centos-yum yum-plugin-fastestmirror
vi /etc/yum.conf
plugins=1

Then give the server a reboot

shutdown -r now

UPDATE ALL THE RPMS

Run the update manager. It will go and look for updated RPMs, then will download and install them.

TIP : If you are running a 64 bit platform eg Opteron, add this line to the /etc/yum.conf to prevent conflicts between the 64bit and non-64bit libraries
vi /etc/yum.conf
# add this line if you are running 64bit
exclude=*.i386 *.i586 *.i686

Be warned that the first update pass on Fedora can be pretty large. Its not uncommon to see 250M+ of updates to be downloaded. CentOS isnt so "bleeding edge" so for that platform there are usually a lot less updates to download.

yum update

Enable ongoing auto-updating

crontab -e
# Keep up to date. Lets only do it during business hours, just to be safe :-)
#
# Dont download kernel updates, or our /boot will overflow eventually. 
# Dont download mysql updates, as I have seen mysql shutdown and not automatically come back up.
#
# If we want to update kernel or mysql, we can run these manually via a "yum update".
50 10 * * 1-5 /usr/bin/yum --exclude=kernel* --exclude=hal --exclude=mysql*  -y update

Give the server are reboot, so the new kernel that yum downloaded can take effect

shutdown -r now

CREATE THE SSL CERTIFICATES

SSL certificates will be used by Postfix (for SMTPS and TLS), Courier (for IMAPS and POP3S) and Apache (for HTTPS)

mkdir /usr/local/ssl
cd /usr/local/ssl
# Generate the RSA private-key for the server.
# We don't want a pass phrase on this key, otherwise it will need to be entered 
# every time courier/apache/postfix starts.
openssl genrsa -out mail.yourdomain.com.key 1024
Generating RSA private key, 1024 bit long modulus
...................++++++
........................++++++
e is 65537 (0x10001)
# Tighten the permissions on this key file
chmod 600 mail.yourdomain.com.key
# Generate a certificate request
openssl req -new -key mail.yourdomain.com.key -out mail.yourdomain.com.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [GB]:AU
State or Province Name (full name) [Berkshire]:South Australia
Locality Name (eg, city) [Newbury]:Adelaide
Organization Name (eg, company) [My Company Ltd]:Yourcompany Limited
Organizational Unit Name (eg, section) []:Hosting Services
Common Name (eg, your name or your server's hostname) []:mail.yourdomain.com
Email Address []:postmaster@yourdomain.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:password
An optional company name []:

At this point you would send your CSR off to a Certificate Authority for signing (such as Verisign or Thawte) . However if you wanted to do some in-house testing, we can set ourselves up as a CA, and then sign the CSR ourselves :

# generate RSA private-key for the CA
openssl genrsa -des3 -out ca.key 1024
Generating RSA private key, 1024 bit long modulus
.....................++++++
...............++++++
e is 65537 (0x10001)
Enter pass phrase for ca.key:capass
Verifying - Enter pass phrase for ca.key:capass
# tighten permissions on this private key
chmod 600 ca.key
# create a self signed CA certificate
openssl req -new -x509 -days 365 -key ca.key -out ca.crt
Enter pass phrase for ca.key:capass
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [GB]:AU
State or Province Name (full name) [Berkshire]:SomeState
Locality Name (eg, city) [Newbury]:SomePlace
Organization Name (eg, company) [My Company Ltd]:Test CA Company
Organizational Unit Name (eg, section) []:SomeGroup
Common Name (eg, your name or your server's hostname) []:CA Signing Biz
Email Address []:postmaster@nowhere
# Use this test CA to sign our server cert 
openssl x509 -req -days 365 -CA ca.crt -CAkey ca.key -set_serial 01 -in mail.yourdomain.com.csr -out mail.yourdomain.com.crt
Signature ok
subject=/C=AU/ST=SomeState/L=SomePlace/O=Test CA Company/OU=SomeGroup/CN=CA Signing Biz/emailAddress=postmaster@nowhere
Getting CA Private Key
Enter pass phrase for ca.key:capass

Combine the server key and certificate into a single file. Postfix and Apache can deal with two separate files, but Courier needs them both in one. To try and keep things consistent we will use a single file with all 3 apps

# create the PEM file in the format that courier wants (both the key and the cert in one file)
cat mail.yourdomain.com.key mail.yourdomain.com.crt > mail.yourdomain.com.pem
chmod 600 mail.yourdomain.com.pem

OK so you should now have something like this :

ls -al
total 36
drwxr-xr-x   2 root root 4096 Nov 28 22:02 .
drwxr-xr-x  14 root root 4096 Nov 20 21:50 ..
-rw-r--r--   1 root root 1371 Nov 28 21:50 ca.crt
-rw-------   1 root root  963 Nov 28 21:47 ca.key
-rw-r--r--   1 root root 1001 Nov 28 21:51 mail.yourdomain.com.crt
-rw-r--r--   1 root root  773 Nov 28 21:45 mail.yourdomain.com.csr
-rw-------   1 root root  887 Nov 28 21:45 mail.yourdomain.com.key
-rw-------   1 root root 1888 Nov 28 22:02 mail.yourdomain.com.pem

MYSQL

http://www.mysql.com

MySQL has been installed as part of the CentOS installation. Databases will be stored in /var/lib/mysql

Tune, based on how busy the MySQL is going to be. There are a few sample cnf files supplied so choose the one that best matches your needs. (IMPORTANT NOTE : MySQL query caching isn't enabled in any of the sample files except for my-large and my-huge. If you decide to use one of the smaller config files, it will be worth you while to copy the query_cache_size setting across from the large/huge files)

mv /etc/my.cnf /etc/my.cnf.original
cp /usr/share/doc/mysql-server-4.1.20/my-medium.cnf /etc/my.cnf

. Edit the config file and make the following tweaks in the [mysqld] section

vi /etc/my.cnf
# put this one in if its not there already
query_cache_size= 64M
# Our databases will only be accessed by programs running locally, so disable TCP connections and replication functions
skip-networking
#log-bin
#Add an entry to tweak the max number of allowed connections
#The default is 100. On a busy server you will probably need to increase this
max_connections = 400
# Record of any queries that run slowly
log-slow-queries

Create the MySQL tables for Postfix to use.

mysql
GRANT SELECT ON postfix.* TO postfixuser@localhost IDENTIFIED BY 'postfixpass';
CREATE DATABASE postfix;
USE postfix;
CREATE TABLE mailbox_domains (
domain varchar(255) NOT NULL default '',
description varchar(255) NOT NULL default 'Postfix virtual mailbox domain',
maxaliases int(10) NOT NULL default '-1',
maxmailboxes int(10) NOT NULL default '-1',
maxquota int(10) NOT NULL default '-1',
server varchar(255) NOT NULL default '',
created datetime NOT NULL default '0000-00-00 00:00:00',
modified datetime NOT NULL default '0000-00-00 00:00:00',
modified_by varchar(255) NOT NULL default '',
active tinyint(1) NOT NULL default '1',
PRIMARY KEY (domain)
) TYPE=MyISAM COMMENT='Postfix Virtual Mailbox Domains';
CREATE TABLE mailbox (
email varchar(255) NOT NULL default '',
password varchar(255) NOT NULL default '',
clear_password varchar(255) NOT NULL default '',
name varchar(255) NOT NULL default '',
maildir varchar(255) NOT NULL default '',
mailquota int(10) NOT NULL default '20',
ftpquota int(10) NOT NULL default '20',
disableftp tinyint(1) NOT NULL default '0',
disableimap tinyint(1) NOT NULL default '0',
disablepop3 tinyint(1) NOT NULL default '0',
disablewebmail tinyint(1) NOT NULL default '0',
disablesmtpauth tinyint(1) NOT NULL default '0',
virus_lover tinyint(1) default NULL,
spam_lover tinyint(1) default NULL,
banned_files_lover tinyint(1) default NULL,
bad_header_lover tinyint(1) default NULL,
bypass_virus_checks tinyint(1) default NULL,
bypass_spam_checks tinyint(1) default NULL,
bypass_banned_checks tinyint(1) default NULL,
bypass_header_checks tinyint(1) default NULL,
spam_tag2_level float default NULL,
spam_kill_level float default NULL,
server varchar(255) NOT NULL default '',
created datetime NOT NULL default '0000-00-00 00:00:00',
modified datetime NOT NULL default '0000-00-00 00:00:00',
modified_by varchar(255) NOT NULL default '',
active tinyint(1) NOT NULL default '1',
PRIMARY KEY (email)
) TYPE=MyISAM COMMENT='Postfix - Virtual Mailbox Maps';
CREATE TABLE alias_domains (
domain varchar(255) NOT NULL default '',
description varchar(255) NOT NULL default 'Postfix virtual alias domain',
maxaliases int(10) NOT NULL default '-1',
server varchar(255) NOT NULL default '',
created datetime NOT NULL default '0000-00-00 00:00:00',
modified datetime NOT NULL default '0000-00-00 00:00:00',
modified_by varchar(255) NOT NULL default '',
active tinyint(1) NOT NULL default '1',
PRIMARY KEY (domain)
) TYPE=MyISAM COMMENT='Postfix Virtual Alias Domains';
CREATE TABLE alias (
address varchar(255) NOT NULL default '',
goto text NOT NULL,
server varchar(255) NOT NULL default '',
created datetime NOT NULL default '0000-00-00 00:00:00',
modified datetime NOT NULL default '0000-00-00 00:00:00',
modified_by varchar(255) NOT NULL default '',
active tinyint(1) NOT NULL default '1',
PRIMARY KEY (address)
) TYPE=MyISAM COMMENT='Postfix - Virtual Alias Maps';
CREATE TABLE client_access (
client varchar(255) NOT NULL default '',
response varchar(255) NOT NULL default '',
note varchar(255) NOT NULL default '',
server varchar(255) NOT NULL default '',
created datetime NOT NULL default '0000-00-00 00:00:00',
modified datetime NOT NULL default '0000-00-00 00:00:00',
modified_by varchar(255) NOT NULL default '',
active tinyint(1) NOT NULL default '1',
PRIMARY KEY (client)
) TYPE=MyISAM COMMENT='Postfix - Client Access';
CREATE TABLE sender_access (
sender varchar(255) NOT NULL default '',
response varchar(255) NOT NULL default '',
note varchar(255) NOT NULL default '',
server varchar(255) NOT NULL default '',
created datetime NOT NULL default '0000-00-00 00:00:00',
modified datetime NOT NULL default '0000-00-00 00:00:00',
modified_by varchar(255) NOT NULL default '',
active tinyint(1) NOT NULL default '1',
PRIMARY KEY (sender)
) TYPE=MyISAM COMMENT='Postfix - Sender Access';
CREATE TABLE recipient_access (
recipient varchar(255) NOT NULL default '',
response varchar(255) NOT NULL default '',
note varchar(255) NOT NULL default '',
server varchar(255) NOT NULL default '',
created datetime NOT NULL default '0000-00-00 00:00:00',
modified datetime NOT NULL default '0000-00-00 00:00:00',
modified_by varchar(255) NOT NULL default '',
active tinyint(1) NOT NULL default '1',
PRIMARY KEY (recipient)
) TYPE=MyISAM COMMENT='Postfix - Recipient Access';
quit

SASL ( FOR SMTP-AUTH )

http://asg.web.cmu.edu/cyrus/download/sasl/

Grab the SQL modules for SASL

yum install cyrus-sasl-sql cyrus-sasl-devel

Create a config file so that Postfix will be able to use the SASL libraries to do SMTP authentications via MySQL

vi /usr/lib/sasl2/postfix.conf
pwcheck_method: auxprop
auxprop_plugin: sql
mech_list: CRAM-MD5 DIGEST-MD5 PLAIN LOGIN
sql_engine: mysql
sql_hostnames: localhost
sql_user: postfixuser
sql_passwd: postfixpass
sql_database: postfix
sql_verbose: yes
sql_select: SELECT clear_password FROM mailbox WHERE email='%u@%r' AND disablesmtpauth=0
TIP : If you are using x86_64 platform ( eg Opteron ), you will need to put this file in a different place:
/usr/lib64/sasl2/postfix.conf

POSTFIX

http://www.postfix.org/

Remove sendmail ( which is installed by default on CentOS )

yum remove sendmail

Install the prerequisites

yum install pcre-devel

Add the required user accounts to run the Postfix MTA

groupadd -r postfix
useradd -r -g postfix -d /no/where -s /no/shell postfix
groupadd -r postdrop

Before we forget, tighten up the permissions on the SASL conf file, as it contains the database username/password

chown root.postfix /usr/lib/sasl2/postfix.conf
chmod 640 /usr/lib/sasl2/postfix.conf 

Add the account that will own all the virtual mail

groupadd -g 1001 vmail
useradd -u 1001 -s /sbin/nologin -g vmail vmail

Download and extract the Postfix sources

cd /usr/local/src
wget ftp://ftp.planetmirror.com/pub/postfix/official/postfix-2.4.1.tar.gz
tar xzf postfix-2.4.1.tar.gz
chown -R root.root postfix-2.4.1
cd postfix-2.4.1

Compile, enabling the optional support for MySQL, SASL (SMTP-AUTH), SSL (SMTPS and TLS)

make -f Makefile.init makefiles \
  'CCARGS=-DHAS_MYSQL -I/usr/include/mysql -DUSE_SASL_AUTH -DUSE_CYRUS_SASL -I/usr/include/sasl -DUSE_TLS -I/usr/include/openssl' \
  'AUXLIBS=-L/usr/lib/mysql -lmysqlclient -lz -lm -lsasl2 -lssl -lcrypto'
make
make install
# (press enter to all the "make install" questions)
TIP : If you are using x86_64 platform ( eg Opteron ), you will need to use a slightly modified make :
make -f Makefile.init makefiles \
  'CCARGS=-DHAS_MYSQL -I/usr/include/mysql -DUSE_SASL_AUTH -DUSE_CYRUS_SASL -I/usr/include/sasl -DUSE_TLS -I/usr/include/openssl' \
  'AUXLIBS=-L/usr/lib64/mysql -lmysqlclient -lz -lm -lsasl2 -lssl -lcrypto'

Some applications expect the sendmail binary to be available at /usr/lib/sendmail, but Postfix doesn't put it there, so lets make a symlink

ln -s /usr/sbin/sendmail /usr/lib/sendmail
vi /etc/postfix/master.cf
# Now enabled SMTP over SSL (smtps : port 465)
#
# To do this, we need to edit the maser.cf file.
# The two line are already there, but are commented out. 
# To enable the service, we simply need to remove the comment markers ("#").
#
# Note, there needs to be whitespace (either a space or tab) at the start of the 2nd line (in front of the -o)
smtps inet n - n - - smtpd
 -o smtpd_tls_wrappermode=yes
 -o smtpd_sasl_auth_enable=yes
vi /etc/postfix/main.cf
# make the following changes :
myhostname	= mail.yourdomain.com
mydomain	= yourdomain.com
mydestination	= $myhostname, localhost.$mydomain, localhost
local_recipient_maps = unix:passwd.byname $alias_maps
mynetworks	= $config_directory/mynetworks
relayhost	= [smarthost.yourdomain.com]  #<-- if you have a smarthost server
alias_maps	= hash:/etc/aliases
home_mailbox	= Maildir/
# Next, add all these to the bottom of the file :
disable_vrfy_command	= yes
smtpd_recipient_limit	= 250
biff			= no
# (note this setting below only affects LOCAL mail delivery agent, not virtual mailboxes)
mailbox_size_limit	= 20480000  
maximal_queue_lifetime	= 5d
message_size_limit	= 18000000
delay_warning_time	= 4h
default_process_limit	= 50
append_dot_mydomain	= no
parent_domain_matches_subdomains =
###################################################################################
### ENABLE SASL SUPPORT ( SMTP-AUTH )
# smtpd_sasl_auth_enable	= yes
#   Enable SASL support in postfix
# smtpd_sasl_security_options	= noanonymous
#   Anonymous logins will not be permitted
# broken_sasl_auth_clients	= yes
#   Allow RFC-broken mail clients like Outlook Express4 to use SMTP AUTH
# smtpd_sasl_path		= postfix
#   Tells SASL to get the config from /usr/lib/sasl2/postfix.conf
# smtpd_sasl_local_domain	=
#   If the user fails to nominate a domain, don't auto append one
# smtpd_sasl_authenticated_header = yes
#   Include the authenticated username in the message headers.
#   Having this on will make it easier if a spammer cracks one of your user's weak passwords,
#   and starts using SMTP-AUTH to relay spam through your server
smtpd_sasl_auth_enable		= yes
smtpd_sasl_security_options	= noanonymous
broken_sasl_auth_clients	= yes
smtpd_sasl_path			= postfix
smtpd_sasl_local_domain		=
smtpd_sasl_authenticated_header = yes
###################################################################################
### ENABLE TLS SUPPORT ( "STARTTLS" ... enables SSL to be negotiated during a SMTP connection )
# smtp_use_tls = no
#   dont enable TLS for outbound SMTP connections
# smtpd_use_tls = yes 
#   announce TLS availability for incoming SMTP connections
# smtpd_tls_auth_only = no :
#   TLS is optional, not enforced
# smtpd_tls_key_file :
#   specify the private key ( must not be encrypted - ie no password)
# smtpd_tls_cert_file :
#   specify the certificate
# smtpd_tls_session_cache_database : 
#   nominate a server-side TLS session cache. Improves performance.
# smtpd_tls_loglevel = 1 :
#   log basic TLS handshake and cert info 
# smtpd_tls_received_header = yes
#   record some protocol/cipher etc info in the Received header smtp_use_tls = no
smtp_use_tls                     = no
smtpd_use_tls                    = yes
smtpd_tls_auth_only              = no
smtpd_tls_key_file               = /usr/local/ssl/mail.yourdomain.com.key
smtpd_tls_cert_file              = /usr/local/ssl/mail.yourdomain.com.crt
smtpd_tls_session_cache_database = btree:/etc/postfix/tls_smtpd_scache
smtpd_tls_loglevel               = 1
smtpd_tls_received_header        = yes
###################################################################################
### add $smtpd_recipient_restrictions to the standard list
### so that we can "proxy:" the check_client|sender|recipient_access entries
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,
  $smtpd_recipient_restrictions
###################################################################################
### DEFINE OUR SMTPD RESTRICTIONS, RELAY CONTROL, RBL BLOCKING ETC
smtpd_helo_restrictions	=
smtpd_client_restrictions =
smtpd_sender_restrictions =
smtpd_recipient_restrictions = 
	check_client_access	proxy:mysql:/etc/postfix/mysql-client-access.cf,
	check_sender_access	proxy:mysql:/etc/postfix/mysql-sender-access.cf,
	check_recipient_access	proxy:mysql:/etc/postfix/mysql-recipient-access.cf,
	permit_sasl_authenticated,
	permit_mynetworks,
	reject_unauth_destination,
	reject_invalid_helo_hostname,
	reject_non_fqdn_sender,
	reject_non_fqdn_recipient,
	reject_unknown_sender_domain,
	reject_unknown_recipient_domain,
	reject_rbl_client list.dsbl.org,
	reject_rbl_client cbl.abuseat.org,
	reject_rbl_client dnsbl.njabl.org,
	permit
smtpd_data_restrictions = 
	reject_unauth_pipelining,
	permit
###################################################################################
### Virtual alias config
virtual_alias_domains 	= proxy:mysql:/etc/postfix/mysql-virtual-alias-domains.cf
virtual_alias_maps 	= proxy:mysql:/etc/postfix/mysql-virtual-alias-maps.cf,
                          proxy:mysql:/etc/postfix/mysql-virtual-mailbox-to-alias-maps.cf
###################################################################################
### Virtual mailbox config
# virtual_mailbox_domains : A list of all the virtual mailbox domains
# virtual_mailbox_base	: This value will be prepended to all the virtual_mailbox_maps
# virtual_mailbox_maps	: Virtual email addr to disk location mappings
# virtual_mailbox_limit	: Maximal size of an individual mailbox/Maildir file
virtual_mailbox_domains	= proxy:mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
virtual_mailbox_base	= /var/vmail
virtual_mailbox_maps	= proxy:mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
virtual_mailbox_limit	= 20480000
virtual_transport       = maildrop 
maildrop_destination_recipient_limit = 1

Create the config files that tell Postfix how to access the various MySQL tables.

Note "hosts = localhost" means Postfix will use sockets, "hosts = 127.0.0.1" means Postfix will use TCP. We have disabled TCP access in MySQL (with the "skip-networking" config option), so make sure you stick with using the word localhost  in these files. Also, sockets are faster than TCP.

echo "user = postfixuser" > /etc/postfix/mysql-virtual-mailbox-domains.cf 
echo "password = postfixpass" >> /etc/postfix/mysql-virtual-mailbox-domains.cf 
echo "hosts = localhost" >> /etc/postfix/mysql-virtual-mailbox-domains.cf 
echo "dbname = postfix" >> /etc/postfix/mysql-virtual-mailbox-domains.cf 
echo "query = SELECT 'ignored by postfix' FROM mailbox_domains WHERE domain='%s' AND active=1"  >> /etc/postfix/mysql-virtual-mailbox-domains.cf 
echo "user = postfixuser" > /etc/postfix/mysql-virtual-mailbox-maps.cf
echo "password = postfixpass" >> /etc/postfix/mysql-virtual-mailbox-maps.cf
echo "hosts = localhost" >> /etc/postfix/mysql-virtual-mailbox-maps.cf
echo "dbname = postfix" >> /etc/postfix/mysql-virtual-mailbox-maps.cf
echo "query = SELECT 'ignored by postfix' FROM mailbox WHERE email='%s' AND active=1" >> /etc/postfix/mysql-virtual-mailbox-maps.cf 
echo "user = postfixuser" > /etc/postfix/mysql-virtual-alias-domains.cf 
echo "password = postfixpass" >> /etc/postfix/mysql-virtual-alias-domains.cf 
echo "hosts = localhost" >> /etc/postfix/mysql-virtual-alias-domains.cf 
echo "dbname = postfix" >> /etc/postfix/mysql-virtual-alias-domains.cf 
echo "query = SELECT 'ignored by postfix' FROM alias_domains WHERE domain='%s' AND active=1" >> /etc/postfix/mysql-virtual-alias-domains.cf 
echo "user = postfixuser" > /etc/postfix/mysql-virtual-alias-maps.cf
echo "password = postfixpass" >> /etc/postfix/mysql-virtual-alias-maps.cf
echo "hosts = localhost" >> /etc/postfix/mysql-virtual-alias-maps.cf
echo "dbname = postfix" >> /etc/postfix/mysql-virtual-alias-maps.cf
echo "query = SELECT goto FROM alias WHERE address='%s' AND active=1" >> /etc/postfix/mysql-virtual-alias-maps.cf
echo "user = postfixuser" > /etc/postfix/mysql-client-access.cf
echo "password = postfixpass" >> /etc/postfix/mysql-client-access.cf
echo "hosts = localhost" >> /etc/postfix/mysql-client-access.cf
echo "dbname = postfix" >> /etc/postfix/mysql-client-access.cf
echo "query = SELECT response FROM client_access WHERE client = '%s' AND active=1" >> /etc/postfix/mysql-client-access.cf 
echo "user = postfixuser" > /etc/postfix/mysql-sender-access.cf
echo "password = postfixpass" >> /etc/postfix/mysql-sender-access.cf
echo "hosts = localhost" >> /etc/postfix/mysql-sender-access.cf
echo "dbname = postfix" >> /etc/postfix/mysql-sender-access.cf
echo "query = SELECT response FROM sender_access WHERE sender = '%s' AND active=1" >> /etc/postfix/mysql-sender-access.cf 
echo "user = postfixuser" > /etc/postfix/mysql-recipient-access.cf
echo "password = postfixpass" >> /etc/postfix/mysql-recipient-access.cf
echo "hosts = localhost" >> /etc/postfix/mysql-recipient-access.cf
echo "dbname = postfix" >> /etc/postfix/mysql-recipient-access.cf
echo "query = SELECT response FROM recipient_access WHERE recipient = '%s' AND active=1" >> /etc/postfix/mysql-recipient-access.cf 
#THIS IS A WORKAROUND TO ALLOW US TO USE CATCHALL ENTRIES IN THE VIRTUAL ALIAS MAPS.
#
# This issue is that Postfix will process all the alias entries before looking at the
# virtual mailbox maps. Thus if you add a catchall entry for a virtual mailbox domain,
# the catchall will grab all mail, and no mail will go to the virtual mailboxes.
#
# The only solution for virtual mailbox domains that have a catchall, is to populate
# all the mailbox addresses into the alias table as well. The mailbox entries need to
# be pointed back to themselves eg
#
# ADDRESS GOTO
# user1@domain -> user1@domain
# user2@domain -> user2@domain
# @domain -> somewhere
#
# However we don't want to pollute our alias table with all these workaround records,
# so we will define a lookup now that lets Postfix do the work for us. Then we modify
# main.cf so that virtual_alias_maps changes from this :
#
# virtual_alias_maps = proxy:mysql:/etc/postfix/mysql-virtual-alias-maps.cf
#
# to this :
#
# virtual_alias_maps = proxy:mysql:/etc/postfix/mysql-virtual-alias-maps.cf,
# proxy:mysql:/etc/postfix/mysql-virtual-mailbox-to-alias-maps.cf
#
# NOTE: I "stole" this workaround from
# http://workaround.org/articles/ispmail-sarge/#mysql-virtual_email2email.cf 
#
echo "user = postfixuser" >> /etc/postfix/mysql-virtual-mailbox-to-alias-maps.cf
echo "password = postfixpass" >> /etc/postfix/mysql-virtual-mailbox-to-alias-maps.cf
echo "hosts = localhost" >> /etc/postfix/mysql-virtual-mailbox-to-alias-maps.cf
echo "dbname = postfix" >> /etc/postfix/mysql-virtual-mailbox-to-alias-maps.cf
echo "query = SELECT email FROM mailbox WHERE email='%s' AND active=1" >> /etc/postfix/mysql-virtual-mailbox-to-alias-maps.cf
# these files contain our database username/password, so tighten the security a bit
chown root.postfix /etc/postfix/mysql-*.cf
chmod 640 /etc/postfix/mysql-*.cf 

Now we need to populate the mynetworks file. This file lists the IPs that are able to "relay" mail through your server. We put localhost into this file, so that scripts running on this server can relay mail to the internet. If you have workstations on a LAN, or other users on the internet with fixed-ip addresses, you can add them here as well, and these users will then be permitted to relay mail. For all other users who have mailboxes on your server, when sending mail they can either use SMTP-AUTH, or alternatively they could set their email client's SMTP server settings to point to their ISP's mail server.

echo '# Localhost' > /etc/postfix/mynetworks
echo '127.0.0.0/8' >>/etc/postfix/mynetworks
echo '' >>/etc/postfix/mynetworks
echo '# MyCompany blocks' >>/etc/postfix/mynetworks
echo 'xxx.xxx.xxx.xxx/24' >>/etc/postfix/mynetworks
echo 'yyy.yyy.yyy.yyy/24' >>/etc/postfix/mynetworks

Double check that syslog is configured not to fsync after every output to maillog ( as this would bog server down badly )

vi /etc/syslog.conf
-/var/log/maillog

Create a init script for postfix

echo '#!/bin/bash' > /etc/rc.d/init.d/postfix
echo '#' >> /etc/rc.d/init.d/postfix
echo '# postfix      Postfix Mail Transfer Agent' >> /etc/rc.d/init.d/postfix
echo '#' >> /etc/rc.d/init.d/postfix
echo '# chkconfig: 2345 80 30' >> /etc/rc.d/init.d/postfix
echo '# description: Postfix is a Mail Transport Agent, which is the program \' >> /etc/rc.d/init.d/postfix
echo '#              that moves mail from one machine to another.' >> /etc/rc.d/init.d/postfix
echo '# processname: master' >> /etc/rc.d/init.d/postfix
echo '# pidfile: /var/spool/postfix/pid/master.pid' >> /etc/rc.d/init.d/postfix
echo '# config: /etc/postfix/main.cf' >> /etc/rc.d/init.d/postfix
echo '# config: /etc/postfix/master.cf' >> /etc/rc.d/init.d/postfix
echo '#' >> /etc/rc.d/init.d/postfix
echo '# Source function library.' >> /etc/rc.d/init.d/postfix
echo '. /etc/rc.d/init.d/functions' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo '# Source networking configuration.' >> /etc/rc.d/init.d/postfix
echo '. /etc/sysconfig/network' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo '# Check that networking is up.' >> /etc/rc.d/init.d/postfix
echo '[ ${NETWORKING} = "no" ] && exit 0' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo '[ -x /usr/sbin/postfix ] || exit 0' >> /etc/rc.d/init.d/postfix
echo '[ -d /etc/postfix ] || exit 0' >> /etc/rc.d/init.d/postfix
echo '[ -d /var/spool/postfix ] || exit 0' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo 'RETVAL=0' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo 'start() {' >> /etc/rc.d/init.d/postfix
echo '        # Start daemons.' >> /etc/rc.d/init.d/postfix
echo '        echo -n $"Starting postfix: "' >> /etc/rc.d/init.d/postfix
echo '        alias_database=$(postconf -h alias_database 2>/dev/null)' >> /etc/rc.d/init.d/postfix
echo '        RETVAL=$?' >> /etc/rc.d/init.d/postfix
echo '        if [ $RETVAL -ne 0 ]; then' >> /etc/rc.d/init.d/postfix
echo '            failure $"determination of alias_database"' >> /etc/rc.d/init.d/postfix
echo '            echo' >> /etc/rc.d/init.d/postfix
echo '            return 0' >> /etc/rc.d/init.d/postfix
echo '        fi' >> /etc/rc.d/init.d/postfix
echo '        if [ -n "$alias_database" ]; then' >> /etc/rc.d/init.d/postfix
echo '            /usr/sbin/postalias ${alias_database//,} 2>/dev/null' >> /etc/rc.d/init.d/postfix
echo '            RETVAL=$?' >> /etc/rc.d/init.d/postfix
echo '            if [ $RETVAL -ne 0 ]; then' >> /etc/rc.d/init.d/postfix
echo '                failure $"postalias $alias_database"' >> /etc/rc.d/init.d/postfix
echo '                echo' >> /etc/rc.d/init.d/postfix
echo '                return 0' >> /etc/rc.d/init.d/postfix
echo '            fi' >> /etc/rc.d/init.d/postfix
echo '        fi' >> /etc/rc.d/init.d/postfix
echo '        /usr/sbin/postfix start 2>/dev/null 1>&2 && success || failure $"postfix start"'	>> /etc/rc.d/init.d/postfix
echo '        RETVAL=$?' >> /etc/rc.d/init.d/postfix
echo '        [ $RETVAL -eq 0 ] && touch /var/lock/subsys/postfix' >> /etc/rc.d/init.d/postfix
echo '        echo' >> /etc/rc.d/init.d/postfix
echo '        return $RETVAL' >> /etc/rc.d/init.d/postfix
echo '}' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo 'stop() {' >> /etc/rc.d/init.d/postfix
echo '        # Stop daemons.' >> /etc/rc.d/init.d/postfix
echo '        echo -n $"Shutting down postfix: "' >> /etc/rc.d/init.d/postfix
echo '        /usr/sbin/postfix stop 2>/dev/null 1>&2 && success || failure $"postfix stop"' >> /etc/rc.d/init.d/postfix
echo '        RETVAL=$?' >> /etc/rc.d/init.d/postfix
echo '        [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/postfix' >> /etc/rc.d/init.d/postfix
echo '        echo' >> /etc/rc.d/init.d/postfix
echo '        return $RETVAL' >> /etc/rc.d/init.d/postfix
echo '}' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo 'reload() {' >> /etc/rc.d/init.d/postfix
echo '        echo -n $"Reloading postfix: "' >> /etc/rc.d/init.d/postfix
echo '        /usr/sbin/postfix reload 2>/dev/null 1>&2 && success || failure $"postfix reload"' >> /etc/rc.d/init.d/postfix
echo '        RETVAL=$?' >> /etc/rc.d/init.d/postfix
echo '        echo' >> /etc/rc.d/init.d/postfix
echo '        return $RETVAL' >> /etc/rc.d/init.d/postfix
echo '}' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo 'abort() {' >> /etc/rc.d/init.d/postfix
echo '        /usr/sbin/postfix abort 2>/dev/null 1>&2 && success || failure $"postfix abort"' >> /etc/rc.d/init.d/postfix
echo '        return $?' >> /etc/rc.d/init.d/postfix
echo '}' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo 'flush() {' >> /etc/rc.d/init.d/postfix
echo '        /usr/sbin/postfix flush 2>/dev/null 1>&2 && success || failure $"postfix flush"' >> /etc/rc.d/init.d/postfix
echo '        return $?' >> /etc/rc.d/init.d/postfix
echo '}' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo 'check() {' >> /etc/rc.d/init.d/postfix
echo '        /usr/sbin/postfix check 2>/dev/null 1>&2 && success || failure $"postfix check"' >> /etc/rc.d/init.d/postfix
echo '        return $?' >> /etc/rc.d/init.d/postfix
echo '}' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo 'restart() {' >> /etc/rc.d/init.d/postfix
echo '        stop' >> /etc/rc.d/init.d/postfix
echo '        start' >> /etc/rc.d/init.d/postfix
echo '}' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo '# See how we were called.' >> /etc/rc.d/init.d/postfix
echo 'case "$1" in' >> /etc/rc.d/init.d/postfix
echo '  start)' >> /etc/rc.d/init.d/postfix
echo '        start' >> /etc/rc.d/init.d/postfix
echo '        ;;' >> /etc/rc.d/init.d/postfix
echo '  stop)' >> /etc/rc.d/init.d/postfix
echo '        stop' >> /etc/rc.d/init.d/postfix
echo '        ;;' >> /etc/rc.d/init.d/postfix
echo '  restart)' >> /etc/rc.d/init.d/postfix
echo '        stop' >> /etc/rc.d/init.d/postfix
echo '        start' >> /etc/rc.d/init.d/postfix
echo '        ;;' >> /etc/rc.d/init.d/postfix
echo '  reload)' >> /etc/rc.d/init.d/postfix
echo '        reload' >> /etc/rc.d/init.d/postfix
echo '        ;;' >> /etc/rc.d/init.d/postfix
echo '  abort)' >> /etc/rc.d/init.d/postfix
echo '        abort' >> /etc/rc.d/init.d/postfix
echo '        ;;' >> /etc/rc.d/init.d/postfix
echo '  flush)' >> /etc/rc.d/init.d/postfix
echo '        flush' >> /etc/rc.d/init.d/postfix
echo '        ;;' >> /etc/rc.d/init.d/postfix
echo '  check)' >> /etc/rc.d/init.d/postfix
echo '        check' >> /etc/rc.d/init.d/postfix
echo '        ;;' >> /etc/rc.d/init.d/postfix
echo '  status)' >> /etc/rc.d/init.d/postfix
echo '        status master' >> /etc/rc.d/init.d/postfix
echo '        ;;' >> /etc/rc.d/init.d/postfix
echo '  condrestart)' >> /etc/rc.d/init.d/postfix
echo '        [ -f /var/lock/subsys/postfix ] && restart || :' >> /etc/rc.d/init.d/postfix
echo '        ;;' >> /etc/rc.d/init.d/postfix
echo '  *)' >> /etc/rc.d/init.d/postfix
echo '        echo $"Usage: postfix {start|stop|restart|reload|abort|flush|check|status|condrestart}"' >> /etc/rc.d/init.d/postfix
echo '        exit 1' >> /etc/rc.d/init.d/postfix
echo 'esac' >> /etc/rc.d/init.d/postfix
echo ' ' >> /etc/rc.d/init.d/postfix
echo 'exit $?' >> /etc/rc.d/init.d/postfix
chmod 744 /etc/rc.d/init.d/postfix
chkconfig --add postfix

Create the vmail tree

mkdir /var/vmail
chown vmail.vmail /var/vmail

Tweak the aliases file. These mappings are used for system related mails eg crontab messages, postfix bounces etc. <username@mail.yourdomain.com>

vi /etc/aliases
root:    someone@yourdomain.com
newaliases

Try starting Postfix

/etc/rc.d/init.d/postfix start

If all goes well, you should be able to run "ps axf" see something like :

7184 ? Ss 0:00 /usr/libexec/postfix/master
7185 ? S 0:00   \_ pickup -l -t fifo -u
7186 ? S 0:00   \_ qmgr -l -t fifo -u

Also, you should take a look in the maillog file to see if any errors are being reported there

tail -f /var/log/maillog

Disable logwatch script from reporting on Postfix logs

The logwatch script runs nightly, and emails a report to the root user Unless your server is very low volume, I would recommend you tell logwatch not to report postfix stats, otherwise the report gets much too big

vi /etc/log.d/conf/logwatch.conf
# Look for where it says "Service = All" and underneath that add this line :
Service = -postfix

INSTALL A 2ND ETHERNET IP

We want to have a 2nd IP address so that we can keep our MX-traffic separate from our customer-SMTP traffic

Customers will use the "mail.yourdomain.com" (192.168.1.10) name in their mail client's SMTP server settings, but in our zone files we will point the primary MX records to a different hostname such as "mail-mx.yourdomain.com" (192.168.1.11)

Having two interfaces allows us to set the Postfix MX concurrency to something sensibly low (to prevent a spam influx from overloading our server), while at the same time we can retain a high concurrency on the customer-SMTP settings so that our users don't ever see a connection-refused error

Let's assume our network interface is eth0. Then there is a file /etc/sysconfig/network-scripts/ifcfg-eth0 which looks like this:

DEVICE=eth0
BOOTPROTO=static
BROADCAST=192.168.1.255
IPADDR=192.168.1.10
NETMASK=255.255.255.0
NETWORK=192.168.1.0
ONBOOT=yes
TYPE=Ethernet

Now we want to create the virtual interface eth0:0 with the IP address 192.168.0.101. All we have to do is to create the file /etc/sysconfig/network-scripts/ifcfg-eth0:0 which looks like this:

DEVICE=eth0:0
BOOTPROTO=static
BROADCAST=192.168.1.255
IPADDR=192.168.1.11
NETMASK=255.255.255.0
NETWORK=192.168.1.0
ONBOOT=yes
TYPE=Ethernet

Once that has been done, you have to restart the network for the changes to take effect

/etc/rc.d/init.d/network restart

If you then run ifconfig, you should see something like this :

eth0 Link encap:Ethernet HWaddr 00:07:E9:4B:AD:83 
inet addr:192.168.1.10 Bcast:192.168.1.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:28442 errors:0 dropped:0 overruns:0 frame:0
TX packets:28385 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000 
RX bytes:14081755 (13.4 MiB) TX bytes:4847753 (4.6 MiB)

eth0:0 Link encap:Ethernet HWaddr 00:07:E9:4B:AD:83 
inet addr:192.168.1.11 Bcast:192.168.1.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1

lo Link encap:Local Loopback 
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:16436 Metric:1
RX packets:30 errors:0 dropped:0 overruns:0 frame:0
TX packets:30 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0 
RX bytes:2190 (2.1 KiB) TX bytes:2190 (2.1 KiB)

Tell Postfix which IP does what :

vi /etc/postfix/master.cf

# Change this :

smtp inet n - n - - smtpd

# to this :

127.0.0.1:smtp    inet n - n - 10 smtpd
192.168.1.10:smtp inet n - n - 100 smtpd
192.168.1.11:smtp inet n - n - 50  smtpd-mx
   -o smtpd_sasl_auth_enable=no
cd /usr/libexec/postfix
ln -s smtpd smtpd-mx

Notes :

Our MX concurrency has been set to a lower value (50) than our customer-SMTP concurrency (100). You can tweak the these values up/down (particularly the MX concurrency) to suit your server's power / load. For a small server I would recommend the SMTP-MX concurrency be set to something like 25. For a medium server you can set it to something like 50, for a large server you will need to use 100+.

We have created a new application-name called smtpd-mx. This is just a cosmetic tweak, so that when you are running commands like pstree, you can easily see exactly how many sessions of MX and customer-SMTP you have running :-)

For the smtpd-mx service, we have used a configuration option to disable SASL ( SMTP-AUTH ). In most scenarios, there is no need to have SMTP-AUTH enabled for MX traffic - disabling it will save some resources.

Also note that you might like to add additional IPs to the server as well (eth0:1 eth0:2 etc), one for each virtual domain you host. As this opens up options for other config tweaks which will be discussed later in this document. Eg setting a default pop3/imap smtp-auth ftp sqwebmail domain per hostname, and also allowing Postfix to attach one SSL cert per hostname.

Add entries to your reverse DNS

For each IP address, you need to make sure you have added a reverse DNS entry. If you fail to do this you might find your server is blocked by some mail servers. eg :

$ORIGIN 1.168.192.in-addr.arpa.
10	PTR	mail.yourdomain.com.
11	PTR	mail-mx.yourdomain.com.

SQLGREY GREYLISTING

http://sqlgrey.sourceforge.net/

Greylisting provides excellent protection against spammers, at the expense of slightly delaying some mail deliveries

Install the prerequisite Perl modules

perl -MCPAN -e shell 
# press enter through all the questions until you come to the region selections
# At that point you will need to choose your preferred mirrors
o conf prerequisites_policy follow
install MD5 LWP Net::Server IO::Multiplex DBD::mysql
quit

Create the user account

groupadd sqlgrey
useradd -r -m -g sqlgrey sqlgrey

Download and unpack the sources

cd /usr/local/src
wget http://optusnet.dl.sourceforge.net/sourceforge/sqlgrey/sqlgrey-1.6.7.tar.bz2
tar xjf sqlgrey-1.6.7.tar.bz2
chown -R root.root sqlgrey-1.6.7
cd sqlgrey-1.6.7

Create the MySQL database

mysql
CREATE DATABASE sqlgrey;
GRANT ALL ON sqlgrey.* to sqlgrey@localhost;
quit

( SQLgrey program will auto create its tables for us on startup )

Install the binaries and config files

make install
# installed to /usr/sbin by default, I prefer to have them in /usr/local/sbin
mv /usr/sbin/sqlgrey /usr/local/sbin
mv /usr/sbin/update_sqlgrey_config /usr/local/sbin
mv /usr/bin/sqlgrey-logstats.pl /usr/local/sbin
# create the files that can be used for storing local whitelist entries
touch /etc/sqlgrey/clients_ip_whitelist.local
touch /etc/sqlgrey/clients_fqdn_whitelist.local

Make the following changes to the config file

vi /etc/sqlgrey/sqlgrey.conf
reconnect_delay = 1
awl_age = 32
group_domain_level = 10
db_type = mysql
optmethod = optout
admin_mail = someadmin@yourdomain.com

TIP : If your are building a big/busy server you might like to reduce the amount of logging generated by sqlgrey by adjusting these settings :

loglevel = 1
log_override = conf:2

Install the init script

cp init/sqlgrey /etc/rc.d/init.d/sqlgrey
vi /etc/rc.d/init.d/sqlgrey
#go to the start section, and change the sqlgrey -d command to include the full path
/usr/local/sbin/sqlgrey -d
#go to the stop section, and change the sqlgrey -k command to include the full path
/usr/local/sbin/sqlgrey -k
chkconfig --add sqlgrey
chkconfig sqlgrey on

Start the program

/etc/rc.d/init.d/sqlgrey start

If all goes well, ps axf should should show you something like this :

1898 ? Ss 0:00 /usr/bin/perl -w /usr/local/sbin/sqlgrey -d

Tweak the Postfix config, so incoming external mail gets routed via the greylisting daemon. ( You need to add the line shown in bold text )

vi /etc/postfix/main.cf
reject_rbl_client list.dsbl.org,
reject_rbl_client sbl-xbl.spamhaus.org,
check_policy_service inet:127.0.0.1:2501,
permit
postfix reload

Setup auto-downloading of rules. These will be stored into the clients_ip_whitelist and clients_fqdn_whitelist files.

crontab -e
# download sqlgrey rules
0 0 * * * /usr/local/sbin/update_sqlgrey_config

For your local users who want to opt-out of the greylisting, you simply add their email address to the optout_email table :

mysql
USE sqlgrey;
-- this user wants to opt out
INSERT INTO optout_email (email) VALUES ('someuser@somedomain.com');
-- all users on this domain want to opt out
INSERT INTO optout_domain (domain) VALUES ('somedomainother.com');
quit

To reduce the load on your database server, you can put the most common hosts into the clients_ip_whitelist.local files. You can use sqlgrey-logstats.pl tool to extract this info from your logs.

If you have got secondary MX servers setup for your domain, you need to install SQLgrey on the secondary MX machines as well. Don't forget to put the secondary MX server IP's into the primary MX's clients_ip_whitelist.local file.

eg :

echo "*.yourdomain.com" > /etc/sqlgrey/clients_fqdn_whitelist.local
echo "192.168"        > /etc/sqlgrey/clients_ip_whitelist.local

COURIER-AUTHLIB

Courier-authlib provides user authentication services to Courier-IMAP, Courier-POP3d, and SqWebMail

Install the prerequisites

yum install expect
# This next one isnt used by authlib, but are used by several of the other courier tools 
# like maildrop and Courier-IMAP
yum install gamin-devel

Download and unpack the sources

cd /usr/local/src
wget http://optusnet.dl.sourceforge.net/sourceforge/courier/courier-authlib-0.59.3.tar.bz2
tar xjf courier-authlib-0.59.3.tar.bz2
chown -R root.root courier-authlib-0.59.3
cd courier-authlib-0.59.3

Configure, compile, install

./configure \
  --prefix=/usr/local/courier-authlib \
  --without-ipv6 \
  --disable-root-check \
  --without-authpwd \
  --without-authshadow \
  --without-authuserdb \
  --without-authpgsql \
  --without-authldap \
  --without-authvchkpw \
  --without-authcustom \
  --without-authpam \
  --without-authpipe \
  --with-authmysql \
  --with-authdaemon \
  --with-redhat
make
make check
make install
make install-configure

Tweak the config file so that courier-authlib can access the info from our database

vi /usr/local/courier-authlib/etc/authlib/authmysqlrc
MYSQL_SERVER localhost
MYSQL_USERNAME postfixuser
MYSQL_PASSWORD postfixpass
MYSQL_SOCKET /var/lib/mysql/mysql.sock
MYSQL_PORT 0
MYSQL_OPT 0
MYSQL_DATABASE postfix
MYSQL_USER_TABLE mailbox
MYSQL_CRYPT_PWFIELD password
MYSQL_CLEAR_PWFIELD clear_password
#you can optionally enable this next setting if you want a particular domain to be appended
#when users haven't specified a domain during authentication
#DEFAULT_DOMAIN yourdomain.com  
MYSQL_UID_FIELD '1001'
MYSQL_GID_FIELD '1001'
MYSQL_LOGIN_FIELD email
MYSQL_HOME_FIELD '/var/vmail'
MYSQL_NAME_FIELD name
MYSQL_MAILDIR_FIELD CONCAT(maildir,"Maildir/")
MYSQL_QUOTA_FIELD CONCAT(mailquota*1024*1024,"S")
MYSQL_AUXOPTIONS_FIELD CONCAT("disableimap=",disableimap,",disablepop3=",disablepop3,",disablewebmail=",disablewebmail)
MYSQL_WHERE_CLAUSE active='1'

Tweak the config to disable some unneeded features

vi /usr/local/courier-authlib/etc/authlib/authdaemonrc
#if your server is going to be very busy, you might need to increase this one
daemons=5
# Disable some unneeded functionality.
# (Note that these could optionally be re-enabled per-user by adding appropriate columns to the mailbox database)
#
#   wbnochangepass : In our case the postfix MySQL database will be maintained by our billing system, so although 
#                    sqwebmail has functionality to allow users to can change their passwords, these changes would 
#                    be overwritten by the billing system... So we will turn that option off
#   wbusexsender   : Include an X-Sender header to all outgoing mail ( allows you to track actual sender, even if
#                    user has altered their From address in sqwebmail )
#   disableshared  : We don't want shared folders, as this mail server is going to be used in ISP rather than corporate scenario
#   webnodsn       : Hide the option "

DEFAULTOPTIONS="wbnochangepass=1,wbusexsender=1,disableshared=1,wbnodsn=1"

Install the init script

cp courier-authlib.sysvinit /etc/rc.d/init.d/courier-authlib
chmod 744 /etc/rc.d/init.d/courier-authlib
chkconfig --add courier-authlib

Start the daemon

/etc/rc.d/init.d/courier-authlib start

ps axf should give something like this :

1515 ? S 0:00 /usr/local/courier-authlib/sbin/courierlogger -pid=/usr/local/courier-authlib/var/spool/authdaemon/pid -s
1516 ? S 0:00  \_ /usr/local/courier-authlib/libexec/courier-authlib/authdaemond
1517 ? S 0:00      \_ /usr/local/courier-authlib/libexec/courier-authlib/authdaemond
1518 ? S 0:00      \_ /usr/local/courier-authlib/libexec/courier-authlib/authdaemond
1519 ? S 0:00      \_ /usr/local/courier-authlib/libexec/courier-authlib/authdaemond
1520 ? S 0:00      \_ /usr/local/courier-authlib/libexec/courier-authlib/authdaemond
1521 ? S 0:00      \_ /usr/local/courier-authlib/libexec/courier-authlib/authdaemond

TROUBLESHOOTING TIP :

Courier-authlib includes a couple of debugging tools. These can be handy if you are having problems eg auth'ing via POP3, but aren't sure if its your POP3 config that's broken or whether its actually the courier-authlib that's not working properly.

# display all accounts
/usr/local/courier-authlib/sbin/authenumerate
# perform a test authentication, and show all values returned from courier-authlib
/usr/local/courier-authlib/sbin/authtest someuser@yourdomain.com somepassword

MAILDROP

Maildrop provides Postfix with a Maildir++ softquota-compatible way to deliver mail into user's mailboxes.

Note : Instead of using maildrop, many people use the "Postfix VDA" patch instead. This patch hacks the Postfix virtual delivery agent to (supposedly) support Maildir++ softquotas. However I would strongly recommend you don't use that patch! The doco etc for the patch makes it sounds like it does everything you need. However when you actually inspect the code it is a total debacle zone. There are numerous logic errors - the patch fails to follow the Maildir++ specs, and will cause a ridiculous amount of needless load on your server. Maildrop does everything correctly, doesn't require the Postfix source code to be patched ( which is good for Postfix's security/reliability ), and gives additional features like quota warnings. Maildrop also has the huge bonus of being from the same author as Courier-imap/pop3d/sqwebmail so you are guaranteed excellent interoperability between all your tools that touch the Maildir

Download and unpack the sources

cd /usr/local/src
wget http://optusnet.dl.sourceforge.net/sourceforge/courier/maildrop-2.0.4.tar.bz2
tar xjf maildrop-2.0.4.tar.bz2
chown -R root.root maildrop-2.0.4
cd maildrop-2.0.4

Configure, compile, install

COURIERAUTHCONFIG=/usr/local/courier-authlib/bin/courierauthconfig \
CPPFLAGS=-I/usr/local/courier-authlib/include \
./configure \
  --enable-smallmsg=262144 \
  --enable-maildirquota
make
make install-strip
make install-man

TIP: if you read the Maildrop docs, there is a configure option "--without-db" which sounds quite desirable. However its not a good idea to use that option because it will prevent some of the Maildrop autoresponder functionality from working properly.

Configure maildrop binary to have siud root. It needs root permissions to be able to connect to the Courier-authlib socket. It drops root permissions as soon as it determines the final account user and group id. ( You cant just go and grant world permissions on the socket, as this would allow anyone on the system to obtain any account's password ).

chmod u+s /usr/local/bin/maildrop

Setup automatic quota warnings

When you use the -w option with maildrop, it enables the sending of quota warning messages. The warning message is copied verbatim from /usr/local/etc/quotawarnmsg with the addition of the "Date:" and "Message-Id:" headers. The warning is repeated every 24 hours (at least), until the Maildir drops below X percent full.

cp maildir/quotawarnmsg /usr/local/etc
vi /usr/local/etc/quotawarnmsg
# tweak wording to suit your needs
vi /etc/postfix/master.cf
# Maildrop will use courier-authlib to lookup account data for the user specified in the -d option.
#
# The example here tells Postfix to launch a maximum of 10 simultaneous mailbox processes.
# You might need to tweak this depending on how busy your server is. A higher number will 
# tell Postfix to try to deliver more messages per second, but setting the value too high can
# cause problems if you run out of disk I/O. I would recommend setting this concurrency value
# to 5 for a small server, 10 for medium, and 20 for large.
#
maildrop unix - n n - 10 pipe
  flags=DRhu user=vmail argv=/usr/local/bin/maildrop -w 80 -d ${recipient} 
postfix reload

Optionally can configure some global maildrop rules

vi /etc/maildroprc
# You can uncomment this next line if you want some debugging output.
# Useful if you are doing some tweaking to try and get mail deliveries to follow
# some more sophisticated rulesets.
#
#logfile "/var/log/maildroprc.log"
# Per-user .mailfilter files are installed (eg by sqwebmail) into the user's home dir
# at /var/vmail/yourdomain.com/u/user1/.mailfilter
#
# When maildrop runs, it is coded to look for any global rules in : /etc/maildroprc
# and then next will look for any per-user rules in : $HOME/.mailfilter
#
# However, with a vmail style config we have a problem...  $HOME doesn't point to a 
# per-user location. On a machine with all the accounts in /etc/passwd, $HOME would 
# point to the right place, but vmail systems are different because they host all the 
# mailboxes  under a single UID.
#
# When postfix calls maildrop to perform a message delivery,
# $HOME will contain the vmail home : /var/vmail
# $DEFAULT will contain the path to the maildir eg : yourdomain.com/u/user1/Maildir
#
# So to allow us to still run per-user rules, we will add an INCLUDE command in 
# the global file so that we can still pick up any per-user filtering rules.
#
# if the user has a .mailfilter file, then execute it's contents now
exception {
    include "$HOME/$DEFAULT/../.mailfilter"
}

TIP : If you need to do some debugging of maildrop you can run it from the command prompt with verbose logging enabled. This will identify if its talking to the database successfully, and will also show up if there are permissions problems etc on the destination dir  :

[root@mail01 ~]# maildrop -V9 -d someone@yourdomain.com
maildrop: authlib: groupid=1001
maildrop: authlib: userid=1001
maildrop: authlib: logname=someone@yourdomain.com, home=/var/vmail, mail=yourdomain.com/s/someone/Maildir/
maildrop: Changing to /var/vmail
<press CTRL-D here>

COURIER-IMAP / COURIER-POP3D

http://www.courier-mta.org/imap/

Download and unpack the sources

cd /usr/local/src
wget http://optusnet.dl.sourceforge.net/sourceforge/courier/courier-imap-4.1.3.tar.bz2
tar xjf courier-imap-4.1.3.tar.bz2
chown -R root.root courier-imap-4.1.3
cd courier-imap-4.1.3

Configure, compile, install the program

COURIERAUTHCONFIG=/usr/local/courier-authlib/bin/courierauthconfig \
CPPFLAGS=-I/usr/local/courier-authlib/include \
./configure \
  --without-ipv6 \
  --prefix=/usr/local/courier-imap \
  --disable-root-check \
  --with-redhat
make
make install
make install-configure

Appropriately tweak the config files

vi /usr/local/courier-imap/etc/imapd
# If you are going to run a busy IMAP-based webmail package, you will need to substantially increase this.
# The default value of 4 is insufficient even for servicing individual users, since clients like Thunderbird default to using up to 5 simultaneous connections 
# 
MAXPERIP=20
# Add our collection of supported auth methods to the advertised capability string
IMAP_CAPABILITY="IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA AUTH=CRAM-MD5 AUTH=CRAM-SHA1 AUTH=PLAIN AUTH=LOGIN IDLE"
# we want to turn off the announcement of IMAP ACL extensions, as we dont need this ( we arent using shared folders ),
# and the ACL stuff makes Thunderbird spit errors in some cases
IMAP_ACL=0
IMAP_CAPABILITY_TLS="$IMAP_CAPABILITY"
# Enabled the enhanced IDLE functionality
# This allows the IMAP server to notify your client when something has changed (eg a new message has arrived)
IMAP_ENHANCEDIDLE=1
# If you were going to have mainly Outlook Express based IMAP users, you can tell Courier-IMAP to name the trash folder "Deleted Items"
# However in our case we are expecting most IMAP users to be webmail, so sticking with the default "Trash" foldername is probably best.
#IMAP_TRASHFOLDERNAME="Deleted Items"
#IMAP_EMPTYTRASH="Deleted Items":7
# Enable the Courier-IMAP daemon
IMAPDSTART=YES
vi /usr/local/courier-imap/etc/imapd-ssl
# enable courier-imaps (port 993) daemon
IMAPDSSLSTART=YES
# enable STARTTLS extensions for IMAP. Enabling this means "STARTTLS" will be added to the IMAP CAPABILITY line
IMAPDSTARTTLS=YES
# nominate where the SSL key/certificate can be found
TLS_CERTFILE=/usr/local/ssl/mail.yourdomain.com.pem
vi /usr/local/courier-imap/etc/pop3d
# you would likely have to increase this for a busy server
MAXDAEMONS=40
# Add out collection of supported auth methods to the advertised capability string
POP3AUTH="CRAM-MD5 CRAM-SHA1 PLAIN LOGIN"
POP3AUTH_TLS="$POP3AUTH"
# enabled the courier-pop3 daemon
POP3DSTART=YES
vi /usr/local/courier-imap/etc/pop3d-ssl
# enable the courier-pop3s (port 995) daemon 
POP3DSSLSTART=YES
# enable STARTTLS extensions for POP3.
POP3_STARTTLS=YES
# nominate where the SSL key/certificate can be found
TLS_CERTFILE=/usr/local/ssl/mail.yourdomain.com.pem

Setup the init script

cp courier-imap.sysvinit /etc/rc.d/init.d/courier-imap
chmod 744 /etc/rc.d/init.d/courier-imap
chkconfig --add courier-imap

Create an empty shared folder index file, to prevent Courier-IMAP from complaining to syslog about it being missing. We aren't using shared folders at all on this server.

touch /usr/local/courier-imap/etc/shared/index

Start the daemons

/etc/rc.d/init.d/courier-imap start

ps axf should give something like this

14611 ? S 0:00 /usr/local/courier-authlib/sbin/courierlogger -pid=/var/run/imapd.pid -start -name=imapd /usr/local/couri
14612 ? S 0:00  \_ /usr/local/courier-imap/libexec/couriertcpd -address=0 -maxprocs=40 -maxperip=4 -nodnslookup -noident
14618 ? S 0:00 /usr/local/courier-authlib/sbin/courierlogger -pid=/var/run/imapd-ssl.pid -start -name=imapd-ssl /usr/loc
14619 ? S 0:00  \_ /usr/local/courier-imap/libexec/couriertcpd -address=0 -maxprocs=40 -maxperip=4 -nodnslookup -noident
14624 ? S 0:00 /usr/local/courier-authlib/sbin/courierlogger -pid=/var/run/pop3d.pid -start -name=pop3d /usr/local/couri
14625 ? S 0:00  \_ /usr/local/courier-imap/libexec/couriertcpd -address=0 -maxprocs=40 -maxperip=4 -nodnslookup -noident
14630 ? S 0:00 /usr/local/courier-authlib/sbin/courierlogger -pid=/var/run/pop3d-ssl.pid -start -name=pop3d-ssl /usr/loc
14631 ? S 0:00  \_ /usr/local/courier-imap/libexec/couriertcpd -address=0 -maxprocs=40 -maxperip=4 -nodnslookup -noident[

Disable logwatch script from reporting on imap logs

The logwatch script runs nightly, and emails a report to the root user. Unless your server is very low volume, I would recommend you tell logwatch not to report imap stats, otherwise the report gets much too big

vi /etc/log.d/conf/logwatch.conf
# Look for where it says "Service = All" and underneath that add this line :
Service = -imapd

APACHE

Create some directory structure

mkdir -p /var/www/mail/html
mkdir /var/www/mail/cgi-bin
mkdir /var/www/mail/logs

Remark out the following modules, which are part of the standard Apache installation, but we wont need. This will reduce the bulk of Apache, and in my experience will make it more stable

vi /etc/httpd/conf/httpd.conf
#LoadModule auth_anon_module modules/mod_auth_anon.so
#LoadModule auth_dbm_module modules/mod_auth_dbm.so
#LoadModule auth_digest_module modules/mod_auth_digest.so
#LoadModule ldap_module modules/mod_ldap.so
#LoadModule auth_ldap_module modules/mod_auth_ldap.so
#LoadModule cern_meta_module modules/mod_cern_meta.so
#LoadModule headers_module modules/mod_headers.so
#LoadModule usertrack_module modules/mod_usertrack.so
#LoadModule dav_module modules/mod_dav.so
#LoadModule status_module modules/mod_status.so
#LoadModule asis_module modules/mod_asis.so
#LoadModule info_module modules/mod_info.so
#LoadModule dav_fs_module modules/mod_dav_fs.so
#LoadModule speling_module modules/mod_speling.so
#LoadModule proxy_module modules/mod_proxy.so
#LoadModule proxy_ftp_module modules/mod_proxy_ftp.so
#LoadModule proxy_http_module modules/mod_proxy_http.so
#LoadModule proxy_connect_module modules/mod_proxy_connect.so
#LoadModule cache_module modules/mod_cache.so
#LoadModule disk_cache_module modules/mod_disk_cache.so
#LoadModule file_cache_module modules/mod_file_cache.so
#LoadModule mem_cache_module modules/mod_mem_cache.so

Add these commands to the bottom of the file so that Apache can serve sqwebmail pages

vi /etc/httpd/conf/httpd.conf
LogFormat "%v %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined_vhost
NameVirtualHost *:80
<VirtualHost *:80>
    # The first vhost entry is used if no hostname matches are found amongst other vhost entries.
    # So lets pop a "default" entry here to send any such requests through to our corporate webpage
    ServerAdmin webmaster@yourdomain.com
    DocumentRoot /var/www/mail/html
    ServerName placeholder-for-no-vhost-match
    ErrorLog /var/www/mail/logs/error_log
    CustomLog /var/www/mail/logs/access_log combined_vhost
    RewriteEngine On
    RewriteRule .* http://www.yourdomain.com/ [R,L]
</VirtualHost>
<VirtualHost *:80>
    ServerAdmin webmaster@yourdomain.com
    DocumentRoot /var/www/mail/html
    ServerName mail.yourdomain.com
    ServerAlias webmail.yourdomain.com
    ServerAlias mail.hosteddomain1.com.au webmail.hosteddomain1.com
    ServerAlias mail.hosteddomain2.com.au webmail.hosteddomain2.com
    ServerAlias mail.hosteddomain3.com.au webmail.hosteddomain3.com
    ServerAlias mail.hosteddomain4.com.au webmail.hosteddomain4.com
    ServerAlias mail.hosteddomain5.com.au webmail.hosteddomain5.com
    ErrorLog /var/www/mail/logs/error_log
    CustomLog /var/www/mail/logs/access_log combined_vhost
    RewriteEngine on
    RewriteRule ^/index\.html$ http://%{HTTP_HOST}/cgi-bin/sqwebmail
    ScriptAlias /cgi-bin/ "/var/www/mail/cgi-bin/"
    <Directory "/var/www/mail/cgi-bin">
	AllowOverride None
	Options None
	Order allow,deny
	Allow from all
    </Directory>
    BrowserMatch "MSIE [45]" nokeepalive downgrade-1.0 force-response-1.0
</VirtualHost>
 
vi /etc/httpd/conf.d/ssl.conf
<VirtualHost _default_:443>
    ServerAdmin postmaster@yourdomain.com
    DocumentRoot /var/www/mail/html
    SSLCertificateFile /usr/local/ssl/mail.yourdomain.com.crt
    SSLCertificateKeyFile /usr/local/ssl/mail.yourdomain.com.key
    ErrorLog /var/www/mail/logs/ssl_error_log
    RewriteEngine On
    RewriteRule ^/index\.html$ http://mail.yourdomain.com/cgi-bin/sqwebmail
    ScriptAlias /cgi-bin/ "/var/www/mail/cgi-bin/"
    <Directory "/var/www/mail/cgi-bin">
        AllowOverride None
        Options None
        Order allow,deny
        Allow from all
    </Directory>
    SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0
    CustomLog /var/www/mail/logs/ssl_request_log "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
</VirtualHost>

Add some more directory structure to allow us to serve user homepages

mkdir -p /var/www/users/html
mkdir /var/www/users/cgi-bin
mkdir /var/www/users/logs

Add these commands so Apache can serve user pages. I would suggest you put in 1 VirtualHost entry per domain

vi /etc/httpd/conf/httpd.conf
<VirtualHost *:80>
    ServerAdmin webmaster@yourdomain.com
    DocumentRoot /var/www/users/html
    ServerName users.yourdomain.com
    ServerAlias home.yourdomain.com
    ErrorLog /var/www/users/logs/error_log
    CustomLog /var/www/users/logs/access_log combined_vhost
    DirectoryIndex index.html index.htm default.html default.htm
    RewriteEngine on
    RewriteRule ^/~([\w-_\.])([\w-_\.]*)/?(.*)$ /var/vmail/yourdomain.com/$1/$1$2/public_html/$3 [L]
    RewriteRule ^/index.html$ http://www.yourdomain.com
    php_flag Engine Off
    <Directory /var/vmail/yourdomain.com/*/*/public_html>
      AllowOverride FileInfo AuthConfig Indexes Limit
      Options MultiViews Indexes IncludesNoExec
      Order allow,deny
      Allow from all
    </Directory>
    ScriptAlias /cgi-bin/ "/var/www/users/cgi-bin/"
    <Directory "/var/www/users/cgi-bin">
	AllowOverride None
	Options None
	Order allow,deny
	Allow from all
    </Directory>
</VirtualHost>

Remove the default welcome page

vi /etc/httpd/conf.d/welcome.conf

#<LocationMatch "^/+$">
#Options -Indexes
#ErrorDocument 403 /error/noindex.html
#</LocationMatch>

Setup favicons

cd /var/www/mail/html
wget http://www.yourdomain.com/favicon.ico
cd /var/www/users/html
wget http://www.yourdomain.com/favicon.ico

Restart your apache :

/etc/rc.d/init.d/httpd restart

Setup log rotation for your httpd logs

vi /etc/logrotate.d/httpd
# change line from this 
/var/log/httpd/*log {
# to this
/var/log/httpd/*log /var/www/mail/logs/*log /var/www/users/logs/*log {

SQWEBMAIL

http://www.courier-mta.org/sqwebmail/

Create the directory structure which will receive the sqwebmail png images, binary, and logs

mkdir -p /var/www/mail/html/images/sqwebmail
mkdir -p /var/www/mail/cgi-bin
mkdir -p /var/www/mail/logs

Create an advertising banner type script that will be used by the sqwebmail binary. The banner is displayed at the bottom of every page.

echo '#!/bin/sh' > /usr/local/bin/sqwebmail-banner.sh
echo '##' >> /usr/local/bin/sqwebmail-banner.sh
echo '## This progam is called by sqwebmail for each [#B#] tag in the html templates' >> /usr/local/bin/sqwebmail-banner.sh
echo '## The ARGV[0] will be the name of the html template that launched the call' >> /usr/local/bin/sqwebmail-banner.sh
echo '##' >> /usr/local/bin/sqwebmail-banner.sh
echo 'echo "<center>";' >> /usr/local/bin/sqwebmail-banner.sh
echo 'echo "<hr>";' >> /usr/local/bin/sqwebmail-banner.sh
echo 'echo "<b>SomeISP support - call 1300 xxx xxx</b>";' >> /usr/local/bin/sqwebmail-banner.sh
echo 'echo "</center>";' >> /usr/local/bin/sqwebmail-banner.sh
chmod 755 /usr/local/bin/sqwebmail-banner.sh

Download and extract the sources

cd /usr/local/src
wget http://optusnet.dl.sourceforge.net/sourceforge/courier/sqwebmail-5.1.6.tar.bz2
tar xjf sqwebmail-5.1.6.tar.bz2
chown -R root.root sqwebmail-5.1.6
cd sqwebmail-5.1.6

Configure and compile

COURIERAUTHCONFIG=/usr/local/courier-authlib/bin/courierauthconfig \
CPPFLAGS=-I/usr/local/courier-authlib/include \
./configure \
  --prefix=/usr/local/sqwebmail \
  --disable-autorenamesent \
  --enable-cgibindir=/var/www/mail/cgi-bin/ \
  --enable-imagedir=/var/www/mail/html/images/sqwebmail/ \
  --enable-imageurl=/images/sqwebmail \
  --with-maxformargsize=17500000 \
  --with-maxmsgsize=18000000 \
  --enable-bannerprog=/usr/local/bin/sqwebmail-banner.sh
make
make install
make install-configure

Allow sqwebmail users to use (maildrop) message filtering rules

vi /usr/local/sqwebmail/etc/maildirfilterconfig
MAILDIRFILTER=../.mailfilter
MAILDIR=$HOME/$DEFAULT

Configure sqwebmail to start at bootup (NEED TO WRITE AN PROPER SYSV-STYLE INIT SCRIPT)

vi /etc/rc.d/rc.local
/usr/local/sqwebmail/libexec/sqwebmaild.rc start

Start the sqwebmail

/usr/local/sqwebmail/libexec/sqwebmaild.rc start

If all goes well, ps axf should give something like this

29727 ? S 0:00 /usr/local/courier-authlib/sbin/courierlogger -pid=/usr/local/sqwebmail/var/run/sqwebmaild.pid -start /us
29728 ? S 0:00 \_ /usr/local/sqwebmail/libexec/sqwebmail/sqwebmaild
29730 ? S 0:00 /usr/local/sqwebmail/libexec/sqwebmail/sqwebmaild
29732 ? S 0:00 /usr/local/sqwebmail/libexec/sqwebmail/sqwebmaild
29734 ? S 0:00 /usr/local/sqwebmail/libexec/sqwebmail/sqwebmaild
29736 ? S 0:00 /usr/local/sqwebmail/libexec/sqwebmail/sqwebmaild
29738 ? S 0:00 /usr/local/sqwebmail/libexec/sqwebmail/sqwebmaild

Sqwebmail has a cache, that requires old files to be zapped regularly

crontab -e
# Purge sqwebmail cache files once per hour
0 * * * * /usr/local/sqwebmail/share/sqwebmail/cleancache.pl

Test access :

http://mail.yourdomain.com/
or
https://mail.yourdomain.com/

Customise the sqwebmail templates ( apply your own branding )

The ones I tweak are :

/usr/local/sqwebmail/share/sqwebmail/html/en/loginform.inc.html
/var/www/mail/html/images/sqwebmail/sqwebmail.css

CLAM ANTIVIRUS

http://www.clamav.net/

Install prerequisite modules

yum install bzip2-devel

Create the user/group for clamd to run under

groupadd -r clamav
useradd -r -g clamav -d /var/amavis -m -s /bin/false -c "Clam AntiVirus" clamav

Download and extract the sources

cd /usr/local/src
wget http://optusnet.dl.sourceforge.net/sourceforge/clamav/clamav-0.90.2.tar.gz
tar xzf clamav-0.90.2.tar.gz
chown -R root.root clamav-0.90.2
cd clamav-0.90.2

Configure and compile

./configure
# TIP: on some platforms (Incl CentOS) you may have to use 
# ./configure --disable-zlib-vcheck
make
make install
# This next step is recommended by ClamAV authors after installing 0.90.1
ldconfig

Tweak the config files as shown below

ln -s /usr/local/etc/clamd.conf /etc/clamd.conf
vi /etc/clamd.conf
# Example
LogSyslog yes
LocalSocket /var/amavis/clamd.sock
FixStaleSocket yes
MaxThreads 10
User clamav
ln -s /usr/local/etc/freshclam.conf /etc/freshclam.conf
vi /etc/freshclam.conf
# Example
LogSyslog yes
DatabaseMirror db.au.clamav.net
NotifyClamd /etc/clamd.conf

Setup the init scripts

cp contrib/init/RedHat/clamd /etc/rc.d/init.d/clamd
chmod 744 /etc/rc.d/init.d/clamd
chkconfig --add clamd
vi /etc/rc.d/rc.local
/usr/local/bin/freshclam -d 

Start the daemons

/etc/rc.d/init.d/clamd start
/usr/local/bin/freshclam -d 

ps axf should give something like this :

11139 ? Ss 0:00 /usr/local/sbin/clamd
11142 ? Ss 0:00 /usr/local/bin/freshclam -d

SPAMASSASSIN

http://spamassassin.apache.org/

Install the prerequisite perl modules

perl -MCPAN -e shell
o conf prerequisites_policy follow
install LWP MD5
install Digest::SHA1 HTML::Parser Net::DNS MD5 HTTP::Date IO::Zlib Archive::Tar
install MIME::Base64 DB_File Net::SMTP Mail::SPF Time::HiRes
quit

Download and unpack the sources

cd /usr/local/src
wget http://apache.mirror.pacific.net.au/spamassassin/source/Mail-SpamAssassin-3.2.0.tar.gz
tar xzf Mail-SpamAssassin-3.2.0.tar.gz
chown -R root.root Mail-SpamAssassin-3.2.0
cd Mail-SpamAssassin-3.2.0

Compile and install

perl Makefile.PL
#[answer the questions]
make 
make install

Setup the SpamAssassin config file. ( Note, this is a complete config file that can replace the default supplied one )

vi /etc/mail/spamassassin/local.cf
## Enable auto-whitelisting
use_auto_whitelist 1
####
# WILL HAVE NO EFFECT. THE EQUIVALENT SETTINGS IN AMAVISD ARE THE ONES YOU NEED TO SET
#
# ## Required point score before considered spam
# required_score 5
#
#
# ## What to tag the subject line with
# rewrite_header Subject [SPAM]
#
# ## Put the report in the headers. Dont touch the body of the message at all
# report_safe 0
# Enable the Bayes system
use_bayes 1
bayes_auto_learn 1
bayes_auto_learn_threshold_spam 10
## Set headers which may provide inappropriate cues to the Bayesian
## classifier
bayes_ignore_header X-Bogosity
bayes_ignore_header X-Spam-Flag
bayes_ignore_header X-Spam-Status
## Enable or disable network checks
skip_rbl_checks 0
## File locking method. We dont need to worry about being NFS-safe
lock_method flock
## Give spamassassin some hints as to what IPs are under our control.
## Generally this will be a similar list to what you have put in the postfix mynetworks file
trusted_networks 127.0.0.1         # needed so amavisd headers don't trip up spamassassin
trusted_networks 192.168.1.0/24    # you need to include all the IPs your mail server, and local LAN workstation

AMAVISD

http://www.ijs.si/software/amavisd/

Install some optional rpm's that will help amavisd inspect different types of attachments

# These are available via yum on Fedora, but not on CentOS
yum install arc cabextract zoo lzop freeze 

Install the prerequisite perl modules

perl -MCPAN -e shell
o conf prerequisites_policy follow
install Archive::Zip Compress::Zlib Convert::TNEF Convert::UUlib MIME::Base64 MIME::Parser
install Mail::Internet Net::Server Digest::MD5 IO::Stringy Time::HiRes Unix::Syslog BerkeleyDB
install DBI DBD::mysql
quit

Download and unpack the sources

cd /usr/local/src
wget http://www.ijs.si/software/amavisd/amavisd-new-2.5.0.tar.gz
tar xzf amavisd-new-2.5.0.tar.gz
chown -R root.root amavisd-new-2.5.0
cd amavisd-new-2.5.0

Install the program

mkdir /var/amavis/tmp /var/amavis/var /var/amavis/db
chown -R clamav.clamav /var/amavis
chmod -R 750 /var/amavis
cp amavisd /usr/local/sbin/
chown root /usr/local/sbin/amavisd
chmod 755 /usr/local/sbin/amavisd
cp amavisd.conf-sample /etc/amavisd.conf
chown root /etc/amavisd.conf
chmod 600 /etc/amavisd.conf

Make the following changes to the config file

vi /etc/amavisd.conf
$mydomain = 'yourdomain.com';
$daemon_user = 'clamav';
$daemon_group = 'clamav';
$TEMPBASE = "$MYHOME/tmp";
$forward_method = 'smtp:[127.0.0.1]:10025';
$notify_method = $forward_method;

$max_servers = 10;

@local_domains_maps = ( [".$mydomain"] );
# 
# Normally we would want a list of local domains set here,
# but when using SQL-based recipient lookups this isn't necessary.
# Doco says :
#	A special shorthand is provided when SQL lookups are used: when a match
#	for recipient address (or domain) is found in SQL tables (regardless of
#	field values), the recipient is considered local, regardless of static
#	@local_comains_acl or %local_domains lookup tables. This simplifies
#	life when a large number of dynamically changing domains is hosted.
#	To overrule this behaviour, add an explicit boolean field 'local'
#	to table 'users' (missing field defaults to true, meaning record match
#	implies the recipient is local; a NULL field 'local' is not special,
#	it is interpreted as undef like other NULL fields, causing search
#	to continue into other lookup tables).
#
# On other (non-mysql) servers that I have built as antivirus/antispam front-ends 
# (which then onforward mail to server that has the mailboxes), I have used a setting like this :
#   @local_domains_maps = ("."); # consider all domains to be local   
$log_level = 1;
$final_virus_destiny = D_DISCARD;
$final_banned_destiny = D_BOUNCE;
$final_spam_destiny = D_DISCARD;
$virus_admin = undef;
$spam_admin = undef;
#$QUARANTINEDIR = '/var/virusmails';
$virus_quarantine_to = undef; 
$bad_header_quarantine_to = undef;
$banned_quarantine_to = undef;
$spam_quarantine_to = undef;
# qr'^\.(exe-ms|dll)$',
# qr'^application/x-msdownload$'i,
# qr'^application/x-msdos-program$'i,
#  qr'.\.(exe|vbs|pif|scr|cpl)$'i, # banned extension - basic
qr'.\.(vbs|pif|scr|cpl)$'i, # banned extension - basic
@spam_lovers_maps = (
  ['postmaster@', 'abuse@']
);
@lookup_sql_dsn = ( ['DBI:mysql:database=postfix;host=localhost', 'postfixuser', 'postfixpass'] ); 
$sql_select_policy = 
  'SELECT virus_lover, spam_lover, banned_files_lover, bad_header_lover, '.
  'bypass_virus_checks, bypass_spam_checks, bypass_banned_checks, bypass_header_checks, '.
  'spam_tag2_level, spam_kill_level FROM mailbox WHERE email IN (%k)';
$sql_select_white_black_list = undef;  ## STILL NEED TO WORK OUT HOW TO IMPLEMENT THIS FEATURE
@whitelist_sender_maps = ( 
  ['MAILER-DAEMON@', 'postmaster@']
);
$sa_local_tests_only = 0;
$sa_tag_level_deflt = undef;
$sa_tag2_level_deflt = 5;
$sa_kill_level_deflt = 10;
$sa_dsn_cutoff_level = undef;
$sa_spam_subject_tag = '[SPAM] ';
$sa_spam_modifies_subj = 1;
### http://www.clamav.net/
['ClamAV-clamd',
\&ask_daemon, ["CONTSCAN {}\n", "/var/amavis/clamd.sock"],
qr/\bOK$/, qr/\bFOUND$/,
qr/^.*?: (?!Infected Archive)(.*) FOUND$/ ],
# invoke custom hooks
#my($custom_config) = '/etc/amavisd-custom.conf';
#$! = 0;
#if (!defined($custom_config)) {}
#elsif (defined(do $custom_config)) {} # good, code successfully loaded
#elsif ($@ ne '') { die "Error in config file \"$custom_config\": $@" }
#elsif ($! != 0) { die "Error reading config file \"$custom_config\": $!" }

Install init scripts, and start the daemon

cp amavisd_init.sh /etc/rc.d/init.d/amavisd
chown root.root /etc/rc.d/init.d/amavisd
chmod 744 /etc/rc.d/init.d/amavisd
chkconfig --add amavisd
vi /etc/rc.d/init.d/amavisd
prog="/usr/local/sbin/amavisd"
/etc/rc.d/init.d/amavisd start

If all goes well, ps axf should show you something like this :

16534 ? Ss 0:00 amavisd (master)
16537 ? S 0:00   \_ amavisd (virgin child)
16538 ? S 0:00   \_ amavisd (virgin child)
16539 ? S 0:00   \_ amavisd (virgin child)
16540 ? S 0:00   \_ amavisd (virgin child)
16541 ? S 0:00   \_ amavisd (virgin child)
16542 ? S 0:00   \_ amavisd (virgin child)
16543 ? S 0:00   \_ amavisd (virgin child)
16544 ? S 0:00   \_ amavisd (virgin child)
16545 ? S 0:00   \_ amavisd (virgin child)
16546 ? S 0:00   \_ amavisd (virgin child)

Add these commands to tell Postfix to use Amavis for scanning

vi /etc/postfix/main.cf
content_filter=smtp-amavis:[127.0.0.1]:10024
smtp-amavis_destination_concurrency_limit=10
vi /etc/postfix/master.cf
smtp-amavis unix - - n - - lmtp
  -o lmtp_data_done_timeout=1200
  -o lmtp_send_xforward_command=yes
  -o lmtp_connection_timeout=2
  -o disable_dns_lookups=yes
  -o max_use=20
127.0.0.1:10025 inet n - n - - smtpd-av
  -o content_filter=
  -o smtpd_restriction_classes=
  -o smtpd_delay_reject=no
  -o smtpd_client_restrictions=permit_mynetworks,reject
  -o smtpd_helo_restrictions=
  -o smtpd_sender_restrictions=
  -o smtpd_recipient_restrictions=permit_mynetworks,reject
  -o smtpd_data_restrictions=reject_unauth_pipelining
  -o smtpd_end_of_data_restrictions=
  -o mynetworks=127.0.0.0/8
  -o smtpd_error_sleep_time=0
  -o smtpd_soft_error_limit=1001
  -o smtpd_hard_error_limit=1000
  -o smtpd_client_connection_count_limit=0
  -o smtpd_client_connection_rate_limit=0
  -o smtpd_milters=
  -o local_header_rewrite_clients=
  -o local_recipient_maps=
  -o relay_recipient_maps=
  -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_address_mappings
cd /usr/libexec/postfix
ln -s smtpd smtpd-av
postfix reload

Disable logwatch script from reporting on amavisd logs

The logwatch script runs nightly, and emails a report to the root user Unless your server is very low volume, I would recommend you tell logwatch not to report amavisd stats, otherwise the report gets much too big

vi /etc/log.d/conf/logwatch.conf
# Look for where it says "Service = All" and underneath that add this line :
Service = -amavis

PUREFTPD

http://www.pureftpd.org

Pureftpd has been chosen because it supports MySQL and softquotas. The softquotas work in a similar way to the maildir++ quota system. But instead of the quotas being stored in a maildirsize file, they are stored in a file called ".ftpquota"

Download and unpack

cd /usr/local/src
wget http://download.pureftpd.org/pub/pure-ftpd/releases/pure-ftpd-1.0.21.tar.gz
tar xzf pure-ftpd-1.0.21.tar.gz
chown -R root.root pure-ftpd-1.0.21
cd pure-ftpd-1.0.21

configure, compile, install

./configure \
  --without-inetd \
  --without-shadow \
  --without-humor \
  --without-usernames \
  --with-boring \
  --with-extauth \
  --with-mysql \
  --with-quotas \
  --with-virtualchroot \
  --with-cookie \
  --with-tls \
  --with-certfile=/usr/local/ssl/mail.yourdomain.com.pem 

# Note, the configure script will emit this warning :
# "configure: WARNING: No certificate is installed in /usr/local/ssl/mail.yourdomain.com.pem yet".
# However apon inspecting the source code it can be seen this is a bug. If the file exists it reports that warning,
# when it should actually report that warning if the file is missing. I have reported that bug to the pureftpd authors.
# They have acknowledged the bug and the fix will be included in v1.0.22

TIP : If you are using x86_64 platform ( eg Opteron ), you will have to modify your configure command to be :
LDFLAGS=-L/usr/lib64/mysql \
./configure \
  --without-inetd \
  ......etc 
 
make
make install-strip

Setup the config file by making these changes

cp pureftpd-mysql.conf /etc
vi /etc/pureftpd-mysql.conf
#MYSQLServer 127.0.0.1
#MYSQLPort 3306
MYSQLSocket /var/lib/mysql/mysql.sock
MYSQLUser postfixuser
MYSQLPassword postfixpass
MYSQLDatabase postfix
MYSQLCrypt cleartext
MYSQLGetPW SELECT clear_password FROM mailbox WHERE email="\L" AND disableftp=0
#MYSQLGetUID SELECT Uid FROM mailbox WHERE email="\L"
MYSQLDefaultUID 1001
#MYSQLGetGID SELECT Gid FROM mailbox WHERE email="\L"
MYSQLDefaultGID 1001
MYSQLGetDir SELECT CONCAT('/var/vmail/',maildir,'public_html') FROM mailbox WHERE email="\L" AND disableftp=0
MySQLGetQTASZ SELECT ftpquota*1024 FROM mailbox WHERE email="\L" and disableftp=0
chmod 600 /etc/pureftpd-mysql.conf

Setup a welcome banner

echo "##-------------------------------------------"    >  /etc/pureftpd-banner.txt
echo "## yourdomain.com users FTP"                      >> /etc/pureftpd-banner.txt
echo "##-------------------------------------------"    >> /etc/pureftpd-banner.txt
echo "##"                                               >> /etc/pureftpd-banner.txt
echo "## IMPORTANT"                                     >> /etc/pureftpd-banner.txt
echo "## Please login using your full email address"    >> /etc/pureftpd-banner.txt
echo "## eg username@yourdomain.com"                    >> /etc/pureftpd-banner.txt
echo "##"                                               >> /etc/pureftpd-banner.txt
echo "##-------------------------------------------"    >> /etc/pureftpd-banner.txt
chmod 644 /etc/pureftpd-banner.txt

Configure pureftpd to start at boot time

vi /etc/rc.d/rc.local
/usr/local/sbin/pure-ftpd -l mysql:/etc/pureftpd-mysql.conf --noanonymous --ipv4only --fortunesfile=/etc/pureftpd-banner.txt --createhomedir --customerproof --tls=1 --daemonize

Start pureftpd

/usr/local/sbin/pure-ftpd -l mysql:/etc/pureftpd-mysql.conf --noanonymous --ipv4only --fortunesfile=/etc/pureftpd-banner.txt --createhomedir --customerproof --tls=1 --daemonize

If all goes well, ps axf should show something like this :

11204 ? Ss 0:00 pure-ftpd (SERVER)

Allow your IPTables to handle ftp connections at least semi-gracefully

vi /etc/rc.d/rc.local
/sbin/modprobe ip_conntrack_ftp

Setup a crontab to ensure virtual quotas are always kept correct. Especially important after you have copied any content into the public_html dirs using a method other than pureftpd

vi /usr/local/sbin/rebuild-ftp-softquotas.pl
#!/usr/bin/perl -w
##
## Loop through SQL and extract a list of userdirs
## For each user, recalc their ftp softquota file
##
use strict;
use DBI;
my $dbh = DBI->connect('DBI:mysql:postfix', 'postfixuser', 'postfixpass') || die "Database connection failed: $DBI::errstr";
my $sql = "SELECT CONCAT('/var/vmail/',maildir) AS homedir FROM mailbox WHERE active=1 ORDER BY maildir";
my $sth = $dbh->prepare($sql);
$sth->execute() || die "Could not execute SQL statement";
while (my($homedir)=$sth->fetchrow_array) {
    my $ftpdir = $homedir . "public_html";
    if (-d "$ftpdir") {
        my $escaped_ftpdir = $ftpdir;
        $escaped_ftpdir =~ s/\&/\\&/g;
        $escaped_ftpdir =~ s/\ /\\ /g;
        my $cmd = sprintf ("/usr/local/sbin/pure-quotacheck -u 1001 -g 1001 -d $escaped_ftpdir");
        system ($cmd);
    } else {
        print ("WARNING: dir does not exist : $ftpdir\n");
    }
}
$sth->finish(); 
$dbh->disconnect();
chmod 700 /usr/local/sbin/rebuild-ftp-softquotas.pl
crontab -e
### Periodically recalc all the ftp softquotas ( just in case )
0 2 * * Sun /bin/nice /usr/local/sbin/rebuild-ftp-softquotas.pl

LAST BITS OF CLEANUP

Remove some unneeded stuff from MySQL, and set some passwords

mysql
-- MySQL comes with a "test" database and an anonymous user which has access to the test database
-- We don't want either of these so lets get rid of them now
DROP DATABASE test;
DELETE FROM mysql.user WHERE User='';
DELETE FROM mysql.db WHERE User='';
-- Set a root password ( by default there is no password set,
-- which means anyone with shell access could type "mysql -u root" and login to MySQL as root user )
UPDATE mysql.user SET Password = PASSWORD('newpwd') WHERE User = 'root';
-- Tell MySQL to pickup our username/password changes
FLUSH PRIVILEGES;

Setup a config file that stores the MySQL root password, so the system root user doesn't have to type the MySQL root password when logging into MySQL

vi /root/.my.cnf
[client]
password=newpwd
chmod 600 /root/.my.cnf

Create a local user for yourself, and disable root SSH logins

By default the SSH daemon will permit root logins. Its a security risk to leave this enabled. I would recommend you create a local account which you can use when connecting via SSH. Once connected you can then type "su -" to switch to the root user if required.

useradd someuser
passwd someuser
vi /etc/ssh/sshd_config
PermitRootLogin no
/etc/rc.d/init.d/sshd restart

Here's a script which I use to periodically create any missing softquota files. I run this weekly from crontab. Note the script tests for chmod 0 on the user's public_html : this is done because when I suspend a user for non-payment etc I go and set their disableftp/imap/pop3/webmail=1 in the mailbox table ( but leave active=1 so incoming mail doesn't get bounced), and I also chmod 0 their public_html dir to stop their webpages from being served.

vi /usr/local/sbin/create-missing-softquotas.pl
#!/usr/bin/perl -w
### Wonder if it would be worth while enhancing the script further to autocreate missing dirs?
### Might be bad news though if the db contains some sort of shonky home dir info
### Maybe just sending an email alert to admin is the best bet, get someone to investigate manually.
###
use strict;
use DBI;
my $dbh = DBI->connect('DBI:mysql:postfix', 'postfixuser', 'postfixpass');
my $sql = "SELECT CONCAT('/var/vmail/',maildir) as homedir, CONCAT(mailquota*1024*1024,'S') AS mailquota, ftpquota*1024 as ftpquota FROM mailbox WHERE active=1 ORDER BY maildir";
my $sth = $dbh->prepare($sql);
$sth->execute() || die "Could not execute SQL statement";
my $cmd;
while (my($homedir, $mailquota, $ftpquota)=$sth->fetchrow_array) {
# chop the trailing slash off the homedir
$homedir =~ s/\/$//;
#print "checking $homedir ($mailquota, $ftpquota) ..\n";
unless (-e "$homedir/Maildir/maildirsize") {
if (-d "$homedir/Maildir") {
print "maildirsize missing from $homedir/Maildir, creating it now\n";
my $escaped_maildir = $homedir . "/Maildir";
$escaped_maildir =~ s/\&/\\&/g;
$escaped_maildir =~ s/\ /\\ /g;
#### Create the quota file
$cmd = sprintf ("/usr/local/courier-imap/bin/maildirmake -q $mailquota $escaped_maildir");
system ($cmd);
#### Fix up permissions
$cmd = sprintf ("chown vmail.vmail $escaped_maildir/maildirsize");
system ($cmd);
$cmd = sprintf ("chmod 600 $escaped_maildir/maildirsize");
system ($cmd);
} else {
print "Error, $homedir/Maildir doesnt exist, this needs to be rectified\n";
}
}
unless (-e "$homedir/public_html/.ftpquota") {
if (-d "$homedir/public_html") {
# bypass any dirs that have been chmod to 0 ( suspended )
my $mode = (stat $homedir."/public_html")[2];
unless ($mode == 0x4000) {
print ".ftpquota missing from $homedir/public_html, creating it now\n";
my $escaped_ftpdir = $homedir . "/public_html";
$escaped_ftpdir =~ s/\&/\\&/g;
$escaped_ftpdir =~ s/\ /\\ /g;
$cmd = sprintf ("/usr/local/sbin/pure-quotacheck -u 1001 -g 1001 -d $escaped_ftpdir");
system ($cmd);
}
} else {
print "Error, $homedir/public_html doesnt even exist, this needs to be rectified\n";
}
}
}
$sth->finish(); 
$dbh->disconnect();
chmod 700 /usr/local/sbin/create-missing-softquotas.pl
crontab -e
0 3 * * * /bin/nice /usr/local/sbin/create-missing-softquotas.pl

SAMPLE / TEST DATA

Notes :

For every new virtual mailbox domain, insert a new row into the 'mailbox_domains' table

For every new virtual mailbox user, insert a new row into the 'mailbox' table, and create their maildir on the disk.   Note: In our example we are going to "hash" the path to the user's dir, by taking the first letter of the username and putting that into the path eg domain/<1stletter>/username. This is done to the system from slowing down when it has to working with a directory that contains a huge number of entries. If you only plan to have a few hundred users you could probably go without any hashing. If you have thousands of users then 1 level of hashing is a good idea. If you have many tens of thousands of users then you might want to increase the level of hashing eg domain/<1stletter>/<1stletter><2ndletter>/username, or domain/<firstletter><2ndletter>/username)

For every new virtual alias domain, insert a new row into the 'alias_domains' table

For every every new alias/forward (doesn't matter if they are for a mailbox_domain or alias_domain), insert a row into the 'alias' table

Here is some examples of data  :

use postfix;
----------------------------------------------------------------------------------------------------
-- Tell postfix which domains we host this as virtual mailbox domains
--
INSERT INTO mailbox_domains ( domain, description, created, modified )
VALUES ('testdomain.com', 'Postfix virtual mailbox domain', NOW(), NOW());
INSERT INTO mailbox_domains ( domain, description, created, modified )
VALUES ('testdomain2.com', 'Postfix virtual mailbox domain', NOW(), NOW());
----------------------------------------------------------------------------------------------------
-- Tell postfix about the virtual mailboxes that we host
--
-- Note that when generating the crypted password, you can't use the MySQL CRYPT function.
-- If you are migrating users out of an /etc/passwd type file, you can just copy the crypted password 
-- from there. 
-- If you need to generate a crypted password, you can use some code this like :
--   perl -e "print crypt('testpass', join '', ('.', '/', 0..9,'A'..'Z', 'a'..'z')[rand 64, rand 64]);"
--
-- If you don't enter the clear password, password auth will still work, except for auth methods
-- that require the clear password to be available eg CRAM or DIGEST. More info on this subject here
--
-- We haven't defined any of the spam_lover columns etc. If you don't specify a value there, amavisd
-- will just apply its default settings from amavisd.conf ( virusscanning on, antispam scanning on, tag at 5 drop at 10 etc )
--
INSERT INTO mailbox (email, password, clear_password, maildir, created, modified)
VALUES ('user1@testdomain.com', 'dzOCCGgyq3TVo', 'testpass', 'testdomain.com/u/user1/', NOW(), NOW());
INSERT INTO mailbox (email, password, clear_password, maildir, created, modified)
VALUES ('someuser2@testdomain.com', 'dzOCCGgyq3TVo', 'testpass', 'testdomain.com/s/someuser2/', NOW(), NOW());
INSERT INTO mailbox (email, password, clear_password, maildir, created, modified)
VALUES ('user1@testdomain2.com', 'dzOCCGgyq3TVo', 'testpass', 'testdomain2.com/u/user1/', NOW(), NOW());
INSERT INTO mailbox (email, password, clear_password, maildir, created, modified)
VALUES ('thisuser2@testdomain2.com', 'dzOCCGgyq3TVo', 'testpass', 'testdomain2.com/t/thisuser2/', qNOW(), NOW());
----------------------------------------------------------------------------------------------------
-- Add in some alias address mappings 
--
INSERT INTO alias (address, goto, created, modified)
VALUES ('user3@testdomain.com', 'user2@hotmail.com', NOW(), NOW());
INSERT INTO alias (address, goto, created, modified)
VALUES ('sales@testdomain.com', 'someuser2@testdomain.com', NOW(), NOW());
-- "Catchall" entry 
INSERT INTO alias (address, goto, created, modified)
VALUES ('@testdomain.com', 'user1@testdomain.com', NOW(), NOW());

To create the Maildirs on the disk you will need some commands like :

# create the full directory tree through to the users dir
mkdir -p /var/vmail/testdomain.com/u/user1/public_html
# create the maildir structure
maildirmake /var/vmail/testdomain.com/u/user1/Maildir
# create the softquota "maildirsize" file in the maildir.
# ( If this file isn't present, no quotas will be enforced )
maildirmake -q 20971520S /var/vmail/testdomain.com/u/user1/Maildir
chmod g-r,o-r /var/vmail/testdomain.com/u/user1/Maildir/maildirsize 
chown -R vmail.vmail /var/vmail/testdomain.com/u/user1

You can also host non-mailbox domains, where all addresses are forwarding on to other locations :
Note that postfix will use any user@domain mappings before any @domain mappings are matched

----------------------------------------------------------------------------------------------------
INSERT INTO alias_domains ( domain, description, created, modified )
VALUES ('testdomain3.com', 'Postfix virtual alias domain', NOW(), NOW());
-- map xxx@testdomain3.com to xxx@testdomain.com
INSERT INTO alias (address, goto, created, modified)
VALUES ('@testdomain3.com', '@testdomain.com', NOW(), NOW());
----------------------------------------------------------------------------------------------------
INSERT INTO alias_domains ( domain, description, created, modified )
VALUES ('testdomain4.com', 'Postfix virtual alias domain', NOW(), NOW());
INSERT INTO alias (address, goto, created, modified)
VALUES ('user1@testdomain4.com', 'jim@blah.com', NOW(), NOW());
INSERT INTO alias (address, goto, created, modified)
VALUES ('user2@testdomain4.com', 'john@something.com', NOW(), NOW());
INSERT INTO alias (address, goto, created, modified)
VALUES ('@testdomain4.com', 'fred@somewhere.com', NOW(), NOW());

Here are some examples of data for the "access" files :

-- Always permit mail to our abuse and postmaster addresses.
-- Don't do RBL checking etc for such mail
INSERT INTO recipient_access ( recipient, response, note, created, modified )
VALUES ('abuse', 'OK', 'Dont do RBL checking etc for mail to our abuse address', NOW(), NOW());
INSERT INTO recipient_access ( recipient, response, note, created, modified )
VALUES ('postmaster', 'OK', 'Dont do RBL checking etc for mail to our postmaster address', NOW(), NOW());
-- match incoming smtp connections based on senders IP
INSERT INTO client_access ( client, response, created, modified )
VALUES ('66.6.223.100','REJECT Sorry, you are sending spam', NOW(), NOW());
-- match incoming smtp connections based on senders address
INSERT INTO sender_access ( sender, response, created, modified )
VALUES ('support@westpac.com.au', 'REJECT Sober VIRUS', NOW(), NOW());
-- recipient access can be handy if you have a customer who has configured
-- a wildcard mapping enabled for their domain, but wants to reject one 
-- particular address from matching the wildcard
INSERT INTO recipient_access ( recipient, response, note, created, modified )
VALUES ('example@testdomain2.com', 'REJECT Mailbox closed', NOW(), NOW());

MRTG / SNMP

http://people.ee.ethz.ch/~oetiker/webtools/mrtg/

You want to be able to keep an eye on your mail server using MRTG

Install the SNMP applications

yum install net-snmp net-snmp-utils

Add some basic scripts for snmpd to make use of

echo '#!/bin/sh' > /usr/local/bin/mrtg-incoming-count.sh
echo 'find /var/spool/postfix/incoming -type f | wc -l' >> /usr/local/bin/mrtg-incoming-count.sh
chmod 744 /usr/local/bin/mrtg-incoming-count.sh 
echo '#!/bin/sh' > /usr/local/bin/mrtg-active-count.sh
echo 'find /var/spool/postfix/active -type f | wc -l' >> /usr/local/bin/mrtg-active-count.sh
chmod 744 /usr/local/bin/mrtg-active-count.sh 
echo '#!/bin/sh' > /usr/local/bin/mrtg-deferred-count.sh
echo 'find /var/spool/postfix/deferred -type f | wc -l' >> /usr/local/bin/mrtg-deferred-count.sh
chmod 744 /usr/local/bin/mrtg-deferred-count.sh 

Configure the snmpd

echo 'com2sec local localhost yourstring' > /etc/snmp/snmpd.conf
echo 'com2sec mynetwork xxx.xxx.xxx.xxx/32 yourstring' >>/etc/snmp/snmpd.conf
echo 'group MyROGroup v1 local' >>/etc/snmp/snmpd.conf
echo 'group MyROGroup v1 mynetwork' >>/etc/snmp/snmpd.conf
echo 'view all included .1 80' >>/etc/snmp/snmpd.conf
echo 'access MyROGroup "" any noauth exact all none none' >>/etc/snmp/snmpd.conf
echo 'syslocation Some Location' >>/etc/snmp/snmpd.conf
echo 'syscontact Some Name <some@emailaddress>' >>/etc/snmp/snmpd.conf
echo 'proc pipe' >>/etc/snmp/snmpd.conf
echo 'proc smtp' >>/etc/snmp/snmpd.conf
echo 'proc lmtp' >>/etc/snmp/snmpd.conf
echo 'proc smtpd-mx' >>/etc/snmp/snmpd.conf
echo 'proc pop3d' >>/etc/snmp/snmpd.conf
echo 'proc imapd' >>/etc/snmp/snmpd.conf
echo 'exec active-count /bin/sh /usr/local/bin/mrtg-active-count.sh' >>/etc/snmp/snmpd.conf
echo 'exec incoming-count /bin/sh /usr/local/bin/mrtg-incoming-count.sh' >>/etc/snmp/snmpd.conf
echo 'exec deferred-count /bin/sh /usr/local/bin/mrtg-deferred-count.sh' >>/etc/snmp/snmpd.conf

Protect your SNMP password from prying eyes

chmod 600 /etc/snmp/snmpd.conf

Configure snmpd to launch at boot time

chkconfig snmpd on

Start snmpd

/etc/rc.d/init.d/snmpd start

ps axf should give something like this :

9918 ? S 0:00 /usr/sbin/snmpd -Lsd -Lf /dev/null -p /var/run/snmpd -a

Here is an example mrtg cfg file for polling your server

Workdir: /home/httpd/stats/html/mail-servers/data
IconDir: /images
	Options[^]: growright, unknaszero
WithPeak[^]: ymw
XSize[^]: 180

##Remarked out this next line out, as it means you are only alerted when threshold is crossed
##Rather than nagged repeatedly that you are over threshold
##For us, being nagged is better I reckon ;-)
##ThreshDir: /home/httpd/stats/thresh
ThreshProgI[_]: /home/httpd/stats/mrtg-threshwarn.pl
ThreshProgOKI[_]: /home/httpd/stats/mrtg-threshwarn.pl
ThreshProgO[_]: /home/httpd/stats/mrtg-threshwarn.pl
ThreshProgOKO[_]: /home/httpd/stats/mrtg-threshwarn.pl
#----------------------------------------------------------------------
#----------------------------------------------------------------------
Target[mail.yourdomain.com.eth]: 2:yourstring@mail.yourdomain.com:
MaxBytes[mail.yourdomain.com.eth]: 1250000
AbsMax[mail.yourdomain.com.eth]: 12500000
Title[mail.yourdomain.com.eth]: Traffic Analysis for eth0 -- mail.yourdomain.com
PageTop[mail.yourdomain.com.eth]: <H1>Traffic Analysis for eth0 -- mail.yourdomain.com</H1>
Options[mail.yourdomain.com.eth]: bits, unknaszero
#----------------------------------------------------------------------
Target[mail.yourdomain.com.cpu]: .1.3.6.1.4.1.2021.10.1.3.2&.1.3.6.1.4.1.2021.10.1.3.3:yourstring@mail.yourdomain.com * 100
MaxBytes[mail.yourdomain.com.cpu]: 1000
AbsMax[mail.yourdomain.com.cpu]: 50000
Title[mail.yourdomain.com.cpu]: System Load Average for mail.yourdomain.com
PageTop[mail.yourdomain.com.cpu]: <H1> System Load Average for mail.yourdomain.com (*100) </H1>
Options[mail.yourdomain.com.cpu]: gauge, unknaszero
YLegend[mail.yourdomain.com.cpu]: Load Average
ShortLegend[mail.yourdomain.com.cpu]: load
Legend1[mail.yourdomain.com.cpu]: Load Average over 5 Minutes
LegendI[mail.yourdomain.com.cpu]: 5 min:
LegendO[mail.yourdomain.com.cpu]: 15 min:
ThreshMaxI[mail.yourdomain.com.cpu]: 8000
#----------------------------------------------------------------------
Target[mail.yourdomain.com.realmem]: .1.3.6.1.2.1.25.2.3.1.6.2&.1.3.6.1.2.1.25.2.3.1.6.2:yourstring@mail.yourdomain.com
MaxBytes[mail.yourdomain.com.realmem]: 2075988
Title[mail.yourdomain.com.realmem]: Real Memory
PageTop[mail.yourdomain.com.realmem]: <h1>Real memory on mail.yourdomain.com</h1>
Unscaled[mail.yourdomain.com.realmem]: dwmy
YLegend[mail.yourdomain.com.realmem]: Real Memory
Options[mail.yourdomain.com.realmem]: Gauge, Integer, unknaszero
#Kilo[mail.yourdomain.com.realmem]: 1024
kMG[mail.yourdomain.com.realmem]: Kb,Mb,Gb,Tb,Pb
ShortLegend[mail.yourdomain.com.realmem]:
#----------------------------------------------------------------------
Target[mail.yourdomain.com.swapmem]: .1.3.6.1.2.1.25.2.3.1.6.3&.1.3.6.1.2.1.25.2.3.1.6.3:yourstring@mail.yourdomain.com
MaxBytes[mail.yourdomain.com.swapmem]: 4096532
Title[mail.yourdomain.com.swapmem]: Swap Memory
PageTop[mail.yourdomain.com.swapmem]: <h1>Swap memory on mail.yourdomain.com</h1>
Unscaled[mail.yourdomain.com.swapmem]: dwmy
YLegend[mail.yourdomain.com.swapmem]: Swap Memory
Options[mail.yourdomain.com.swapmem]: Gauge, Integer, unknaszero
#Kilo[mail.yourdomain.com.swapmem]: 1024
kMG[mail.yourdomain.com.swapmem]: Kb,Mb,Gb,Tb,Pb
ShortLegend[mail.yourdomain.com.swapmem]:
ThreshMaxI[mail.yourdomain.com.swapmem]: 20%
#----------------------------------------------------------------------
#----------------------------------------------------------------------
Target[mail.yourdomain.com.postfix.virtual]: .1.3.6.1.4.1.2021.2.1.5.1&.1.3.6.1.4.1.2021.2.1.5.1:yourstring@mail.yourdomain.com::10
MaxBytes[mail.yourdomain.com.postfix.virtual]: 100
AbsMax[mail.yourdomain.com.postfix.virtual]: 1000
Title[mail.yourdomain.com.postfix.virtual]: mail.yourdomain.com : Postfix virtual-mailbox delivery processes
PageTop[mail.yourdomain.com.postfix.virtual]: <H1>mail.yourdomain.com : Postfix virtual-mailbox delivery processes</H1>
Options[mail.yourdomain.com.postfix.virtual]: gauge, integer
YLegend[mail.yourdomain.com.postfix.virtual]: processes
#----------------------------------------------------------------------
Target[mail.yourdomain.com.postfix.smtp]: .1.3.6.1.4.1.2021.2.1.5.2&.1.3.6.1.4.1.2021.2.1.5.2:yourstring@mail.yourdomain.com::10
MaxBytes[mail.yourdomain.com.postfix.smtp]: 100
AbsMax[mail.yourdomain.com.postfix.smtp]: 1000
Title[mail.yourdomain.com.postfix.smtp]: mail.yourdomain.com : Postfix outbound smtp processes
PageTop[mail.yourdomain.com.postfix.smtp]: <H1>mail.yourdomain.com : Postfix outbound smtp processes</H1>
Options[mail.yourdomain.com.postfix.smtp]: gauge, integer
YLegend[mail.yourdomain.com.postfix.smtp]: processes
#----------------------------------------------------------------------
Target[mail.yourdomain.com.postfix.lmtp]: .1.3.6.1.4.1.2021.2.1.5.3&.1.3.6.1.4.1.2021.2.1.5.3:yourstring@mail.yourdomain.com::10
MaxBytes[mail.yourdomain.com.postfix.lmtp]: 100
AbsMax[mail.yourdomain.com.postfix.lmtp]: 1000 
Title[mail.yourdomain.com.postfix.lmtp]: mail.yourdomain.com : Postfix smtp-amavis processes
PageTop[mail.yourdomain.com.postfix.lmtp]: <H1>mail.yourdomain.com : Postfix smtp-amavis processes</H1>
Options[mail.yourdomain.com.postfix.lmtp]: gauge, integer
YLegend[mail.yourdomain.com.postfix.lmtp]: processes
#---------------------------------------------------------------------- 
Target[mail.yourdomain.com.postfix.smtpd-mx]: .1.3.6.1.4.1.2021.2.1.5.4&.1.3.6.1.4.1.2021.2.1.5.4:yourstring@mail.yourdomain.com::10
MaxBytes[mail.yourdomain.com.postfix.smtpd-mx]: 100 
AbsMax[mail.yourdomain.com.postfix.smtpd-mx]: 1000
Title[mail.yourdomain.com.postfix.smtpd-mx]: mail.yourdomain.com : Postfix smtpd-mx processes
PageTop[mail.yourdomain.com.postfix.smtpd-mx]: <H1>mail.yourdomain.com : Postfix smtpd-mx processes</H1>
Options[mail.yourdomain.com.postfix.smtpd-mx]: gauge, integer
YLegend[mail.yourdomain.com.postfix.smtpd-mx]: processes
#----------------------------------------------------------------------
Target[mail.yourdomain.com.pop3d]: .1.3.6.1.4.1.2021.2.1.5.5&.1.3.6.1.4.1.2021.2.1.5.5:yourstring@mail.yourdomain.com::10
MaxBytes[mail.yourdomain.com.pop3d]: 100
AbsMax[mail.yourdomain.com.pop3d]: 1000
Title[mail.yourdomain.com.pop3d]: mail.yourdomain.com : pop3d processes
PageTop[mail.yourdomain.com.pop3d]: <H1>mail.yourdomain.com : pop3d processes</H1>
Options[mail.yourdomain.com.pop3d]: gauge, integer
YLegend[mail.yourdomain.com.pop3d]: processes
ThreshMaxI[mail.yourdomain.com.pop3d]: 150
#----------------------------------------------------------------------
Target[mail.yourdomain.com.imapd]: .1.3.6.1.4.1.2021.2.1.5.6&.1.3.6.1.4.1.2021.2.1.5.6:yourstring@mail.yourdomain.com::10
MaxBytes[mail.yourdomain.com.imapd]: 100
AbsMax[mail.yourdomain.com.imapd]: 1000
Title[mail.yourdomain.com.imapd]: mail.yourdomain.com : imapd processes
PageTop[mail.yourdomain.com.imapd]: <H1>mail.yourdomain.com : imapd processes</H1>
Options[mail.yourdomain.com.imapd]: gauge, integer
YLegend[mail.yourdomain.com.imapd]: processes
ThreshMaxI[mail.yourdomain.com.imapd]: 150
#----------------------------------------------------------------------
#----------------------------------------------------------------------
Target[mail.yourdomain.com.postfix.active-count]: .1.3.6.1.4.1.2021.8.1.101.1&.1.3.6.1.4.1.2021.8.1.101.1:yourstring@mail.yourdomain.com::10
MaxBytes[mail.yourdomain.com.postfix.active-count]: 1000
AbsMax[mail.yourdomain.com.postfix.active-count]: 1000000
Title[mail.yourdomain.com.postfix.active-count]: mail.yourdomain.com : Postfix active queue
PageTop[mail.yourdomain.com.postfix.active-count]: <H1>mail.yourdomain.com : Postfix active queue</H1>
Options[mail.yourdomain.com.postfix.active-count]: gauge, integer
YLegend[mail.yourdomain.com.postfix.active-count]: Messages
ThreshMaxI[mail.yourdomain.com.postfix.active-count]: 2000
#----------------------------------------------------------------------
Target[mail.yourdomain.com.postfix.incoming-count]: .1.3.6.1.4.1.2021.8.1.101.2&.1.3.6.1.4.1.2021.8.1.101.2:yourstring@mail.yourdomain.com::10
MaxBytes[mail.yourdomain.com.postfix.incoming-count]: 1000
AbsMax[mail.yourdomain.com.postfix.incoming-count]: 1000000
Title[mail.yourdomain.com.postfix.incoming-count]: mail.yourdomain.com : Postfix incoming queue
PageTop[mail.yourdomain.com.postfix.incoming-count]: <H1>mail.yourdomain.com : Postfix incoming queue</H1>
Options[mail.yourdomain.com.postfix.incoming-count]: gauge, integer
YLegend[mail.yourdomain.com.postfix.incoming-count]: Messages
ThreshMaxI[mail.yourdomain.com.postfix.incoming-count]: 2000
#----------------------------------------------------------------------
Target[mail.yourdomain.com.postfix.deferred-count]: .1.3.6.1.4.1.2021.8.1.101.3&.1.3.6.1.4.1.2021.8.1.101.3:yourstring@mail.yourdomain.com::10
MaxBytes[mail.yourdomain.com.postfix.deferred-count]: 1000
AbsMax[mail.yourdomain.com.postfix.deferred-count]: 1000000
Title[mail.yourdomain.com.postfix.deferred-count]: mail.yourdomain.com : Postfix deferred queue
PageTop[mail.yourdomain.com.postfix.deferred-count]: <H1>mail.yourdomain.com : Postfix deferred queue</H1>
Options[mail.yourdomain.com.postfix.deferred-count]: gauge, integer
YLegend[mail.yourdomain.com.postfix.deferred-count]: Messages
The mrtg-threshwarn.pl script looks like this :
#!/usr/bin/perl -w
#
# Called when MRTG detects a threshold problem for a variable.
# ARGV[0] = Parameter name, such as 'wanrouter.cpu'.
# ARGV[1] = Threshold value which was breached, such as "99".
# ARGV[2] = Actual current value of the parameter, such as "100".
#
# Command line looks like:
# thisprogram wanrouter 99[%] 100 description 100
#
my($timestr, $param, $thresh, $raw, $description, $value, $message, $logfile);
$timestr = localtime(time);
$param = $ARGV[0];
$thresh = $ARGV[1];
$raw = $ARGV[2];
$description = $ARGV[3];
$value = $ARGV[4];
$emailprog = "/usr/sbin/sendmail";
$emailuser = "admin\@yourdomain.com";
$percent = "";
$bracket = "";
if ($thresh =~ /%$/) {
$thresh = substr($thresh, 0, length($thresh)-1);
$percent = "%";
$raw = "$raw (";
$bracket = ")";
}else{
$raw = "";
if ($thresh > $value) {
$abovebelow = "below"; 
} else {
$abovebelow = "above";
}
$message = "Notice ! $param is $abovebelow threshold $thresh$percent. Current value is $raw$value$percent$bracket";

#$message .= "Notice ! $param ($value) has passed threshold ($thresh)";

system("echo 'Subject: $message' | $emailprog $emailuser");

exit(0);

OPTIONAL CHAPTERS FROM HERE ON DOWN!
THIS STUFF WILL LIKELY APPLY IF YOU ARE BUILDING A LARGER / MORE COMPLEX SERVER :-)


DEDICATED AMAVISD SERVER

It is possible to put the amavisd/clamav/spamassassin on a different box to the Postfix. This can be advantageous as although the clamav software doesn't consume many resources, the SpamAssassin software can create quite a heavy load. 

ON THE POSTFIX MACHINE :

Add an entry in the firewall that permits your amavisd machine to connect on port TCP 10025 ( Amavisd )

Add an entry in the firewall that permits your amavisd machine to connect on port TCP 3306 ( MySQL )

Allow MySQL to serve lookups to the 2nd machine

GRANT SELECT ON postfix.* TO postfixuser@offload-machines-address-here IDENTIFIED BY 'postfixpass';

Modify the MySQL so it listens for TCP connections

vi /etc/my.cnf
#skip-networking

In the /etc/postfix/main.cf, reconfigure Postfix to send the amavisd traffic to the amavisd machine

content_filter=smtp-amavis:[offload-machines-fdqn-hostname-here]:10024

# Note you could have multiple machines setup eg avs1/2/3/4.yourdomain.com,
# and a round-robin DNS name of avs.yourdomain.com that points to these boxes,
# and you could use that avs.yourdomain.com hostname in that command above.
# This will loadshare the amavisd traffic amongst your multiple servers

In the /etc/postfix/main.cf, reconfigure Postfix to listen for amavisd traffic from the amavisd machine

192.168.1.10:10025 inet n - n - - smtpd-av
  -o content_filter=
  -o smtpd_restriction_classes=
  -o smtpd_delay_reject=no
  -o smtpd_client_restrictions=permit_mynetworks,reject
  -o smtpd_helo_restrictions=
  -o smtpd_sender_restrictions=
  -o smtpd_recipient_restrictions=permit_mynetworks,reject
  -o smtpd_data_restrictions=reject_unauth_pipelining
  -o smtpd_end_of_data_restrictions=
  -o mynetworks=offload-machines-ip-or-subnet-here
  -o smtpd_error_sleep_time=0
  -o smtpd_soft_error_limit=1001
  -o smtpd_hard_error_limit=1000
  -o smtpd_client_connection_count_limit=0
  -o smtpd_client_connection_rate_limit=0
  -o smtpd_milters=
  -o local_header_rewrite_clients=
  -o local_recipient_maps=
  -o relay_recipient_maps=
  -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_address_mappings

ON THE AMAVISD MACHINE :

Follow these steps from the top of the guide

Add an entry in the firewall that permits mail.yourdomain.com to connect on port 10024

Run these commands

# Not running MySQL server on this box ( but still want the libraries ).
# Also not running apache.
chkconfig mysqld off
chkconfig httpd off
# Load postfix from RPM to allow this box to send mail ( nightly reports etc )
yum install postfix system-switch-mail
# Run this next command and choose postfix
system-switch-mail
# Ensure that you have got prerequisite libraries installed
yum install mysql-devel db4-devel db4-utils zlib zlib-devel

Follow the installation steps at the top of the guide for these applications :

Now follow the amavisd installation steps as shown above except for some tweaks shown below

In cpan, you might need to "force install DBD::mysql", otherwise it wont work because the installer tries to test a connection to the local MySQL server ( which may not succeed since our design wont need MySQL running on the amavisd machine )

In the /etc/amavisd.conf, you will need to alter these settings :

$forward_method = 'smtp:*:*';
@inet_acl = qw(127.0.0.0/8 [::1] 192.168.1.10); # adjust list as needed
$inet_socket_bind = undef; # bind to all IP interfaces if undef
@lookup_sql_dsn = ( ['DBI:mysql:database=postfix;host=mail.yourdomain.com', 'postfixuser', 'postfixpass'] );

Next is an optional tweak, which doesn't affect the operation of the server, but does fix a problem seen when people are reporting spam to SpamCop. By default amavisd calls itself localhost in the headers, which is OK when the software is running on same box as Postfix. But when you have split the two applications apart onto separate boxes, we need to use the proper hostname rather than localhost. Unless you make this change SpamCop will trip up on this header and can incorrectly identify your mail server as the source of the SPAM.

In the /etc/amavisd.conf, add this line

$localhost_name = $myhostname;

Probably the best place to put it, is below this line :

# $myhostname = 'host.example.com'; # fqdn of this host, default by uname(3)

SOME OTHER AMAVISD TIPS :

If you have a really busy server, then a single amavisd box might not be enough, you might need to run two or more. Just build them with same config, and setup a round-robin DNS entry that points to both boxes. Tweak the content_filter line in Postfix's main.cf to use the RR name. Also increase the smtp-amavis_destination_concurrency_limit setting in main.cf to a suitable amount ( eg if you have 2 dedicated amavisd machines each configured for 10 clients, then set the smtp-amavis_destination_concurrency_limit=20)

If you have gone down the road of splitting the amavisd onto a dedicated box, you are probably on the lookout for other performance enhancement tips. One suggestion is to put the amavisd tmp folder into a ram drive. Instructions available here : http://www.stahl.bau.tu-bs.de/~hildeb/postfix/amavisd_tmpfs.shtml

If you use the content_filter command in postfix's main.cf, you will be scanning all inbound + outbound mail. If you don't want to bother scanning outbound mail, you remove that content filter line and instead populate your recipient_access table with a list of locally hosted domains you want to do filtering for eg :

recipient            response
hosteddomain1.com    FILTER smtp-avavis:[amavis-hostname]:10024
hosteddomain2.com    FILTER smtp-avavis:[amavis-hostname]:10024
hosteddomain3.com    FILTER smtp-avavis:[amavis-hostname]:10024

HORDE SUITE

We already have a webmail package installed ( sqwebmail ). However sqwebmail only has a very functionality. Many times you will have users who want something more powerful. This is where the Horde suite can help. Its takes a bit of work to get it installed, but the results are worth the effort.

Install the prerequisite modules

yum install php php-devel php-mysql php-imap php-xml php-mbstring php-gd php-mycrypt enscript
pear install -o Log Mail Mail_Mime DB Date File Net_URL Net_Sieve Net_Socket HTTP_Request Fileinfo
# Gotcha with above! The Fileinfo tries to compile in /tmp, which our tweaked fstab disallows.
# Workaround is to remove the nosuid,noexec from fstab, and to mount -o remount /tmp before install,
#  and then put it back again afterwards.
# Would be good if we could come up with something more graceful 
# eg, can we perhaps export TEMP variable or similar to point other than /TMP for this install
# Then I would recommend you run the following to bring all your pear modules up to date
pear upgrade Archive_Tar
pear upgrade PEAR
# If the above command fails saying it requires PEAR-1.3.3, then type "pear upgrade PEAR-1.3.3" and then run above command again
pear channel-update pear.php.net
pear upgrade-all

Install the optional wvHhtml tool, so IMP can render MS Word docs as HTML :

On Fedora you can just do this :

yum install wv

On CentOS its a bit more work involved, you need to do this :

yum install zlib zlib-devel libpng libpng-devel
cd /usr/local/src
wget http://optusnet.dl.sourceforge.net/sourceforge/wvware/wv-1.0.3.tar.gz
tar xzf wv-1.0.3.tar.gz
chown -R root.root wv-1.0.3
cd wv-1.0.3
./configure
make
make install

Suitably tweak the PHP configuration

vi /etc/php.ini
# in the "Dynamic Extensions" part, enter this :
extension=fileinfo.so
# find and tweak these values to allow for large webmail uploads
max_execution_time = 3600
memory_limit = 64M
post_max_size = 18M
file_uploads = On
upload_max_filesize = 17M
killall -HUP httpd

HORDE APPLICATION FRAMEWORK

cd /usr/local/src
wget ftp://ftp.planetmirror.com/pub/horde/horde/horde-3.1.1.tar.gz
cd /var/www/mail/html
tar xzf /usr/local/src/horde-3.1.1.tar.gz
chown -R root.apache horde-3.1.1
chmod -R o-rwx horde-3.1.1
mv horde-3.1.1 horde
cd horde
cd scripts/sql
vi create.mysql.sql
# change this line :
PASSWORD('hordepass')
mysql -u root < create.mysql.sql
cd ../..
cd config
for f in *.dist; do cp $f `basename $f .dist`; done
cd ..
chown -R root.apache config
chmod -R g+rw config
cd /var/www/mail/html/images
# grab a copy of your logo file needs to be max 140px wide and 40px high
wget http://somewhere/images/yourdomain.com-smalllogo.gif 
# grab a copy of your larger logo file
wget http://somewhere/images/yourdomain.com-largelogo.gif 
http://mail.yourdomain.com/horde/test.php
http://mail.yourdomain.com/horde/
click on Administration->Setup
In the Application screen, click on Horde
Database ->
What database backend : MySQL
Request persistent connections : ticked
Database server/host : localhost
Username to connect to the database as : horde
password to connect with : hordepass
database name to use : horde
Preference system ->
Preference driver : SQL Database
DataTree System ->
Backend : SQL Database
Mailer ->
The location of sendmail binary : /usr/sbin/sendmail
Virtual File Storage ->
Backend : SQL Database
Custom sessions handler ->
Sessionhandler : MySQL based sessions
Request persistent connections : ticked
Row level locking : ticked
Database server/host : localhost
Username to connect to the database as : horde
password to connect with : hordepass
database name to use : horde
MIME Detection ->
location : /usr/share/misc/magic
Problem Reporting -> 
whre should problem reports be sent : support@yourdomain.com
Menu Settings ->
Select applications to be linked to Hordes menu : imp, ingo, kronolith, turba
Display problem reporting link : Never
URL of an image for top of horde menu : /images/yourdomain.com-smalllogo.gif
If logo is displayed, what URL should it link to : www.yourdomain.com
Click on generate horde config

If you installed the wvHtml package from source ( CentOS ) rather than RPM ( Fedora), then you need to fix the path Horde uses for this tool :

vi /var/www/mail/html/horde/config/mime_drivers.php
$mime_drivers['horde']['msword']['location'] = '/usr/local/bin/wvHtml';

Now, regardless of whether you are using CentOS or Fedora, there are two other Horde helper tools "xlhtml" and "ppthtml" that need to be disabled. Although these two tools sound useful ( convert Excel and Powerpoint files to HTML for viewing), you cant get them via Yum and the source fails to compile on most boxes I have tried. Also there are two other drivers "webcpp" and "srchighlite" which are only for fairly obscure use and these tools dont exist on our machine so we want to disable them as well :

vi /var/www/mail/html/horde/config/mime_drivers.php
# up towards the top of the file there is a section that looks like this :
$mime_drivers_map['horde']['registered'] = array(
# need to remove the msexcel, mspowerpoint, srchighlite and webcpp entries from this array

IMP WEBMAIL

http://www.horde.org/imp/

cd /usr/local/src
wget ftp://ftp.planetmirror.com/pub/horde/imp/imp-h3-4.1.1.tar.gz
cd /var/www/mail/html/horde
tar xzf /usr/local/src/imp-h3-4.1.1.tar.gz
chown -R root.apache imp-h3-4.1.1
chmod -R o-rwx imp-h3-4.1.1
mv imp-h3-4.1.1 imp
cd imp
cd config/
for foo in *.dist; do cp $foo `basename $foo .dist`; done
vi servers.php
# update the IMAP server info. Replace all existing samples with this :
$servers['imap'] = array(
    'name' => 'Courier IMAP Server',
    'server' => 'localhost',
    'hordeauth' => false,
    'protocol' => 'imap/notls',
    'port' => 143,
    'smtphost' => 'localhost',
    'realm' => '',
    'preferred' => '',
    'dotfiles' => false,
    'quota' => array (
        'driver' => 'courier',
        'params' => array()
    ),
    'hierarchies' => array()
);
vi prefs.php
$_prefs['sent_mail_folder'] = array(
    'value' => 'Sent',
$_prefs['drafts_folder'] = array(
 'value' => 'Drafts',
$_prefs['trash_folder'] = array(
 'value' => 'Trash',
vi header.php
// Add the IP of the remote browser
$_header['X-Originating-IP'] = $_SERVER['REMOTE_ADDR'];
cd ..
chown -R root.apache config
chmod -R g+rw config
vi templates/login/login.inc
replace 
  echo _("Username")
with
  echo _("Email address")
http://mail.yourdomain.com/horde/imp/test.php
http://mail.yourdomain.com/horde/
click on Administration->Setup
In the Application menu, click on Mail (imp)
External Utilities and Menu
Location of aspell : /usr/bin/aspell
Applications that should be linked to IMPs menu : imp, ingo, koronlith, turba
Compose
Should we append the contents of imp/config/trailer.txt : unticked
Can the user request a return receipt : unticked
send attachments as links : no
maximum size of attachments : 17000000
Click on Generate Mail Configuration

TURBA ADDRESS BOOK

cd /usr/local/src
wget ftp://ftp.planetmirror.com/pub/horde/turba/turba-h3-2.1.tar.gz
cd /var/www/mail/html/horde
tar xzf /usr/local/src/turba-h3-2.1.tar.gz
chown -R root.apache turba-h3-2.1
chmod -R o-rwx turba-h3-2.1
mv turba-h3-2.1 turba
cd turba
cd config/
for foo in *.dist; do cp $foo `basename $foo .dist`; done
vi sources.php
remove all sources except the localsql one
cd ..
chown -R root.apache config
chmod -R g+rw config
cd scripts/sql
mysql horde < turba_objects.mysql.sql
cd ../..
http://mail.yourdomain.com/horde/turba/test.php
http://mail.yourdomain.com/horde/
click on Administration->Setup
In the Application menu click on Address Book ( turba)
Select any applications that should be linked to turbas menu : imp, ingo, kronolith, turba
Name of source for creating new shares : localsql
Click on Generate Address book configuration
 

KRONOLITH CALENDER

cd /usr/local/src
wget ftp://ftp.planetmirror.com/pub/horde/kronolith/kronolith-h3-2.1.1.tar.gz
cd /var/www/mail/html/horde
tar xzf /usr/local/src/kronolith-h3-2.1.1.tar.gz
chown -R root.apache kronolith-h3-2.1.1
chmod -R o-rwx kronolith-h3-2.1.1
mv kronolith-h3-2.1.1 kronolith
cd kronolith
cd config/
for foo in *.dist; do cp $foo `basename $foo .dist`; done
cd ..
chown -R root.apache config
chmod -R g+rw config
cd scripts/sql
mysql horde < kronolith.mysql.sql
cd ../..
http://mail.yourdomain.com/horde/
click on Administration->Setup
In the Application menu, click on Calendar (Kronolith)
Server name from which reminders are sent : mail.yourdomain.com
Email address from which reminders are send : reminder@mail.yourdomain.com
Applications that should be linked to the Konolith menu : imp, ingo, kronolith, turba
Click on Generate Calender Configuration

INGO FILTERS

cd /usr/local/src
wget ftp://ftp.planetmirror.com/pub/horde/ingo/ingo-h3-1.1.tar.gz
cd /var/www/mail/html/horde
tar xzf /usr/local/src/ingo-h3-1.1.tar.gz
chown -R root.apache ingo-h3-1.1
chmod -R o-rwx ingo-h3-1.1
mv ingo-h3-1.1 ingo
cd ingo
cd config/
for foo in *.dist; do cp $foo `basename $foo .dist`; done
cd ..
chown -R root.apache config
chmod -R g+rw config
http://mail.yourdomain.com/horde/ingo/test.php
http://mail.yourdomain.com/ingo/
click on Administration->Setup
In the Application menu, click on Filters (Ingo)
Select applictions that should be linked to ingo's menu : imp, ingo, kronolith, turba
Click on Generate Filters configuration

 

You can optionally add a couple more modules, but on the servers I build, I don't use these extra modules :

NAG REMINDERS

cd /usr/local/src
wget ftp://ftp.planetmirror.com/pub/horde/nag/nag-h3-2.1.tar.gz
cd /var/www/mail/html/horde
tar xzf /usr/local/src/nag-h3-2.1.tar.gz
chown -R root.apache nag-h3-2.1
chmod -R o-rwx nag-h3-2.1
mv nag-h3-2.1 nag
cd nag
cd config/
for foo in *.dist; do cp $foo `basename $foo .dist`; done
cd ..
chown -R root.apache config
chmod -R g+rw config
cd scripts/sql
mysql -p horde < nag.sql
cd ../..
http://mail.yourdomain.com/horde/nag/test.php
http://mail.yourdomain.com/horde/
click on Administration->Setup
In the Application menu, click on Tasks (Nag)
Click on Generate Reminders configuration

MNEMO NOTES

cd /usr/local/src
wget ftp://ftp.planetmirror.com/pub/horde/mnemo/mnemo-h3-2.1.tar.gz
cd /var/www/mail/html/horde
tar xzf /usr/local/src/mnemo-h3-2.1.tar.gz
chown -R root.apache mnemo-h3-2.1
chmod -R o-rwx mnemo-h3-2.1
mv mnemo-h3-2.1 mnemo
cd mnemo
cd config/
for foo in *.dist; do cp $foo `basename $foo .dist`; done
cd ..
chown -R root.apache config
chmod -R g+rw config
cd scripts/sql
mysql -p horde < mnemo.sql
cd ../..
http://mail.yourdomain.com/horde/mnemo/test.php
http://mail.yourdomain.com/horde/
click on Administration->Setup
In the Application menu, click on Notes (Mnemo)
Click on Generate Notes configuration

Now that all the modules are installed and setup, lets reconfigure the Horde module to authenticate users via IMP

http://mail.yourdomain.com/horde
Clock on Administration -> Setup
In the Application menu, click on Horde
Authentication ->
Which users should be treated as administrators : someadminemail@yourdomain.com
# NOTE, the address above should be a valid email account that will be hosted on this box.
# When you login into webmail as this user, you will be given access to all the admin
# menus which lets you change the config of the horde suite.
What backend for authenticating : let a horde application handle authentication
The application that is providing authentication : imp
Click on Generate Horde Configuration
you get a nasty forbidden message at this point, because you have just killed off the previous administrator user admin rights

And lets tweak the interface defaults a bit

vi /var/www/mail/html/horde/templates/common-footer.inc
# Add this to the top of the file
<!-- CUSTOM BRANDING TWEAKS -->
<p><center><b>yourdomain.com support - call 1300 xxx xxx</b></center></p>
<!-- CUSTOM BRANDING TWEAKS -->
vi /var/www/mail/html/horde/config/prefs.php
$_prefs['language'] = array(
'value' => 'en_GB',
$_prefs['timezone'] = array(
'value' => 'Australia/Melbourne',
$_prefs['date-format'] = array (
    'value' => '%Y-%m-%d',
$_prefs['show_sidebar'] = array (
    'value' => false,
$_prefs['initial_application'] = array( 
    'value' => 'imp',
vi /var/www/mail/html/horde/imp/login.php
#Replace this line
// $title = sprintf(_("Welcome to %s"), $registry->get('name', ($imp_auth) ? 'horde' : null));
#With this line
$title = sprintf(_("%s Webmail"), _(preg_replace('/^mail\.|^webmail\./', '', $GLOBALS['_SERVER']['SERVER_NAME'])) );
vi /var/www/mail/html/horde/imp/config/motd.php
# replace the existing table with 
<table width="100%"><tr><td align="center"><img src="/images/yourdomain.com-largelogo.gif" alt="yourdomain.com" /></td></tr></table>
vi /var/www/mail/html/horde/imp/config/prefs.php
$_prefs['purge_trash'] = array(
'value' => 1,
$_prefs['purge_trash_interval'] = array(
 'value' => '3',
$_prefs['purge_trash_keep'] = array(
'value' => 7,
$_prefs['fetchmail_menu'] = array(
'value' => 0,
$_prefs['mailbox_start'] = array(
'value' => IMP_MAILBOXSTART_LASTUNSEEN,
$_prefs['sortby'] = array(
'value' => SORTDATE,
$_prefs['sortdir'] = array(
'value' => 1,
$_prefs['filter_on_display'] = array(
'value' => 1,
# This next one you can choose whether you do it or not...
# If you add this tweak, then HTML mails will display inline, rather than requiring the user
# to click on the attachment link.
# Displaying inline can be a security issue, however you have to juggle this against
# helpdesk load that you will suffer from users who complain about not being able to read
# their html mail. 
vi /var/www/mail/html/horde/imp/config/mime_drivers.php
$mime_drivers['imp']['html']['inline'] = true;
vi /var/www/mail/html/horde/imp/templates/login/login.inc
#Search for input box name="imapuser" add size=32 as property of input box.
#Since the default box is a bit too small and can cause users confusion 
#when they arent able to spot typos that have scrolled off the screen

SETTING A DEFAULT IMAP/POP3 DOMAIN PER HOSTNAME

You can bind multiple IPs to your server (one per domain) and set "default" POP3/IMAP domain for each IP :

Probably you first should remove any DEFAULT_DOMAIN entry from the /usr/local/courier-authlib/etc/authdaemonrc

vi /usr/local/courier-imap/etc/imapd
TCPDOPTS="-nodnslookup -noidentlookup -access=/usr/local/courier-imap/etc/default-domains.db -accesslocal"
vi /usr/local/courier-imap/etc/pop3d
TCPDOPTS="-nodnslookup -noidentlookup -access=/usr/local/courier-imap/etc/default-domains.db -accesslocal"
vi /usr/local/courier-imap/etc/default-domains
# A set of mappings for ip addresses to default domains
# These mappings are used by courier-imap, and courier-pop3d
##
## YOU MUST USE A TAB BETWEEN THE IP AND THE RESULT
## SPACES WILL NOT WORK! AUTH WILL FAIL WITH A TEMP ERROR
##
192.168.1.101<TAB>allow,DEFDOMAIN=@domain1.com
192.168.1.102<TAB>allow,DEFDOMAIN=@domain2.com
192.168.1.103<TAB>allow,DEFDOMAIN=@domain3.com
( cat /usr/local/courier-imap/etc/default-domains; echo "." ) \
| /usr/local/courier-imap/libexec/makedatprog - /usr/local/courier-imap/etc/default-domains.tmp /usr/local/courier-imap/etc/default-domains.db

And from my experience, you have to restart courier-imap for the changes to take effect after making any changes to this file

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

SETTING A DEFAULT SQWEBMAIL DOMAIN PER HOSTNAME

You can bind multiple IPs to your server (one per domain) and set a "default" sqwebmail domain for each IP

The config below would look to see what IP the client connected to, and would pre-populate the domain name part of the login box

vi /usr/local/sqwebmail/etc/logindomainlist
# A set of mappings for ip addresses to default domains
domain1.com:192.168.1.101:@
domain2.com:192.168.1.102:@
domain3.com:192.168.1.103:@

SETTING A DEFAULT SMTP-AUTH DOMAIN PER HOSTNAME

When you configure the SASL ( SMTP-AUTH) settings in postfix's main.cf, you can nominate a default domain to use should the SMTP user not supply one (smtpd_sasl_local_domain = yourdomain.com ). However when you are hosting multiple virtual domains on the one box, unless the vast bulk of your users are from a single domain, you really need a way to append the correct domain.

You can bind multiple IPs to your server (one per domain), and then set the smtpd_sasl_local_domain inside the master.cf on a per-smtpd instance basis.

eg Change your master.cf from this ( as described earlier in doc )

127.0.0.1:smtp    inet n - n - 10 smtpd
192.168.1.11:smtp inet n - n - 50  smtpd-mx
   -o smtpd_sasl_auth_enable=no
192.168.1.10:smtp inet n - n - 100 smtpd

To something like this :

127.0.0.1:smtp    inet n - n - 10 smtpd
192.168.1.11:smtp inet n - n - 50 smtpd-mx
   -o smtpd_sasl_auth_enable=no
192.168.1.101:smtp  inet n - n - 30 smtpd-domain1
   -o smtpd_sasl_local_domain=domain1.com
192.168.1.102:smtp  inet n - n - 30 smtpd-domain2
   -o smtpd_sasl_local_domain=domain2.com
192.168.1.103:smtp  inet n - n - 30 smtpd-domain3
   -o smtpd_sasl_local_domain=domain3.com

And don't forget to setup your symlinks

cd /usr/libexec/postfix
ln -s smtpd smtpd-domain1
ln -s smtpd smtpd-domain2
ln -s smtpd smtpd-domain3

Update your reverse DNS entries

$ORIGIN 1.168.192.in-addr.arpa.
11	PTR	mail-mx.yourdomain.com.
101	PTR	mail.domain1.com.
102	PTR	mail.domain2.com.
103	PTR	mail.domain3.com.

SETTING A DEFAULT PURE-FTPD DOMAIN PER HOSTNAME

You can set a default login doman domain based on the IP address the user has connected to

Create a IP <-> Domain mapping table :

mysql
USE postfix;
CREATE TABLE domain_ips (
ip varchar(15) not NULL,
domain varchar(255) NOT NULL,
PRIMARY KEY (ip)
) TYPE=MyISAM COMMENT='IP to Domain mappings for Pure-ftpd';
INSERT INTO domain_ips (ip, domain) VALUES ('192.168.1.101', 'domain1.com');
INSERT INTO domain_ips (ip, domain) VALUES ('192.168.1.102', 'domain2.com');
INSERT INTO domain_ips (ip, domain) VALUES ('192.168.1.103', 'domain3.com');
quit

Tweak the pure-ftpd config :

vi /etc/pureftpd-mysql.conf
MYSQLGetPW SELECT clear_password FROM mailbox WHERE (("\L" LIKE '%@%' AND email = "\L")  OR  ("\L" NOT LIKE '%@%' AND email = CONCAT("\L",'@',(select domain from domain_ips where ip = "\I")))) AND disableftp=0
MYSQLGetDir SELECT CONCAT('/var/vmail/',maildir,'public_html') FROM mailbox WHERE (("\L" LIKE '%@%' AND email = "\L")  OR  ("\L" NOT LIKE '%@%' AND email = CONCAT("\L",'@',(SELECT domain FROM domain_ips WHERE ip = "\I")))) and disableftp=0
MySQLGetQTASZ SELECT ftpquota FROM mailbox WHERE (("\L" LIKE '%@%' AND email = "\L")  OR  ("\L" NOT LIKE '%@%' AND email = CONCAT("\L",'@',(SELECT domain FROM domain_ips WHERE ip = "\I")))) AND disableftp=0

WHAT TO DO IF YOU DONT HAVE CLEAR PASSWORDS AVAILABLE

If you don't have clear passwords ( eg you are migrating a server where you only have /etc/passwd to work with) you have a couple of choices.

First would be to try cracking the passwords. If you get lucky you can crack a very good percentage of them using a tool like "John the Ripper password cracker".

If you have no luck cracking the passwords then you might need to tweak your mailserver config as follows :

Mod your SASL config to use courier-authlib (rather than talking to the MySQL database directly), and to remove CRAM AUTH options ( which require clear password to be available )

vi /usr/lib/sasl2/postfix.conf
# replace all the existing commands with this new block :
pwcheck_method: authdaemond
authdaemond_path: /usr/local/courier-authlib/var/spool/authdaemon/socket
mech_list: plain login
TIP : If you are using x86_64 platform ( eg Opteron ), this file will be found in a different place:
/usr/lib64/sasl2/postfix.conf

Add postfix to the daemon group, and tweak the directory permissions slightly, so that postfix applications can gain access to the courier-authlib socket

usermod -G daemon postfix
chown root.daemon /usr/local/courier-authlib/var/spool

Mod your courier-authlib config, so that it doesn't try to access the clear passwords column:

vi /usr/local/courier-authlib/etc/authlib/authmysqlrc
#MYSQL_CLEAR_PWFIELD clear_password
/etc/rc.d/init.d/courier-authlib restart

Mod your courier-imap/pop3d config to remove any of the CRAM auth options (which require any clear passwords to be available )

vi /usr/local/courier-imap/etc/imapd
IMAP_CAPABILITY="IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA AUTH=PLAIN AUTH=LOGIN IDLE"
vi /usr/local/courier-imap/etc/pop3d
POP3AUTH="PLAIN LOGIN"
/etc/rc.d/init.d/courier-imap restart

Mod your pure-ftpd config to look at your crypted password data, rather than clear password data:

vi /etc/pureftpd-mysql.conf
MYSQLCrypt any
# and then go to the MYSQLGetPW line and change any reference of clear_password to password

HOSTING MULTIPLE SSL DOMAINS FOR COURIER-IMAP/POP3D

Each hostname must have a dedicated IP address

Create a PEM file per hostname.

Save each of them to /usr/local/ssl as

$CERTFILE.192.168.1.101
$CERTFILE.192.168.1.102
$CERTFILE.192.168.1.103

Where $CERTFILE is the name you used for the $CERTFILE setting in imapd-ssl and pop3d-ssl conf files

Courier will look for a certfile with matching ip. If it doesn't find one, it will fallback to trying to use "somecert"


HELO FILTERING FOR POSTFIX

We have found that spammers will often forge the SMTP HELO address when they make a connection to Postfix. It seems fairly common for them to use the hostname or IP address of your server.

To block such spam, first create a list of hostnames and IPs used by your server :

vi /etc/postfix/helo.access
## Deny connections from people forging our hostnames
mail.yourdomain.com		REJECT You are not me
mail-mx.yourdomain.com		REJECT You are not me
mail.domain1.com		REJECT You are not me
mail.domain2.com		REJECT You are not me
mail.domain3.com		REJECT You are not me
domain1.com			REJECT Use of that helo name is not permitted
domain2.com			REJECT Use of that helo name is not permitted
domain3.com			REJECT Use of that helo name is not permitted
## Deny connections from people forging our IP
192.168.1.10			REJECT You are not me
192.168.1.11			REJECT You are not me
192.168.1.101			REJECT You are not me
192.168.1.102			REJECT You are not me
192.168.1.103			REJECT You are not me
postmap /etc/postfix/helo.access

Then tell Postfix to do helo filtering (if you uncomment the warn_if_reject line, hits will be logged but not actually rejected - which can be good for initial testing ) :

vi /etc/postfix/main.cf 
smtpd_helo_required = yes
smtpd_helo_restrictions =
	permit_mynetworks,
	check_helo_access hash:/etc/postfix/helo.access,
	# warn_if_reject,
	reject_invalid_helo_hostname,
	permit
postfix reload

To check for hits, try

tail -f /var/log/maillog | grep "Helo command rejected"

The RFC's state that you aren't meant to reject mail based on the data presented in the helo command, however after much analysis of our logs we have found that all the hits we are getting are SPAM. I have yet to see any legit users be affected by this piece of config, so I am satisfied that it is a safe and worthwhile mod. If you are unsure, then uncomment the warn_if_reject line and inspect the results before going live.


HOSTING MULTIPLE SSL DOMAINS FOR APACHE / SQWEBMAIL


HOSTING MULTIPLE SSL DOMAINS FOR POSTFIX

In one of the sections above, I show how to set multiple smtp-auth domains for Postfix. The same technique can be used to set multiple SSL domains.

Don't set a default global SSL domain in in main.cf

smtpd_tls_key_file =
smtpd_tls_cert_file =

Set use them as -o entries instead in master.cf eg

192.168.1.101:smtp  inet n - n - 30 smtpd-domain1
   -o smtpd_tls_key_file=/usr/local/ssl/mail.domain1.com.key
   -o smtpd_tls_cert_file=/usr/local/ssl/mail.domain1.com.crt
192.168.1.102:smtp  inet n - n - 30 smtpd-domain2
   -o smtpd_tls_key_file=/usr/local/ssl/mail.domain2.com.key
   -o smtpd_tls_cert_file=/usr/local/ssl/mail.domain2.com.crt
192.168.1.103:smtp  inet n - n - 30 smtpd-domain3
   -o smtpd_tls_key_file=/usr/local/ssl/mail.domain3.com.key
   -o smtpd_tls_cert_file=/usr/local/ssl/mail.domain3.com.crt

HAVE TO TEST THIS STILL


WHAT TO DO IF YOU DONT WANT TO USE SSL

Some mail servers will not need SSL functionality enabled. Usually due cost savings that can be made particularly if you are hosting a large number of domains. You can self-sign certs, but that is usually only suitable for testing. In production your users will complain about the security warnings generated by their mail client / web browser.

Disable SSL in Postfix

vi /etc/postfix/master.cf

remark out the smtps service (and any associated -o arguments)

vi /etc/postfix/main.cf
smtpd_use_tls = no
postfix reload

Disable SSL in courier-imap / courier-pop3d

vi /usr/local/courier-imap/etc/imapd-ssl
IMAPDSSLSTART=NO
IMAPDSTARTTLS=NO
vi /usr/local/courier-imap/etc/imapd-ssl
POP3DSSLSTART=NO
POP3_STARTTLS=NO
/etc/rc.d/init.d/courier-imap restart

Disable SSL in Apache

INSTRUCTIONS TO BE ADDED


FUZZYOCR

http://fuzzyocr.own-hero.net/wiki/Downloads 

This is a plugin for SpamAssassin that uses OCR tools to grab the words from inside those annoying image-based spams

NOTE : I have been tinkering with this on a few different machines. It seems to run OK on machines that have only light load, but when I try to run it on some of our busier equipment, I am seeing problems with crashed spamd or amavisd processes. Have tried tinkering with different package versions, patches, source vs RPM install without much success. So it seems the concept is a good one, but maybe we wont be able to use it in busy production environment until some new builds of the software are available.

Install gifsicle application ( a tool which we will use to get info about GIFs )

cd /usr/local/src
wget http://www.lcdf.org/gifsicle/gifsicle-1.44.tar.gz
tar xzf gifsicle-1.44.tar.gz
chown -R root.root gifsicle-1.44
cd gifsicle-1.44
./configure --disable-gifview
make
make install

Install gOCR prerequisite modules, which do things like convert gif/jpg/png/tiff to pbm format

# Tools to convert gif/jpg/png/tiff files to pnm "portable anymaps" format
yum install netpbm netpbm-progs netpbm-devel
# Now we need to install some more tools :
# * giffix tool which allows us to intentionally corrupted GIFs
# * giftext tool which dumps text info about a GIF file ( which has a bug and needs to be patched )
# Although these are available as RPM's, we cant use them as the giftext program contains
# a bug which can cause segfaults. So instead we need to grab the source, patch it and then 
# compile/install
yum remove libungif libungif-progs
cd /usr/local/src
wget http://optusnet.dl.sourceforge.net/sourceforge/libungif/giflib-4.1.4.tar.gz
tar xzf giflib-4.1.4.tar.gz
chown -R root.root giflib-4.1.4
cd giflib-4.1.4
cd util 
wget http://users.own-hero.net/~decoder/fuzzyocr/giftext-segfault.patch 
patch < giftext-segfault.patch
cd ..
./configure
make
make install

Install the gOCR application (which reads pnm archives)

# grab the gOCR source ( FuzzyOcr author recommends 0.40, not 0.41)
cd /usr/local/src
wget http://optusnet.dl.sourceforge.net/sourceforge/jocr/gocr-0.40.tar.gz 
tar xzf gocr-0.40.tar.gz
chown -R root.root gocr-0.40
cd gocr-0.40
# Apply patch for a bug thats been identified
wget http://users.own-hero.net/~decoder/fuzzyocr/gocr-segfault.patch 
patch -p0 < gocr-segfault.patch
./configure
# TIP: On CentOS, but not Fedora, the configure script experiences a failed dependency on the maths library,
# which causes the netpbm libraries to be rejected :
#     checking for library containing pnm_readpnminit... no
#     * * * try option --with-netpbm=PATH
# The workaround is to use this command instead :
./configure LIBS=-lm
make
make install

Install the ocradd application ( which reads pbm formats, and outputs text. Its similar to gOCR, and can be used to give "2nd opinion")

# Some of the boxes I was experimenting with needed this
# (although I assume it should already been installed if you ticked development tools as per my doco)
yum install gcc-c++
cd /usr/local/src
wget ftp://gnu.mirror.pacific.net.au/gnu/gnu/ocrad/ocrad-0.16.tar.bz2
tar xjf ocrad-0.16.tar.bz2
chown -R root.root ocrad-0.16
cd ocrad-0.16
./configure
make
make install

Install some perl modules used by the FuzzyOcr SpamAssassin plugin

perl -MCPAN -e shell
  o conf prerequisites_policy follow
  install String::Approx MLDBM
  quit

Install the FuzzyOcr plugin for SpamAssassin

# grab the source
cd /usr/local/src
wget http://users.own-hero.net/~decoder/fuzzyocr/fuzzyocr-3.4.2-devel.tar.gz 
tar xzf fuzzyocr-3.4.2-devel.tar.gz
chown -R root.root FuzzyOcr-3.4.2
cd FuzzyOcr-3.4.2
cp FuzzyOcr.pm /etc/mail/spamassassin
cp FuzzyOcr.cf /etc/mail/spamassassin
echo "### MAKE SURE YOU DONT PUT ANY BLANK LINES IN HERE ###" > /etc/mail/spamassassin/FuzzyOcr.words
echo "###" >> /etc/mail/spamassassin/FuzzyOcr.words
echo "### * Matches are case insentive" >> /etc/mail/spamassassin/FuzzyOcr.words
echo "### * All special chars, spaces or numbers are stripped before any matching is done" >> /etc/mail/spamassassin/FuzzyOcr.words
echo "### * Your wordlist word will be found even if its inside another word ( submatching )" >> /etc/mail/spamassassin/FuzzyOcr.words
cat FuzzyOcr.words.sample >> /etc/mail/spamassassin/FuzzyOcr.words
echo "goldmark" >> /etc/mail/spamassassin/FuzzyOcr.words
echo "gdki" >> /etc/mail/spamassassin/FuzzyOcr.words
echo "l intl computers inc" >> /etc/mail/spamassassin/FuzzyOcr.words
echo "metropolis technologies" >> /etc/mail/spamassassin/FuzzyOcr.words
echo "### MAKE SURE YOU DONT PUT ANY BLANK LINES IN HERE ###" >> /etc/mail/spamassassin/FuzzyOcr.words
touch /etc/mail/spamassassin/FuzzyOcr.log
chown clamav.clamav /etc/mail/spamassassin/FuzzyOcr.log
touch /etc/mail/spamassassin/FuzzyOcr.db
chown clamav.clamav /etc/mail/spamassassin/FuzzyOcr.db
touch /etc/mail/spamassassin/FuzzyOcr.safe.db
chown clamav.clamav /etc/mail/spamassassin/FuzzyOcr.safe.db
vi /etc/mail/spamassassin/FuzzyOcr.cf
focr_logfile /etc/mail/spamassassin/FuzzyOcr.log
focr_bin_gifsicle /usr/local/bin/gifsicle
focr_bin_giffix /usr/local/bin/giffix
focr_bin_giftext /usr/local/bin/giftext
focr_bin_gifinter /usr/local/bin/gifinter
focr_bin_gocr /usr/local/bin/gocr
focr_bin_ocrad /usr/local/bin/ocrad
focr_scansets $gocr -i $pfile, $gocr -l 180 -d 2 -i $pfile, $ocrad -s5 -T 0.5 $pfile
focr_threshold 0.2
focr_add_score 0.25
focr_counts_required 3
focr_enable_image_hashing 2
focr_db_hash /etc/mail/spamassassin/FuzzyOcr.db
focr_db_safe /etc/mail/spamassassin/FuzzyOcr.safe.db

Test to see if your installation is working properly :

cd samples
spamassassin -t < png.eml
spamassassin -t < jpeg.eml
spamassassin -t < animated-gif.eml
spamassassin -t < corrupted-gif.eml

If you have been running Amavisd/Spamassassin for a while before installing this plugin, I think its a good idea to zap your existing auto-whitelist and bayes database files so that they will be rebuilt more accurately now we have OCR capabilities :

/etc/rc.d/init.d/amavisd stop
cd ~clamav/.spamassassin
rm *
/etc/rc.d/init.d/amavisd start

MISC NOTES :


TODO :


Back to Michael's ISP Links page

Last updated 03-May-2007
Please send me your feedback!



ChangeLog :

3rd May :

2nd May :

24th April :

13th April :

2nd April :

19th March :

15th March :

5th March :

19th February :

14th February :

9th February :

7th February :

5th February :

2nd February :

29th January :

25th January 2007 :

15th December :

6th December :

27th November :

7th November :

30th October :

27th October :

26th October :

19th September :

13th September :

8th September :

4th September :

31st August 2006 :

18th August 2006 :

8th August 2006 :

4th August 2006 :

1st August 2006 :

28th July 2006

19th July 2006

14th July 2006

3rd July 2006

29th June 2006

22nd June 2006

15th June 2006 :

14th June 2006 :

9th June 2006 :

26th May 2006 :

25th May 2006

19th May 2006