Linux as a Server

Install Ubuntu 9.04 Server

This tutorial is based on Ubuntu 9.04, i386-based 32bit Server edition. It should work just as good on 64bit editions and other architectures, although at some point you might need to install more dependencies. I don't think that's a problem though ;)

Install ssh and set the firewall

Login to your new Ubuntu installation and install the packages we will need.
Go to root - we will be doing most stuff as root

sudo su -
Now, you can either use the most up to date standard repository, or if that fails due to some broken dependencies, you can use my repository. My one contains only the packages that are in this tutorial (and their dependencies). The snapshot is from 30th Jul 2009 4:00am. To use my repository do the following:
vi /etc/apt/sources.list
 # comment out all deb sources. I put three ### in front to know which onces I commented out
 # then put this line just above the first deb http:// (which should now be commented out)
deb http://glonek.co.uk/repo/ jaunty main

 # close the file and exit editor
 # now we need to run the below to get the new list of packages
aptitude update
Update the Ubuntu install
aptitude update && aptitude upgrade
Install ssh, wget and vim, lynx
aptitude install openssh-server openssh-client wget vim lynx
Configure the firewall to only allow remote logins (SSH) from all source IPs. Save the configuration to a script so that it will restore on reboot. We need to finish this step for sshdfilter rules to work (next step)
iptables -N SSHD
iptables -A INPUT -m state --state established,related -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 22 -j SSHD
iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
iptables -A INPUT -j DROP
 # if you want to add an exception to sshd blocking rules (e.g. to your local network)
 # you would then execute this command line (assuming 192.168.0.0/16 is your local network)
iptables -I INPUT -p tcp -m tcp -s 192.168.0.0/16 --dport 22 -j ACCEPT
 # in case you added a rule you wish to remove, execute it with -D instead. Example:
iptables -D INPUT -p tcp -m tcp -s 192.168.0.0/16 --dport 22 -j ACCEPT
Now, let's make the rules permanent
iptables-save > /etc/iptables-rules
vi /etc/init.d/networking
Find this line:
start)
        process_options

        log_action_begin_msg "Configuring network interfaces"
        if ifup -a; then
            log_action_end_msg $?
        else
            log_action_end_msg $?
        fi
        ;;
And add the below line just between fi and ;;
/sbin/iptables-restore < /etc/iptables-rules
Download and install sshdfilter - against ssh brute-forcing
mkdir /root/src
cd /root/src
wget http://www.glonek.co.uk/sshdfilter-1.5.5.tar.gz
tar -zxvf sshdfilter-1.5.5.tar.gz
cd sshdfilter-1.5.5
vi install_aswrapper.pl
Now let's patch the install script so it will work on Ubuntu. Find the below lines:
 } else {
   printf "System type does not appear to be Fedora, RedHat pre Fedora, Debian, \n";
   printf "CentOS, SUSE, RH Enterprise, Gentoo or Slackware. So, you will have to install manually \n";
   printf "(see INSTALL) and send me some hints on how to identify your system.\n";
   exit 1;
And replace with this:
 } else {
   printf "System type: Debian system\n";
   $pattype="deb31";
   $inittype="$pattype";
   $confpath="/etc/";
   $exepath="/usr/local/sbin/";
   $initname="ssh";
   $logconf="/etc/log.d/conf/services/";
   $logserv="/usr/share/logwatch/scripts/services/";
Now, let's install sshd filter:
./install_aswrapper.pl
 # you will get some errors there about Hunk failing. Let's fix that
cp /etc/init.d/ssh /etc/init.d/ssh-original
sed 's/--exec \/usr\/sbin\/sshd/--exec \/usr\/local\/sbin\/sshdfilter/g' /etc/init.d/ssh-original > /etc/init.d/ssh
 # now edit sshdfilter config file to your liking. It's well commented and doesn't really need much tweaking
vi /etc/sshdfilterrc
 # you will want to wteak the main rules (5,3d = DEFAULT etc) to remove the examples
 # you will also want to look at SECTION IPPOLICY for ip based filtering to remove examples
 # restart ssh and we are ready
/etc/init.d/ssh restart

Install MySQL, PHP, APACHE

Let's install the services now for LAMP (Linux Apache MySQL PHP)

aptitude install mysql-server mysql-client apache2 php5
You will be asked to provide a new MySQL root password. Put one in and make sure you don't forget it.
And some php common modules
aptitude install php5-cli php5-curl php5-gd php5-imap php5-mcrypt php5-mhash php5-mysql
There is a problem I found with the php configuration on ubuntu that needs fixing (small fix, don't worry)
Edit the file below and put the correct contents inside.
vi /etc/apache2/mods-available/php5.conf
 # the contents should read

<IfModule mod_php5.c>
  AddType application/x-httpd-php .php
  AddType application/x-httpd-php-source .phps
</IfModule>

 # let's restart apache now to read in new changes
/etc/init.d/apache2 restart
The webserver will now work. Now, let's configure it for virtual hosting with named vhosts. Ubuntu apache packages have that preconfigured, which is quite nice of them.
cd /etc/apache2
cat ports.conf
 # you should see uncommented NameVirtualHost and Listen. These tell apache to listen on port 80 and do virtual hosting there.
 # let's create a new virtual host now for example.com website (simple one)
cd sites-available
vi example.com

<VirtualHost *:80>
        ServerAdmin webmaster@localhost
	ServerName www.example.com
	ServerAlias example.com
	DocumentRoot /var/www/example.com
	ErrorLog /var/log/apache2/example.com-error.log
	LogLevel warn
	# can change to debug, info, notice, warn, error, crit, alert, emerg
	CustomLog /var/log/apache2/example.com-access.log combined
</VirtualHost>

 # now save the file and let's create the actual directory
mkdir /var/www/example.com
 # enable the site
ln -s /etc/apache2/sites-available/example.com /etc/apache2/sites-enabled/example.com
 # check sanity of config file
apache2ctl -t
 # reload configuration
/etc/init.d/apache2 reload
 # let's put test file in there
echo "<?php phpinfo(); ?>" > /var/www/example.com/index.php
 # that's it, easy and it works. Once we setup DNS in BIND for the test domain, we will be able to test it out
Now, let's open the firewall for the web server
iptables -I INPUT -p tcp -m tcp --dport 80 -j ACCEPT
iptables -I INPUT -p tcp -m tcp --dport 443 -j ACCEPT
iptables-save > /etc/iptables-rules

Install BIND9 DNS server

Install BIND

aptitude install bind9
cd /etc/bind
Add a new forward and backward lookup zone and check if bind works
vi /etc/bind/named.conf.local
 # put the below in the end of the file
zone "example.com" {
        type master;
        file "/etc/bind/zones/example.com.db";
};

zone "0.168.192.in-addr.arpa" {
        type master;
        file "/etc/bind/zones/rev.0.168.192.in-addr.arpa";
};

 # now let's create the zone files
mkdir /etc/bind/zones
cd /etc/bind/zones
vi example.com.db
Put the below in the example.com.db zone file. In a life server, you would replace 192.168.0.50 with an external internet IP. For testing, put your server actual IP in
$TTL 1h
example.com.  IN      SOA     ns.example.com.        webadmin@example.com. (
                                                        2009010910 ;serial
                                                        3600 ;refresh
                                                        3600 ;retry
                                                        3600 ;expire
                                                        3600 ;minimum TTL
)

example.com. IN  NS      ns.example.com.
example.com. IN  MX      10      mail.example.com.
example.com. IN  MX      20      mail.example.com.

@       IN      A       192.168.0.50
www     IN      A       192.168.0.50
mail    IN      A       192.168.0.50
ns 	IN 	A 	192.168.0.50

example.com.     IN      TXT     "v=spf1 a mx ip4:192.168.0.50 -all"
example.com.     IN      SPF     "v=spf1 a mx ip4:192.168.0.50 -all"
Now edit the rev.0.168.192.in-addr.arpa and put the below in it
vi rev.0.168.192.in-addr.arpa

 # put this in the file
$TTL 1h
@ IN SOA ns.example.com. webadmin@example.com. (
                                                        2008112111 ;serial
                                                        3600 ;refresh
                                                        3600 ;retry
                                                        3600 ;expire
                                                        3600 ;minimum TTL
)

                IN      NS      ns.example.com.
50              IN      PTR     example.com
That should do the trick and this DNS server should soon respond to queries to example.com with an actual IP :)
Now a very important step is to stop this server from being an open DNS server. To the outside world it should only respond to queries for domains it is configured as an authoritative server.
vi /etc/bind/named.conf.options

 # at the end of the file, just above the enclosure "};" which ends the options part, insert this line
 # this is assuming you want to allow all lookups from your internal network
 # and that your internal network is 192.168.24.0/24
allow-recursion { 127.0.0.1; 192.168.24.0/24; };
Right, let's restart BIND and do some tests
 # restart bind name server (named)
/etc/init.d/bind9 restart
 # if that hangs, ctrl+c the restart. Then run the below 2 commands:
NMD=`ps -A |grep named |grep -v grep |cut -d " " -f 1`; kill -9 $NMD
/etc/init.d/bind9 start
 # test new configuration. you should get your 192.168.0.50
dig @localhost example.com
dig @localhost -x 192.168.0.50
 # configure machine to use our DNS server as the main one
vi /etc/resolv.conf
 # add the below line as the first nameserver entry
nameserver 127.0.0.1
 # save the file, no need to restart anything
 # now can can also check our vhost from apache
lynx http://example.com
 # you should see a lot of output with the configuration of your apache and php. Hit Q and Y to exit

Install FTP server

Install proftpd

aptitude install libmysqlclient-dev proftpd proftpd-mod-mysql
When asked if you want to install in inetd or standalone, choose standalone.
Now, let's create ftp user, group and mysql tables. Replace password with an actual password
groupadd -g 2001 ftpgroup
useradd -u 2001 -s /bin/false -d /bin/null -c "proftpd user" -g ftpgroup ftpuser
mysql -u root -p
create database ftp;
GRANT SELECT, INSERT, UPDATE, DELETE ON ftp.* TO 'proftpd'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE, DELETE ON ftp.* TO 'proftpd'@'localhost.localdomain' IDENTIFIED BY 'password';
FLUSH PRIVILEGES;
USE ftp;

CREATE TABLE ftpgroup (
groupname varchar(16) NOT NULL default '',
gid smallint(6) NOT NULL default '5500',
members varchar(16) NOT NULL default '',
KEY groupname (groupname)
) TYPE=MyISAM COMMENT='ProFTP group table';

CREATE TABLE ftpquotalimits (
name varchar(30) default NULL,
quota_type enum('user','group','class','all') NOT NULL default 'user',
per_session enum('false','true') NOT NULL default 'false',
limit_type enum('soft','hard') NOT NULL default 'soft',
bytes_in_avail int(10) unsigned NOT NULL default '0',
bytes_out_avail int(10) unsigned NOT NULL default '0',
bytes_xfer_avail int(10) unsigned NOT NULL default '0',
files_in_avail int(10) unsigned NOT NULL default '0',
files_out_avail int(10) unsigned NOT NULL default '0',
files_xfer_avail int(10) unsigned NOT NULL default '0'
) TYPE=MyISAM;
CREATE TABLE ftpquotatallies (
name varchar(30) NOT NULL default '',
quota_type enum('user','group','class','all') NOT NULL default 'user',
bytes_in_used int(10) unsigned NOT NULL default '0',
bytes_out_used int(10) unsigned NOT NULL default '0',
bytes_xfer_used int(10) unsigned NOT NULL default '0',
files_in_used int(10) unsigned NOT NULL default '0',
files_out_used int(10) unsigned NOT NULL default '0',
files_xfer_used int(10) unsigned NOT NULL default '0'
) TYPE=MyISAM;

CREATE TABLE ftpuser (
id int(10) unsigned NOT NULL auto_increment,
userid varchar(32) NOT NULL default '',
passwd varchar(32) NOT NULL default '',
uid smallint(6) NOT NULL default '5500',
gid smallint(6) NOT NULL default '5500',
homedir varchar(255) NOT NULL default '',
shell varchar(16) NOT NULL default '/sbin/nologin',
count int(11) NOT NULL default '0',
accessed datetime NOT NULL default '0000-00-00 00:00:00',
modified datetime NOT NULL default '0000-00-00 00:00:00',
PRIMARY KEY (id),
UNIQUE KEY userid (userid)
) TYPE=MyISAM COMMENT='ProFTP user table';
quit;
Now, configure proftpd to use the given tables
vi /etc/proftpd/proftpd.conf

 # add the following lines to the file (don't need to remove comments from it)

DefaultRoot ~


# The passwords in MySQL are encrypted using CRYPT
SQLBackend     mysql
SQLEngine on
SQLAuthTypes            Plaintext Crypt
SQLAuthenticate         users* groups*


# used to connect to the database
# databasename@host database_user user_password
SQLConnectInfo  ftp@localhost proftpd password


# Here we tell ProFTPd the names of the database columns in the "usertable"
# we want it to interact with. Match the names with those in the db
SQLUserInfo     ftpuser userid passwd uid gid homedir shell

# Here we tell ProFTPd the names of the database columns in the "grouptable"
# we want it to interact with. Again the names match with those in the db
SQLGroupInfo    ftpgroup groupname gid members

# set min UID and GID - otherwise these are 999 each
SQLMinID        500

# create a user's home directory on demand if it doesn't exist
SQLHomedirOnDemand on

# Update count every time user logs in
SQLLog PASS updatecount
SQLNamedQuery updatecount UPDATE "count=count+1, accessed=now() WHERE userid='%u'" ftpuser

# Update modified everytime user uploads or deletes a file
SQLLog  STOR,DELE modified
SQLNamedQuery modified UPDATE "modified=now() WHERE userid='%u'" ftpuser

# User quotas
# ===========
QuotaEngine on
QuotaDirectoryTally on
QuotaDisplayUnits Mb
QuotaShowQuotas on

SQLNamedQuery get-quota-limit SELECT "name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail FROM ftpquotalimits WHERE name = '%{0}' AND quota_type = '%{1}'"

SQLNamedQuery get-quota-tally SELECT "name, quota_type, bytes_in_used, bytes_out_used, bytes_xfer_used, files_in_used, files_out_used, files_xfer_used FROM ftpquotatallies WHERE name = '%{0}' AND quota_type = '%{1}'"

SQLNamedQuery update-quota-tally UPDATE "bytes_in_used = bytes_in_used + %{0}, bytes_out_used = bytes_out_used + %{1}, bytes_xfer_used = bytes_xfer_used + %{2}, files_in_used = files_in_used + %{3}, files_out_used = files_out_used + %{4}, files_xfer_used = files_xfer_used + %{5} WHERE name = '%{6}' AND quota_type = '%{7}'" ftpquotatallies

SQLNamedQuery insert-quota-tally INSERT "%{0}, %{1}, %{2}, %{3}, %{4}, %{5}, %{6}, %{7}" ftpquotatallies

QuotaLimitTable sql:/get-quota-limit
QuotaTallyTable sql:/get-quota-tally/update-quota-tally/insert-quota-tally

RootLogin off
RequireValidShell off
SQLNamedQuery userquota SELECT "IF ((SELECT (@availmbytes:=ROUND((`bytes_in_avail`/1048576),2)) FROM `ftpquotalimits` WHERE `name`='%u') = 0, \"No user quota applies.\", CONCAT(\"User quota: Used \", (SELECT (@usedmbytes:=ROUND((`bytes_in_used`/1048576),2)) FROM `ftpquotatallies` WHERE `name`='%u'), \"MB from \", @availmbytes, \"MB. You have \", ROUND(@availmbytes-@usedmbytes,2), \"MB available space.\"))"

SQLShowInfo   LIST    "226" "%{userquota}"
PassivePorts 60000 65000
Enable the SQL module
vi /etc/proftpd/modules.conf

 #uncomment (or add if not exist) the below lines
LoadModule mod_sql.c
LoadModule mod_sql_mysql.c
Let's restart proftpd so it will read the new configs
/etc/init.d/proftpd restart
Populate data into proftpd and test
mysql -u root -p
USE ftp;
INSERT INTO `ftpgroup` (`groupname`, `gid`, `members`) VALUES ('ftpgroup', 2001, 'ftpuser');
 # now we are done with table ftpgroup, no need to add any new entries here
 # new users are added to ftpquotalimits and ftpuser, let's do that now
INSERT INTO `ftpquotalimits` (`name`, `quota_type`, `per_session`, `limit_type`, `bytes_in_avail`, `bytes_out_avail`, `bytes_xfer_avail`, `files_in_avail`, `files_out_avail`, `files_xfer_avail`) VALUES ('exampleuser', 'user', 'true', 'hard', 15728640, 0, 0, 0, 0, 0);
INSERT INTO `ftpuser` (`id`, `userid`, `passwd`, `uid`, `gid`, `homedir`, `shell`, `count`, `accessed`, `modified`) VALUES (1, 'exampleuser', 'secret', 2001, 2001, '/home/www.example.com', '/sbin/nologin', 0, '', '');
quit;
Test new configuration and open firewall (ftp and passwive mode)
ftp localhost
user: exampleuser
pass: secret
ls
quit
ls /home
 # you should see folder called www.example.com created automatically for the user.
iptables -I INPUT -p tcp -m tcp --dport 21 -j ACCEPT
iptables -I INPUT -p tcp -m tcp --dport 60000:65000 -j ACCEPT
iptables-save > /etc/iptables-rules

Install MAIL server

Install required packages

 # postfix
aptitude install postfix postfix-mysql
 # sasl
aptitude install libsasl2-modules-sql libgsasl7 libauthen-sasl-cyrus-perl
 # courier
aptitude install courier-base courier-authdaemon courier-authlib-mysql courier-imap courier-imap-ssl courier-ssl
 # clamav
aptitude install clamav-base libclamav6 clamav-daemon clamav-freshclam
 # amavis, spamassassin
aptitude install amavisd-new
aptitude install spamassassin spamc
 # squirrelmail
aptitude install squirrelmail squirrelmail-locales php-pear
 # phpmyadmin
aptitude install phpmyadmin
 # maildrop
aptitude install maildrop
Configure postfix
Configure mailname - put your server name here. Example: smtp.domain.name
vi /etc/mailname
Configure main.cf - vi the file, find the lines given below and change to the settings given below. If the lines don't exist, create them. I normally comment out all the lines mentioned below first and then add my configurations on the bottom of the file (for easy manipulation)
vi /etc/postfix/main.cf

myorigin=example.com
smtpd_banner = $myhostname ESMTP $mail_name
inet_interfaces = all
mynetworks_style = host
local_recipient_maps = 
mydestination =
# how long if undelivered before sending warning update to sender
delay_warning_time = 4h
# will it be a permanent error or temporary
unknown_local_recipient_reject_code = 450
# how long to keep message on queue before return as failed.
maximal_queue_lifetime = 7d
# max and min time in seconds between retries if connection failed
minimal_backoff_time = 1000s
maximal_backoff_time = 8000s
# how long to wait when servers connect before receiving rest of data
smtp_helo_timeout = 60s
# how many address can be used in one message.
# effective stopper to mass spammers, accidental copy in whole address list
# but may restrict intentional mail shots.
smtpd_recipient_limit = 16
# how many error before back off.
smtpd_soft_error_limit = 3
# how many max errors before blocking it.
smtpd_hard_error_limit = 12
# Requirements for the HELO statement
smtpd_helo_restrictions = permit_mynetworks, warn_if_reject reject_non_fqdn_hostname, reject_invalid_hostname, permit
# Requirements for the sender details
smtpd_sender_restrictions = permit_mynetworks, warn_if_reject reject_non_fqdn_sender, reject_unknown_sender_domain, reject_unauth_pipelining, permit
# Requirements for the connecting server
smtpd_client_restrictions = reject_rbl_client sbl.spamhaus.org, reject_rbl_client blackholes.easynet.nl, reject_rbl_client dnsbl.njabl.org
# Requirement for the recipient address
smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_pipelining, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination, permit
smtpd_data_restrictions = permit_mynetworks, reject_unauth_pipelining
# require proper helo at connections
smtpd_helo_required = yes
# waste spammers time before rejecting them
smtpd_delay_reject = yes
disable_vrfy_command = yes
# not sure of the difference of the next two
# but they are needed for local aliasing
alias_maps = hash:/etc/postfix/aliases
alias_database = hash:/etc/postfix/aliases
# this specifies where the virtual mailbox folders will be located
virtual_mailbox_base = /var/spool/mail/virtual
# this is for the mailbox location for each user
virtual_mailbox_maps = mysql:/etc/postfix/mysql_mailbox.cf
# and their user id
virtual_uid_maps = mysql:/etc/postfix/mysql_uid.cf
# and group id
virtual_gid_maps = mysql:/etc/postfix/mysql_gid.cf
# and this is for aliases
virtual_alias_maps = mysql:/etc/postfix/mysql_alias.cf
# and this is for domain lookups
virtual_mailbox_domains = mysql:/etc/postfix/mysql_domains.cf
# this is how to connect to the domains (all virtual, but the option is there)
# not used yet, so commented out
transport_maps = mysql:/etc/postfix/mysql_transport.cf
# relayhost, leave empty to deliver mail yourself
# or replace with a domain name if you intend to use your ISP
# eg: relayhost = smtp.yourisp.com
relayhost =

 # comment out the following lines
#myhostname= mail.example.com
#masquerade_domains = mail.example.com www.example.com !sub.dyndomain.com
#masquerade_exceptions = root
#mailbox_command = procmail -a "$EXTENSION"
Create an alias table, setup folders where virtual mail will be stored
cp /etc/aliases /etc/postfix/aliases
postalias /etc/postfix/aliases
mkdir /var/spool/mail/virtual 
groupadd virtual -g 5000 
useradd virtual -u 5000 -g 5000 
chown -R virtual:virtual /var/spool/mail/virtual
Next let's configure Postfix MySQL tables
vi /etc/postfix/mysql_transport.cf

user=mail
password=mailPASSWORD
dbname=maildb
table=domains
select_field=transport
where_field=domain
hosts=127.0.0.1
additional_conditions = and enabled = 1


vi /etc/postfix/mysql_mailbox.cf

user=mail
password=mailPASSWORD
dbname=maildb
table=users
select_field=maildir
where_field=id
hosts=127.0.0.1
additional_conditions = and enabled = 1


vi /etc/postfix/mysql_uid.cf

user=mail
password=mailPASSWORD
dbname=maildb
table=users
select_field=uid
where_field=id
hosts=127.0.0.1


vi /etc/postfix/mysql_gid.cf

user=mail
password=mailPASSWORD
dbname=maildb
table=users
select_field=gid
where_field=id
hosts=127.0.0.1


vi /etc/postfix/mysql_alias.cf

user=mail
password=mailPASSWORD
dbname=maildb
table=aliases
select_field=destination
where_field=mail
hosts=127.0.0.1
additional_conditions = and enabled = 1


vi /etc/postfix/mysql_domains.cf

user=mail
password=mailPASSWORD
dbname=maildb
table=domains
select_field=domain
where_field=domain
hosts=127.0.0.1
additional_conditions = and enabled = 1
Ok, now let's create the database itself
mysql -u root -p
create database maildb;
GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,DROP ON maildb.* TO 'mail'@'localhost' IDENTIFIED by 'mailPASSWORD';
GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,DROP ON maildb.* TO 'mail'@'%' IDENTIFIED by 'mailPASSWORD'; 
exit;
mysql -u mail -p maildb
CREATE TABLE `aliases` ( `pkid` smallint(3) NOT NULL auto_increment, `mail` varchar(120) NOT NULL default '', `destination` varchar(120) NOT NULL default '', `enabled` tinyint(1) NOT NULL default '1', PRIMARY KEY (`pkid`), UNIQUE KEY `mail` (`mail`) ) ;
CREATE TABLE `domains` ( `pkid` smallint(6) NOT NULL auto_increment, `domain` varchar(120) NOT NULL default '', `transport` varchar(120) NOT NULL default 'virtual:', `enabled` tinyint(1) NOT NULL default '1', PRIMARY KEY (`pkid`) ) ;
CREATE TABLE `users` ( `id` varchar(128) NOT NULL default '', `name` varchar(128) NOT NULL default '', `uid` smallint(5) unsigned NOT NULL default '5000', `gid` smallint(5) unsigned NOT NULL default '5000', `home` varchar(255) NOT NULL default '/var/spool/mail/virtual', `maildir` varchar(255) NOT NULL default 'blah/', `enabled` tinyint(3) unsigned NOT NULL default '1', `change_password` tinyint(3) unsigned NOT NULL default '1', `clear` varchar(128) NOT NULL default 'ChangeMe', `crypt` varchar(128) NOT NULL default 'sdtrusfX0Jj66', `quota` varchar(255) NOT NULL default '', `procmailrc` varchar(128) NOT NULL default '', `spamassassinrc` varchar(128) NOT NULL default '', PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) ) ;
# To visualise the tables created: 
describe aliases; describe domains; describe users;
exit;
OPTIONAL: It is useful to log all SQL calls that are made to MySQL in order to find if all works fine. Then, in a few weeks, comment out this line
vi /etc/mysql/my.cnf
log = /var/log/mysql/mysql.log

#save the file and exit vi

/etc/init.d/mysql restart

POP/IMAP Configuration
Configure Courier IMAP to use MySQL
vi /etc/courier/authdaemonrc
authmodulelist="authmysql"
DEBUG_LOGIN=2

vi /etc/courier/authmysqlrc
MYSQL_USERNAME mail
MYSQL_PASSWORD mailPASSWORD
MYSQL_DATABASE maildb
MYSQL_USER_TABLE users
 # change the below 2 lines to allow clear instead of crypt passwords
MYSQL_CRYPT_PWFIELD crypt
# MYSQL_CLEAR_PWFIELD clear
MYSQL_MAILDIR_FIELD concat(home,'/',maildir)
MYSQL_WHERE_CLAUSE enabled=1

 # have a look at the below file to see if you want to change anything
 # now changes should be required though
vi /etc/courier/imapd

Configure content checks (anti-spam and anti-virus)
Take a look at the below files. No need to change anything:
cd /etc/amavis/conf.d
less 05-domain-id
less 05-node-id
less 15-av_scanners
less 20-debian_defaults
Edit content check file
vi 05-node_id
 # oncomment and edit the below line
$myhostname = "example.com";

#and save the file now

vi 15-content_filter_mode
 # uncomment the below content checks
@bypass_virus_checks_maps = ( 
   \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re); 
@bypass_spam_checks_maps = ( 
   \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);
Edit the user file. Insert the below somewhere in the middle
vi 50-user

@local_domains_acl = qw(.);
$log_level = 2; # change to 2 for debugging
$syslog_priority = 'info'; # change to debug for debugging
$sa_kill_level_deflt = 8.0;
# triggers spam evasive actions
$final_spam_destiny = D_DISCARD; # change to D_PASS to allow spam to go through
Configure postfix to use amavis, enable spamassassin
#enable clamav to scan amavis temp files
adduser clamav amavis

#edit postfix
vi /etc/postfix/master.cf

 # append these lines at the end of the file (make sure they are not there before)
 # note the -o switches must have spaces before them
amavis unix - - - - 2 smtp
	 -o smtp_data_done_timeout=1200
	 -o smtp_send_xforward_command=yes
	 -o disable_dns_lookups=yes
	 -o max_use=20
127.0.0.1:10025 inet n - - - - smtpd
	 -o content_filter=
	 -o local_recipient_maps=
	 -o relay_recipient_maps=
	 -o smtpd_restriction_classes=
	 -o smtpd_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 receive_override_options=no_header_body_checks,no_unknown_recipient_checks

 # add the following 2 lines just below "pickup"
	 -o content_filter=
	 -o receive_override_options=no_header_body_checks


vi /etc/postfix/main.cf
 # add the follwing line to this file
content_filter = amavis:[127.0.0.1]:10024


vi /etc/default/spamassassin
ENABLED=1


 # one thing you can do is tweak the below file to enable Bayes and auto-learning
vi /etc/spamassassin/local.cf
Right, now the below is to make spamassassin use MySQL database. It is optional, but makes an excellent addition to the whole configuration
mysql -u root -p
 # your mysql root password
create database spamassassin;
grant all on spamassassin.* to spamassassin identified by 'password';
flush privileges;
exit;

mysql -u spamassassin -p spamassassin
 # enter the spamassassin password we just set

 # here we will have user preferences
CREATE TABLE userpref (
  username varchar(100) NOT NULL default '',
  preference varchar(30) NOT NULL default '',
  value varchar(100) NOT NULL default '',
  prefid int(11) NOT NULL auto_increment,
  PRIMARY KEY  (prefid),
  KEY username (username)
) TYPE=MyISAM;

 # how about whitelist table
CREATE TABLE awl (
  username varchar(100) NOT NULL default '',
  email varchar(200) NOT NULL default '',
  ip varchar(10) NOT NULL default '',
  count int(11) default '0',
  totscore float default '0',
  PRIMARY KEY  (username,email,ip)
) TYPE=MyISAM;

 # Bayes configuration, this is going to make your head hurt:
CREATE TABLE bayes_expire (
  id int(11) NOT NULL default '0',
  runtime int(11) NOT NULL default '0',
  KEY bayes_expire_idx1 (id)
) TYPE=MyISAM;

CREATE TABLE bayes_global_vars (
  variable varchar(30) NOT NULL default '',
  value varchar(200) NOT NULL default '',
  PRIMARY KEY  (variable)
) TYPE=MyISAM;

INSERT INTO bayes_global_vars VALUES ('VERSION','3');

CREATE TABLE bayes_seen (
  id int(11) NOT NULL default '0',
  msgid varchar(200) binary NOT NULL default '',
  flag char(1) NOT NULL default '',
  PRIMARY KEY  (id,msgid)
) TYPE=MyISAM;

CREATE TABLE bayes_token (
  id int(11) NOT NULL default '0',
  token char(5) NOT NULL default '',
  spam_count int(11) NOT NULL default '0',
  ham_count int(11) NOT NULL default '0',
  atime int(11) NOT NULL default '0',
  PRIMARY KEY  (id, token),
  INDEX bayes_token_idx1 (token),
  INDEX bayes_token_idx2 (id, atime)
) TYPE=MyISAM;

CREATE TABLE bayes_vars (
  id int(11) NOT NULL AUTO_INCREMENT,
  username varchar(200) NOT NULL default '',
  spam_count int(11) NOT NULL default '0',
  ham_count int(11) NOT NULL default '0',
  token_count int(11) NOT NULL default '0',
  last_expire int(11) NOT NULL default '0',
  last_atime_delta int(11) NOT NULL default '0',
  last_expire_reduce int(11) NOT NULL default '0',
  oldest_token_age int(11) NOT NULL default '2147483647',
  newest_token_age int(11) NOT NULL default '0',
  PRIMARY KEY  (id),
  UNIQUE bayes_vars_idx1 (username)
) TYPE=MyISAM;

 # now a few defaults that we need in order for this to work
INSERT INTO userpref (username,preference,value) VALUES ('$GLOBAL','required_hits','5.0');
INSERT INTO userpref (username,preference,value) VALUES ('$GLOBAL','report_safe','1');
INSERT INTO userpref (username,preference,value) VALUES ('$GLOBAL','use_bayes','1');
INSERT INTO userpref (username,preference,value) VALUES ('$GLOBAL','use_dcc','1');
exit;

 # ok, let's configure spamassassin now
vi /etc/spamassassin/local.cf

 # put this chunck in at the end of file
 # don't forget to change DBUSER to spamassassin and DBPASSWORD to the password we set eariler
bayes_store_module      Mail::SpamAssassin::BayesStore::MySQL
bayes_sql_dsn           DBI:mysql:spamassassin:localhost:3306
bayes_sql_username      DBUSER
bayes_sql_password      DBPASSWORD

auto_whitelist_factory          Mail::SpamAssassin::SQLBasedAddrList
user_awl_dsn                    DBI:mysql:spamassassin:localhost:3306
user_awl_sql_username           DBUSER
user_awl_sql_password           DBPASSWORD

user_scores_dsn                  DBI:mysql:spamassassin:localhost:3306
user_scores_sql_username         DBUSER
user_scores_sql_password         DBPASSWORD
user_scores_sql_custom_query     SELECT preference, value FROM _TABLE_ WHERE username = _USERNAME_ OR username = '$GLOBAL' OR username = CONCAT('%',_DOMAIN_) ORDER BY username ASC


 # now save the file

 # need to reconfigure spamd options to use sql
vi /etc/init.d/spamassassin
 # find the DOPTIONS line and at the end of it, add this
-c -q -u virtual -H

 # and restart spamassassin
/etc/init.d/spamassassin restart
To find out more about how to control user preferences for spam filtering, look at server administration part of this guide
Now we will want to enhance SpamAssassin to actually do something. The below will create 2 folders per user if not yet exist (Spam, Safe) and run the learning app for SA on those folders. Any emails in the Spam folder will be used for spamassassin to learn what spam is and any mail in Safe folder will be used by spamassassin to learn what is a safe never to move to spam. Then we configure this script to run every midnight. All you have to do to fight spam successfully then is educate your users to use Spam and Safe folders in order to help SpamAssassin learn.
 # let's create the script first
vi /usr/local/bin/sa-learn.sh

#!/bin/bash
for i in `ls /var/spool/mail/virtual`
do
        echo "Checking user: $i"
        cd "/var/spool/mail/virtual/$i"
        if [ -d ".Spam" ]
        then
                echo -n ""
        else
                echo " -> Didn't find .Spam folder, attempting to create"
                mkdir ".Spam"
                mkdir ".Spam/cur"
                mkdir ".Spam/new"
                mkdir ".Spam/tmp"
                chown -R virtual:virtual ".Spam"
                chmod -R 2700 ".Spam"
        fi
        if [ -d ".Safe" ]
        then
                echo -n ""
        else
                echo " -> Didn't find .Safe folder, attempting to create"
                mkdir ".Safe"
                mkdir ".Safe/cur"
                mkdir ".Safe/new"
                mkdir ".Safe/tmp"
                chown -R virtual:virtual ".Safe"
                chmod -R 2700 ".Safe"
        fi
        if [ -f subscriptions ]; then
                echo -n ""
        else
                touch subscriptions
                chown root:virtual subscriptions
                chmod 644 subscriptions
        fi
        if [ -f courierimapsubscribed ]; then
                echo -n ""
        else
                touch courierimapsubscribed
                chown root:virtual courierimapsubscribed
                chmod 644 courierimapsubscribed
        fi
        safe=`cat subscriptions |grep -i Safe |wc -l`
        if [ $safe -lt 1 ]; then
                echo " -> Adding 'safe' subscription"
                echo "Safe" >> subscriptions
        fi
        spam=`cat subscriptions |grep -i Spam |wc -l`
        if [ $spam -lt 1 ]; then
                echo " -> Adding 'spam' subscription"
                echo "Spam" >> subscriptions
        fi
        safe=`cat courierimapsubscribed |grep -i Safe |wc -l`
        if [ $safe -lt 1 ]; then
                echo " -> Adding 'safe' curriersubscription"
                echo "INBOX.Safe" >> courierimapsubscribed
        fi
        spam=`cat courierimapsubscribed |grep -i Spam |wc -l`
        if [ $spam -lt 1 ]; then
                echo " -> Adding 'spam' curriersubscription"
                echo "INBOX.Spam" >> courierimapsubscribed
        fi
done
echo "Done"
for i in `ls /var/spool/mail/virtual`
do
        echo "Checking user: $i"
        cd "/var/spool/mail/virtual/$i"
        if [ -d ".Spam" ]
        then
                echo " -> Found .Spam folder, updating spam list"
                /usr/bin/sa-learn --spam /var/spool/mail/virtual/$i/.Spam/{cur,new} --username="$i";
        fi
        if [ -d ".Safe" ]
        then
                echo " -> Found .Safe folder, updating safe senders list (ham)"
                /usr/bin/sa-learn --ham /var/spool/mail/virtual/$i/.Safe/{cur,new} --username="$i";
        fi
done
echo "Done"


 # now save and close the file
 # and run the file to check if it does the job
chmod 755 /usr/local/bin/sa-learn.sh
/usr/local/bin/sa-learn.sh
 # add the script to cron
echo -e "\n10 0 * * * root /usr/local/bin/sa-learn.sh" >> /etc/crontab
 # reload configuration
/etc/init.d/cron restart
Configure a few stuff in clamav
 # be default freshclam checks for antivirus updates 24 times a day
 # I find that a bit excessive, so I change that to once a day
dpkg-reconfigure clamav-freshclam

 # if you really want to tweak clamav, then run the below command
 # it will ask a lot of questions. Optional, no action really needed
dpkg-reconfigure clamav-base
Make sure smtpd recipient is correct configuration
vi /etc/postfix/main.cf

 # edit the receipient restrictions
smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_pipelining, permit_sasl_authenticated, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination, permit

Authentication
SASL secures the actual authentication while TLS encrypts the whole traffic. Let's configure SASL first
vi /etc/postfix/main.cf

# modify the existing smtpd_recipient_restrictions
smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_pipelining, permit_sasl_authenticated, reject_non_fqdn_recipient, reject_unauth_destination, permit

# modify the existing smtpd_sender_restrictions
smtpd_sender_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_sender, reject_unknown_sender_domain, reject_unauth_pipelining, permit

# then add these
smtpd_sasl_auth_enable = yes
smtp_sasl_auth_enable = yes
broken_sasl_auth_clients = yes
#smtpd_sasl_path = /etc/postfix/sasl:/usr/lib/sasl2
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain =
smtp_sasl_password_maps = mysql:/etc/postfix/mysql_maps.cf
And create mysql lookup maps
vi /etc/postfix/mysql_maps.cf

user = mail
password = mailPASSWORD
hosts = 127.0.0.1
dbname = maildb
query = SELECT id,clear FROM users WHERE id='%s'
Let's create the sasl configuration and configure courier
# May already exist
mkdir /etc/postfix/sasl
vi /etc/postfix/sasl/smtpd.conf
pwcheck_method: auxprop
auxprop_plugin: sql
mech_list: plain login cram-md5 digest-md5
sql_engine: mysql
sql_hostnames: 127.0.0.1
sql_user: mail
sql_passwd: mailPASSWORD
sql_database: maildb
sql_select: select clear from users where id='%u@%r' and enabled = 1

vi /etc/courier/imapd
 # confirm, uncomment the below line
IMAP_CAPABILITY="IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA AUTH=CRAM-MD5 AUTH=CRAM-SHA1 IDLE"
Postfix TLS configuration
vi /etc/postfix/main.cf
 # some of the below might already exist, I commented them out and put my configs in the bottom

# TLS parameters
#smtp_use_tls = no
smtp_tls_security_level = may
#smtpd_use_tls=yes
smtpd_tls_security_level = may
#smtpd_tls_auth_only = no
smtp_tls_note_starttls_offer = yes
smtpd_tls_loglevel = 1
smtpd_tls_received_header = yes
smtpd_tls_session_cache_timeout = 3600s
tls_random_source = dev:/dev/urandom
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
#smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
#smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache


vi /etc/postfix/master.cf
 # by default only smtp service is enabled. I also enable submission (port 587) and force TLS there
 # I also enable smtps (port 465) for compatibility wit some older clients
submission inet n - n - - smtpd
	-o smtpd_sasl_auth_enable=yes
# if you do not want to restrict it encryption only, comment out next line
	-o smtpd_tls_auth_only=yes
	# -o smtpd_tls_security_level=encrypt
	# -o header_checks=
	# -o body_checks=
	-o smtpd_client_restrictions=permit_sasl_authenticated,reject_unauth_destination, reject
	-o smtpd_sasl_security_options=noanonymous,noplaintext
	-o smtpd_sasl_tls_security_options=noanonymous
	# -o milter_macro_daemon_name=ORIGINATING
smtps inet n - - - - smtpd
	-o smtpd_tls_wrappermode=yes
	-o smtpd_sasl_auth_enable=yes
	-o smtpd_tls_auth_only=yes
	-o smtpd_client_restrictions=permit_sasl_authenticated,reject
	-o smtpd_sasl_security_options=noanonymous,noplaintext
	-o smtpd_sasl_tls_security_options=noanonymous
	# -o milter_macro_daemon_name=ORIGINATING
COurier TLS configuration
cd /etc/courier
openssl req -x509 -newkey rsa:1024 -keyout imapd.pem \
 -out imapd.pem -nodes -days 999

vi /etc/courier/imapd-ssl
 # ensure this line is fine
TLS_CERTFILE=/etc/courier/imapd.pem
 # also, if you want to restrict IMAP users to SSL/TLS only, toggle this setting to 1
IMAP_TLS_REQUIRED=1
WebMail
cp /etc/squirrelmail/apache.conf /etc/apache2/sites-available/squirrelmail
ln -s /etc/apache2/sites-available/squirrelmail /etc/apache2/sites-enabled/500-squirrelmail

vi /etc/apache2/sites-available/squirrelmail
 # you might want to accept defaults or change to your liking (eg comment out alias and uncomment virtual hosting)

apache2ctl -t
/etc/init.d/apache2 reload
squirrelmail-configure
 # Type 2, then A, then 8. Enter "courier" (without the quotes)
 # Now type 7, type Y. Now type 5. Enter 993
 # Type S, Enter, Q
PHPMyAdmin
cp /etc/phpmyadmin/apache.conf /etc/apache2/sites-available/phpmyadmin
ln -s /etc/apache2/sites-available/phpmyadmin /etc/apache2/sites-enabled/400-phpmyadmin
rm /etc/apache2/conf.d/phpmyadmin.conf
vi /etc/apache2/sites-available/phpmyadmin
 # here you can edit settings if you want to
 # for example comment out alias and uncomment virtual hosting

apache2ctl -t
/etc/init.d/apache2 reload
Add default basic (and example) users and domains
mysql -u root -p maildb

 # add default stuff
INSERT INTO domains (domain) VALUES ('localhost'), ('localhost.localdomain');
INSERT INTO aliases (mail,destination) VALUES ('postmaster@localhost','root@localhost'), ('sysadmin@localhost','root@localhost'), ('webmaster@localhost','root@localhost'), ('abuse@localhost','root@localhost'), ('root@localhost','root@localhost'), ('@localhost','root@localhost'), ('@localhost.localdomain','@localhost');
INSERT INTO users (id,name,maildir,crypt) VALUES ('root@localhost','root','root/', encrypt('apassword') );

 # add example users
INSERT INTO domains (domain) VALUES ('example.com'),('example2.com');
INSERT INTO aliases (mail,destination) VALUES ('@example.com','@example2.com'),('postmaster@example2.com','webadmin@example2.com'),('abuse@example2.com','webadmin@example2.com'),('webadmin@example2.com','webadmin@example2.com');
INSERT INTO users (id,name,maildir,clear,crypt) VALUES ('webadmin@example2.com','WebAdmin','webadmin@example2.com/','apassword',encrypt('apassword'));
exit;
you have to remember that if you want to add a local user and have mail delivered to them, you need to create an entry in the alias table that will point the email address to itself. Just like we did for webadmin@example2.com

Aliases can be used eg. to deliver mail going to Email1 over to User1 and User2, or to external gmail account or to deliver mail going to Email2 to an external gmail address and a local User3. Anything you can think of

Including pointing email going to domain1.com over to domain2.com or mail going to anything at domain3.com to deliver to a single external gmail user. Really, anything you think of

Right, now for mail filtering. We need maildrop as procmail does not support virtual users
Create configuration files for maildrop first
vi /etc/maildroprc

LOGNAME=tolower("$LOGNAME")
EXTENSION="$1"
RECIPIENT=tolower("$2")
USER="$3"
HOST="$4"
SENDER="$5"
DEFAULT="/var/spool/mail/virtual/$USER@$HOST/."

if ( "$EXTENSION" ne "" )
{
DELIMITER="+"
}

if (!$SENDER)
{
SENDER = ""
}

`test -e /var/spool/mail/virtual/$USER\@$HOST`
if ( $RETURNCODE != 0 )
{
`mkdir /var/spool/mail/virtual/$USER\@$HOST`
`chmod -R 0700 /var/spool/mail/virtual/$USER\@$HOST`
}

`test -f /var/spool/mail/virtual/.mailfilters/$LOGNAME`
if ( $RETURNCODE == 0 )
{
include "/var/spool/mail/virtual/.mailfilters/$LOGNAME"
}
else
{
if ( "$DEFAULT" ne "" )
{
to "$DEFAULT"
}
else
{
EXITCODE=75
exit
}
}
It will create user mailbox should that not exist. It will also include specific configuration for the user should that exist.
vi /etc/postfix/master.cf
 # find the maildrop line and flags and make sure they look like this:
 # the -w 90 means that maildrop will warn user with message every day when user is 90% of mailbox use
maildrop unix - n n - - pipe
        flags=DRhu user=virtual argv=/usr/bin/maildrop -w 90 -d ${user}@${nexthop} ${extension} ${recipient} ${user} ${nexthop} ${sender}

 # now save the file and let's change transport for a domain
/etc/init.d/postfix restart
mysql -u root -p maildb
update domains set transport='maildrop' where domain='example2.com';
Another thing that I like to do is have a script that will create al lthe folders for mail, including Safe and Spam folders for me. This is totally optional though:
vi /usr/local/bin/mail-create.sh

#!/bin/bash
FOLD=$1
mkdir "/var/spool/mail/virtual/$FOLD"
mkdir "/var/spool/mail/virtual/$FOLD/new"
mkdir "/var/spool/mail/virtual/$FOLD/cur"
mkdir "/var/spool/mail/virtual/$FOLD/tmp"
mkdir "/var/spool/mail/virtual/$FOLD/.Spam"
mkdir "/var/spool/mail/virtual/$FOLD/.Safe"
mkdir "/var/spool/mail/virtual/$FOLD/.Spam/new"
mkdir "/var/spool/mail/virtual/$FOLD/.Safe/new"
mkdir "/var/spool/mail/virtual/$FOLD/.Spam/cur"
mkdir "/var/spool/mail/virtual/$FOLD/.Safe/cur"
mkdir "/var/spool/mail/virtual/$FOLD/.Spam/tmp"
mkdir "/var/spool/mail/virtual/$FOLD/.Safe/tmp"
chown -R virtual:virtual "/var/spool/mail/virtual/$FOLD"
chmod -R 2700 "/var/spool/mail/virtual/$FOLD"
cd "/var/spool/mail/virtual/$FOLD"
        if [ -f subscriptions ]; then
                echo -n ""
        else
                touch subscriptions
                chown root:virtual subscriptions
                chmod 644 subscriptions
        fi
        if [ -f courierimapsubscribed ]; then
                echo -n ""
        else
                touch courierimapsubscribed
                chown root:virtual courierimapsubscribed
                chmod 644 courierimapsubscribed
        fi
        safe=`cat subscriptions |grep -i Safe |wc -l`
        if [ $safe -lt 1 ]; then
                echo "Safe" >> subscriptions
        fi
        spam=`cat subscriptions |grep -i Spam |wc -l`
        if [ $spam -lt 1 ]; then
                echo "Spam" >> subscriptions
        fi
        safe=`cat courierimapsubscribed |grep -i Safe |wc -l`
        if [ $safe -lt 1 ]; then
                echo "INBOX.Safe" >> courierimapsubscribed
        fi
        spam=`cat courierimapsubscribed |grep -i Spam |wc -l`
        if [ $spam -lt 1 ]; then
                echo "INBOX.Spam" >> courierimapsubscribed
        fi

 # now save the file and run it for a test
chmod 755 /usr/local/bin/mail-create.sh
/usr/local/bin/mail-create.sh 'webadmin@example2.com'
ls -lh /var/spool/mail/virtual
And now a nasty hack to fix authdaemon permissions
vi /etc/init.d/courier-authdaemon

 # find these 2 lines
        $daemonscript start
        log_end_msg 0

 # and put this line in between
        chmod -R 777 /var/run/courier/authdaemon


# now save the file and restart courier authdaemon
/etc/init.d/courier-authdaemon restart

# and test maildrop. It should succeed
su -c "echo 'test' |maildrop -V 9 -d 'webadmin@example2.com' '' 'webadmin' 'example2.com' 'test@example.com'" virtual
How about controlling mailbox size limits? This is not implemented on postfix by default, so we are going to do a few hacks
Edit/Create the below files in order to add mailbox size limits. Full content provided
vi /etc/postfix/sasl/smtpd.conf

pwcheck_method: auxprop
auxprop_plugin: sql
mech_list: plain login cram-md5 digest-md5
sql_engine: mysql
sql_hostnames: 127.0.0.1
sql_user: mail
sql_passwd: mailPASSWORD
sql_database: maildb
sql_select: select clear from users where id='%u@%r' and enabled = 1 and (current_size<send_limit or send_limit=0)


vi /etc/postfix/mysql_mailbox.cf

user=mail
password=mailPASSWORD
dbname=maildb
table=users
select_field=maildir
where_field=id
hosts=127.0.0.1
additional_conditions = and enabled = 1 and (current_size<receive_limit or receive_limit=0)


vi /etc/postfix/mysql_maps.cf

user = mail
password = mailPASSWORD
hosts = 127.0.0.1
dbname = maildb
query = SELECT id,clear FROM users WHERE id='%s' and (current_size<send_limit or send_limit=0)


vi /etc/courier/authmysqlrc

 # insert this line
MYSQL_QUOTA_FIELD       concat(receive_limit,'S')


vi /etc/quotawarnmsg

Your mailbox is close to reaching it's set limit. In order to avoid any problems, please archive and/or remove some mails.
Once this limit is exceeded, you may not be able to send or receive any emails.

Kind regards,

Mailbox Management Team


vi /usr/local/bin/quota-check.sh

#!/bin/bash
# this script will update the quotas current_size in the database
for i in `ls /var/spool/mail/virtual`
do
        SIZE=`du -bs /var/spool/mail/virtual/$i |awk '{print $1}'`
        MBOX=$i
        UPD="update users set current_size=$SIZE where id='$MBOX';"
done
Right, now let's add new fields over to the users' database and add the size script to cron
 # adjust the 20 to something you like between 1-60. It is basically how often in minutes should we check the mailbox current-size and update it.
 # a value of 30 is every 30 minutes, which is fine for huge systems, for small ones 10 is ok, for medium ones, 20 should do. Adjust accordingly should you need to
echo -e "\n*/20 * * * * root /usr/local/bin/quota-check.sh" >> /etc/crontab

mysql -u root -p maildb
ALTER TABLE `users` ADD `send_limit` INT( 11 ) NOT NULL DEFAULT '0',
ADD `receive_limit` INT( 11 ) NOT NULL DEFAULT '0',
ADD `current_size` INT( 11 ) NOT NULL DEFAULT '0',
ADD INDEX ( send_limit, receive_limit, current_size );
exit;

/etc/init.d/cron restart
Restart affected services
/etc/init.d/postfix restart
/etc/init.d/amavis restart
/etc/init.d/clamav-daemon restart
/etc/init.d/clamav-freshclam restart
/etc/init.d/courier-authdaemon restart
/etc/init.d/courier-imap restart
/etc/init.d/courier-imap-ssl restart
/etc/init.d/spamassassin restart
Open the firewall ports
#smtp
iptables -I INPUT -p tcp -m tcp --dport 25 -j ACCEPT
#imap
iptables -I INPUT -p tcp -m tcp --dport 143 -j ACCEPT
#submission
iptables -I INPUT -p tcp -m tcp --dport 587 -j ACCEPT
#smtps
iptables -I INPUT -p tcp -m tcp --dport 465 -j ACCEPT
#imap-ssl
iptables -I INPUT -p tcp -m tcp --dport 993 -j ACCEPT
iptables-save > /etc/iptables-rules
You should now be able to connect to squirrelmail and phpmyadmin. You should be able to send email to users from and to squirrelmail webmail client. If you are having problems with imap/smtp, you'd need to start connecting to port 25 and 143 using telnet in order to test the configuration. Try logging in using test user account in imap and smtp to confirm all is fine as well.
Let's go through the testing part now:
Execute the below on the mail server (the one you just installed using the above configuration)
tail -f /var/log/mail.log
This will show the last 10 lines from the log file and then any new lines when they are added to the file. This way we will see what's going on as we go along with the testing.
Now, VERY IMPORTANT, to properly test this, you need to execute the below from a remote machine (one that's not localhost of the mail server). Should you be getting problems look at the output of the tail command on the mail server, it will give you details as to why it is failing (e.g. misspelled password in one of the mysql_*.cf files in /etc/postfix)
 # send email to local user
 # replace IP with the IP of the mail server
telnet 192.168.0.50 25
EHLO example3.com
MAIL FROM: test@example3.com
RCPT TO: webadmin@example.com
DATA
this test should deliver mail to test@example2.com as per aliases
.
QUIT

 # test open relay (send email to a non-local user without auth)
telnet 192.168.0.50 25
EHLO example3.com
MAIL FROM: webadmin@example2.com
RCPT TO: test@example4.com
 # at this point you should get error message saying relay is denied.
 # If not, something is wrong and you are and open relay!!!
QUIT

 # auth to smtp and send email to external user
 # execute the below 2 lines on any linux server with base64 installed
 # alternatively use a base64 online encoder to encode username and password
 # simple one is here: http://www.glonek.co.uk/base64/
echo -n "webadmin@example2.com" | base64
echo -n "apassword" | base64
 # and now back to the place where we are using telnet from to test
telnet 192.168.0.50 25
EHLO local
AUTH LOGIN
 # now put in the base64 encoded username here and press enter
 # now put in the base64 encoded password here and press enter
 # you should see login successfull, if not, troubleshoot
MAIL FROM: webadmin@example2.com
RCPT TO: #put some existing email address here that you can check
DATA
this test should pass and I should receive the email
.
QUIT

 # test imap auth
telnet 192.168.0.50 143
. LOGIN webadmin@example2.com apassword
 # you should see auth successfull
. SELECT INBOX
 # you should see more then 0 recent and more then 0 exist messages!
 # if not, troubleshoot smtp, as test number 1 failed
. LOGOUT

 # now login to the existing valid email address you given in test 3 and check if you received the messages
 # all should be fine and it should work. if not, retry test 3 with an email address that doesn't have spam tests

Backup MX Server Guide

Setup postfix for backup MX - do this on trhe backup MX server

vi /etc/postfix/main.cf
relay_domains = mysql:/etc/postfix/mysql_backups.cf
transport_maps = mysql:/etc/postfix/mysql_transport.cf
proxy_interfaces = 1.2.3.4 # should contain your IP address

vi /etc/postfix/mysql_backups.cf 
user=mail
password=apassword
dbname=maildb
table=backups
select_field=domain
where_field=domain
hosts=127.0.0.1
additional_conditions = and enabled = 1

vi /etc/postfix/mysql_transport.cf
user=mail
password=apassword
dbname=maildb
table=backups
select_field=transport
where_field=domain
hosts=127.0.0.1
additional_conditions = and enabled = 1

mysql -u root -p maildb
CREATE TABLE `backups` ( `pkid` smallint(6) NOT NULL auto_increment, `domain` varchar(128) NOT NULL default '', `transport` varchar(128) NOT NULL default ':[]', `enabled` smallint(6) NOT NULL default '1', PRIMARY KEY (`pkid`), UNIQUE KEY `domain` (`domain`) );

 # say you want to backup your friend's server called mail.friend.com
 # for domains friend1.com and friend2.com. You would insert this:
INSERT INTO backups (domain,transport) VALUES ('friend1.com' , ':[mail.friend.com]' ), ('friend2.com' , ':[mail.friend.com]' );

System Administration Guide

Firewall
To look at all the tables and their rules with verbose info, you would normally execute:

iptables -L -vn
You should see 3 tables: INPUT, OUTPUT and FORWARD. In our firewall we are interested in INPUT and OUTPUT. The first one controls which packets can go in and the latter, which can go out.
There are some states of packets that you can configure. As you noticed in step 1 of this tutorial, I am allowing all related and established connection packets. I do that as for example DNS lookups wouldn't work otherwise (especially that they are port 53 UDP). I also allow all connections on loopback interface (which is what you should always do). Then we just add rules on top of the chain to allow other ports (opening ports on firewall). The last rule is the one that gets executed if no other rules match, and that is our DROP policy (which DROPS the packets).
You can have -j ACCEPT to accept packets, -J DROP to drop packets or -j REJECT which will block like DROP, but it will respond to the connecting client with port-closed.
So, now some examples:
 # add a rule to open port 40000 tcp incoming (-I - add on top ; -A - add on bottom)
iptables -I INPUT -p tcp -m tcp --dport 40000 -j ACCEPT
 # and delete the rule
iptables -D INPUT -p tcp -m tcp --dport 40000 -j ACCEPT
 # noew add a rule to allow IP 192.168.0.66 to connect from port 123 to our IP 192.168.0.50 on our interface eth0 to port 333 udp
iptables -I INPUT -i eth0 -p udp -m udp --source 192.168.0.66 --sport 123 --destination 192.168.0.50 --dport 333 -j ACCEPT
 # block our computer (don't know why, but here goes) from connecting to IP 192.168.0.100 to port 53 from port 999
iptables -I OUTPUT -p udp -m udp --sport 999 --destination 192.168.0.100 --dport 53 -j DROP
 # instead of DROP, let's use reject (which will send proper reject message to connecting client)
iptables -A INPUT -j REJECT --reject-with icmp-host-unreachable
 # or to say that only this port is closed instead of whole host
iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable
 # see a list of available reject types
iptables -j REJECT --help
BIND9 DNS
BIND is quite easy to manage. If you want to add new zone (domain), you should just follow the instructions that we used in BIND Configuration manual for the example.com domain. Few things to note:

$TTL 1h - this is the time to live value for the domain cache. You should have something like 8h or 1d (8 hours / 1 day). 1 hour is a bit excessive. It's good for testing and migrations.

2009010910 ;serial - this is the serial number. After you make a change to the zone, you should always incement this to a higher value. I use yyyymmdd and then a number. This gives me up to 100 changes per day ensuring this will get incemented as it should

3600 ;refresh - refresh frequency of the slave server

3600 ;retry - retry frequency of slave in case of a communication problem

3600 ;expire - exiration time of the domain for the slave

3600 ;minimum TTL - minimum caching time in case of failed lookups

TXT and SPF records - these are the records that control mail spam using SPF from your domain. Check http://en.wikipedia.org/wiki/Sender_Policy_Framework for more details

Use your external IPs when configuring your domains. Using your LAN IP (if you use NAT) won't help external clients reach you using DNS lookups.

FTP
Information about the tables used by proftpd and how to fill them in is a follows. For the actual mysql commands, refer to examples in FTP configurations guide. Alternatively use phpmyadmin.
ftpuser Table:

The important columns are these (the others are handled by MySQL or Proftpd automatically, so do not fill these manually!):

    * userid: The name of the virtual Proftpd user (e.g. exampleuser).
    * passwd: The unencrypted (i.e., clear-text) password of the user.
    * uid: The userid of the ftp user you created at the end of step two (e.g. 2001).
    * gid: The groupid of the ftp group you created at the end of step two (e.g. 2001).
    * homedir: The home directory of the virtual Proftpd user (e.g. /home/www.example.com). If it does not exist, it will be created when the new user logs in the first time via FTP. The virtual user will be jailed into this home directory, i.e., he cannot access other directories outside his home directory.
    * shell: It is ok if you fill in /sbin/nologin here by default.

ftpquotalimits Table:

The important columns are these (the others are handled by MySQL or Proftpd automatically, so do not fill these manually!):

    * name: The name of the virtual Proftpd user (e.g. exampleuser).
    * quota_type: user or group. Normally, we use user here.
    * per_session: true or false. true means the quota limits are valid only for a session. For example, if the user has a quota of 15 MB, and he has uploaded 15 MB during the current session, then he cannot upload anything more. But if he logs out and in again, he again has 15 MB available. false means, that the user has 15 MB at, no matter if he logs out and in again.
    * limit_type: hard or soft. A hard quota limit is a never-to-exceed limit, while a soft quota can be temporarily exceeded. Normally you use hard here.
    * bytes_in_avail: Upload limit in bytes (e.g. 15728640 for 15 MB). 0 means unlimited.
    * bytes_out_avail: Download limit in bytes. 0 means unlimited.
    * bytes_xfer_avail: Transfer limit in bytes. The sum of uploads and downloads a user is allowed to do. 0 means unlimited.
    * files_in_avail: Upload limit in files. 0 means unlimited.
    * files_out_avail: Download limit in files. 0 means unlimited.
    * files_xfer_avail: Tranfer limit in files. 0 means unlimited.

The ftpquotatallies table is used by Proftpd internally to manage quotas so you do not have to make entries there!
Mail / Backup MX
Ok, first of all, here is the table description output:
mysql describe aliases;
+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| pkid        | smallint(3)  | NO   | PRI | NULL    | auto_increment |
| mail        | varchar(120) | NO   | UNI |         |                |
| destination | varchar(120) | NO   |     |         |                |
| enabled     | tinyint(1)   | NO   |     | 1       |                |
+-------------+--------------+------+-----+---------+----------------+
4 rows in set (0.02 sec)

mysql describe domains;
+-----------+--------------+------+-----+----------+----------------+
| Field     | Type         | Null | Key | Default  | Extra          |
+-----------+--------------+------+-----+----------+----------------+
| pkid      | smallint(6)  | NO   | PRI | NULL     | auto_increment |
| domain    | varchar(120) | NO   |     |          |                |
| transport | varchar(120) | NO   |     | virtual: |                |
| enabled   | tinyint(1)   | NO   |     | 1        |                |
+-----------+--------------+------+-----+----------+----------------+
4 rows in set (0.00 sec)

mysql describe users;
+-----------------+----------------------+------+-----+-------------------------+-------+
| Field           | Type                 | Null | Key | Default                 | Extra |
+-----------------+----------------------+------+-----+-------------------------+-------+
| id              | varchar(128)         | NO   | PRI |                         |       |
| name            | varchar(128)         | NO   |     |                         |       |
| uid             | smallint(5) unsigned | NO   |     | 5000                    |       |
| gid             | smallint(5) unsigned | NO   |     | 5000                    |       |
| home            | varchar(255)         | NO   |     | /var/spool/mail/virtual |       |
| maildir         | varchar(255)         | NO   |     | blah/                   |       |
| enabled         | tinyint(3) unsigned  | NO   |     | 1                       |       |
| change_password | tinyint(3) unsigned  | NO   |     | 1                       |       |
| clear           | varchar(128)         | NO   |     | ChangeMe                |       |
| crypt           | varchar(128)         | NO   |     | sdtrusfX0Jj66           |       |
| quota           | varchar(255)         | NO   |     |                         |       |
| procmailrc      | varchar(128)         | NO   |     |                         |       |
| spamassassinrc  | varchar(128)         | NO   |     |                         |       |
+-----------------+----------------------+------+-----+-------------------------+-------+
13 rows in set (0.00 sec)
Now, in order to add a new domain, just insert a new entry, only filling in the "domain" row. The rest is set in defaults.
In order to add aliases only fill in mail and destination. Mail is the email address we are accepting and destination is where we are delivering/relaying it to
To add a user, fill in ID (email address here), name (Full Name), maildir (which directory mail is delivered to), crypt and optionally clear password, quota (the limit of the mailbox, leave 0 for no limit).
Here is the template to use:
INSERT INTO domains (domain) VALUES ('domain.tld');

INSERT INTO aliases (mail,destination) VALUES
 ('@domain.tld','email@address'),
 ('postmaster@domain.tld','email@address'),
 ('abuse@domain.tld','email@address');

INSERT INTO users (id,name,maildir,clear) VALUES
 ('email@address','short description','foldername/',encrypt('password')); 

INSERT INTO aliases (mail,destination) VALUES
 ('email@address','email@address');
SpamAssassin User Preferences
Right, now that we gout spamassassin in mysql with userpreferences configurable by username/domain, we should configure some stuff for our domains.
The lookup normally goes like this: USERNAME(EMAIL) CONFIGURATION EXIST? => DOMAIN-WIDE CONFIGURATION EXIST? => GLOBAL CONFIGURATION EXIST?
As an example, required_hits (score) configuration part, once email arrives for a selected user, bayes will check if that user has required_hits configuration. If not, it will check for this domain configuration, if not found there, then only it will look for required_hits in the GLOBAL configuration.
The advantages if this are awsome! I can have 20 customers with their comains all using global configuration. Then I can also have 3 picky customers with domains that want separate spamassassin configuration. And then I can have the manager of 2 of those picky customers have their own spamassassin configuration as well. Get the point?
Now, I will give you the example MySQL command line queries to add configuration and then an example output for an example domain describing the entries of my table. You can then add/remove entries, based on your preferences.
mysql -u spamassassin -p spamassassin
 # show all user preferences
select * from userpref;
 # show all user preferences for anyone ending with domain.com (case insensitive)
select * from userpref where username like "%domain.com";
 # describe the table
describe userpref;
 # insert an entry
insert into userpref (username,preference,value) values ('%domain.com','use_bayes','1');
 # change an entry
update userpref set value=0 where username='someone@domain.com' and preference='use_bayes';
 # remove an entry
delete from userpref where username='someone@domain.com' and preference='use_bayes';

 ### and now the example configuration you could use
 ### !!!make sure you google for each line if you are unsure of the meaning before you use them!!!
 ### for more options, consult spamassassin configuration manual, all is there and it is outside of the scope of this tutorial
---------------------------------------------------------------------------
| username             | preference              | value                  |
+----------------------+-------------------------+------------------------+
| $GLOBAL              | required_hits           | 4.00                   |
| $GLOBAL              | subject_tag             | [SPAM-_HITS_]-         |
| $GLOBAL              | score USER_IN_WHITELIST | -10                    |
| $GLOBAL              | whitelist_from          | *@example.com          |
| $GLOBAL              | score USER_IN_BLACKLIST | 10                     |
| $GLOBAL              | report_safe             | 0                      |
| $GLOBAL              | use_razor2              | 1                      |
| $GLOBAL              | use_pyzor               | 1                      |
| $GLOBAL              | use_dcc                 | 1                      |
| $GLOBAL              | skip_rbl_checks         | 1                      |
| $GLOBAL              | use_bayes               | 1                      |
| $GLOBAL              | ok_locales              | en                     |
| $GLOBAL              | ok_languages            | en                     |
| $GLOBAL              | whitelist_from          | *@xaw.com              |
| $GLOBAL              | use_auto_whitelist      | 1                      |
| $GLOBAL              | rewrite_header Subject  | [SPAM-_HITS_]-         |
| %example.com         | use_bayes               | 1                      |
| %example.com         | whitelist_from          | *@hp.com               |
| %example.com         | score USER_IN_WHITELIST | -10                    |
| %example.com         | score USER_IN_BLACKLIST | 10                     |
| %example.com         | ok_locales              | en                     |
| %example.com         | ok_languages            | en                     |
| %example.com         | use_razor2              | 1                      |
| %example.com         | use_pyzor               | 1                      |
| %example.com         | use_dcc                 | 1                      |
| %example.com         | skip_rbl_checks         | 1                      |
| %example.com         | report_safe             | 0                      |
| %example.com         | required_hits           | 5                      |
| webadmin@example.com | use_razor2              | 1                      |
| webadmin@example.com | use_bayes               | 1                      |
| webadmin@example.com | use_pyzor               | 1                      |
| webadmin@example.com | use_dcc                 | 1                      |
| webadmin@example.com | skip_rbl_checks         | 0                      |
| webadmin@example.com | whitelist_from          | *@paypal.com           |
| webadmin@example.com | ok_languages            | en                     |
| webadmin@example.com | ok_locales              | en                     |
| webadmin@example.com | score USER_IN_WHITELIST | -10                    |
| webadmin@example.com | score USER_IN_BLACKLIST | 10                     |
| webadmin@example.com | required_hits           | 4.10                   |
| webadmin@example.com | report_safe             | 0                      |
| webadmin@example.com | use_auto_whitelist      | 1                      |
| webadmin@example.com | rewrite_header Subject  | [SPAM-_HITS_]-         |
---------------------------------------------------------------------------