Integrating Exim, IMAP, and webmail in Gentoo

The aim is to provide a mail server that serves any number of domains, and integrates access from MTAs and web browsers. The environment uses Gentoo Linux, Exim, Courier IMAP and SquirrelMail. The target users are clubs, societies, small companies and other smallish, relatively low load setups.

The server I have built has no other services running on it. Exim is configured to check for Viruses and spam. Virus infected mails are dropped, spam is flagged, but delivered. MySQL is used to hold the list of domains and users. All components use the database to obtain authentication and authorisation data.

Building the basic system

To build the basic system install a suitable Gentoo stage file, set your USE flags in /etc/make.conf:
USE="amavis apache2 exim exiscan exiscan-acl filter imap maildir mbox mysql mysqli php server spell spf srs syslog threads vhosts xml -fortran ssl"
and then do 'emerge -n world' using this world file:
app-admin/procinfo
www-servers/apache
sys-libs/gpm
net-mail/courier-imap
mail-filter/spamassassin
www-apache/mod_auth_mysql
mail-client/squirrelmail
dev-db/mysql
app-admin/apache-tools
app-admin/sudo
mail-client/mailx
net-misc/rdate
dev-libs/openssl
app-portage/gentoolkit
app-portage/mirrorselect
dev-lang/php
sys-process/vixie-cron
app-editors/nano
dev-perl/Archive-Zip
sys-apps/xinetd
dev-perl/IP-Country
sys-boot/grub
sys-apps/parted
app-editors/vim
net-analyzer/netcat
mail-filter/razor
dev-python/pyzor
app-admin/sysklogd
dev-perl/Mail-ClamAV
net-misc/whois
net-analyzer/nmap
dev-perl/DBD-mysql
mail-mta/exim
sys-kernel/vanilla-sources
sys-process/cronbase
net-misc/openssh
sys-apps/module-init-tools
net-dns/bind
app-portage/ufed
app-text/rcs
www-apache/mod_perl
dev-perl/Mail-SPF-Query
net-firewall/iptables
sys-apps/less
sys-apps/baselayout
net-misc/ntp
app-antivirus/clamav
net-nds/portmap
dev-perl/MIME-tools
net-analyzer/tcpdump

Configuring Linux and core utilities

Use rc-update to configure your startup scripts as follows:

             apache2 |      default                  
            bootmisc | boot                          
             checkfs | boot                          
           checkroot | boot                          
               clamd |      default                  
               clock | boot                          
         consolefont | boot                          
     courier-authlib |      default                  
       courier-imapd |      default                  
   courier-imapd-ssl |      default                  
   courier-pop3d-ssl |      default                  
                exim |      default                  
            hostname | boot                          
            iptables |      default                  
             keymaps | boot                          
               local |      default nonetwork        
          localmount | boot                          
             modules | boot                          
               mysql |      default                  
              net.lo | boot                          
            netmount |      default                  
                 nfs |      default                  
                ntpd |      default                  
           rmnologin | boot                          
               spamd |      default                  
                sshd |      default                  
            sysklogd | boot                          
             urandom | boot                          
          vixie-cron |      default                  
              xinetd |      default                  

The mail is going to live in /virtual. If this is a separate disk partition you can increase system safety by adding

	mount -o ro,remount /
to /etc/conf.d/local.start.

IPTables

Here is my iptables setup, as stored in /etc/conf.d/local.start:

PRIVATE_NET=172.16.0.0/12
LOCAL_NET=62.49.238.224/28
IPTABLES="iptables "
$IPTABLES -F INPUT
$IPTABLES -A INPUT -p udp -m multiport --dports 1024:65535 -j ACCEPT
$IPTABLES -A INPUT -p tcp -m tcp --dport domain -j ACCEPT
$IPTABLES -A INPUT -p udp -m multiport --dports domain,ntp -j ACCEPT
$IPTABLES -A INPUT -i lo -j ACCEPT
$IPTABLES -A INPUT -p tcp -m multiport \
	--dports ident,ssh,smtp,http,ntp,imap,pop3,https,pop3s,imaps \
	-j ACCEPT $IPTABLES -A INPUT -p tcp -s $LOCAL_NET -j ACCEPT
# This next line of for Microsoft software, which is unable to
# use the standard ports.
$IPTABLES -A INPUT -p tcp -m multiport --dports 465,587 -j ACCEPT
$IPTABLES -A INPUT -p icmp -s $PRIVATE_NET -j ACCEPT
$IPTABLES -A INPUT -p tcp -s $PRIVATE_NET -j ACCEPT
$IPTABLES -A INPUT -p icmp -s $LOCAL_NET -j ACCEPT
$IPTABLES -A INPUT -p tcp -s $LOCAL_NET -j ACCEPT
$IPTABLES -A INPUT -p tcp -m state --state RELATED,ESTABLISHED -j ACCEPT
$IPTABLES -P INPUT DROP
The variable LOCAL_NET covers the set of IP adresses that I own and that are visible on the internet. PRIVATE_NET covers those IP addresses that can only be used internally to my network.

NTPD

Setting 'server pool.ntp.org' in /etc/ntp.conf should be adequate. If you have a private network with your own NTP server you may prefer to have all your other machines pointing to that private server.

Configuring Clam AV and SpamAssassin

Clam AV

You only need clamd and freshclam, not amavisd.

Clamd can talk over either a Unix or a TCP socket. Do not try to use the Unix socket, you will get all sorts of permissions problems. Instead, use the TCP socket: it may be slightly less efficient, but it works with no problems. Ensure that your exim.conf matches whatever you configure here.

You might get away with using the Unix socket if you make user 'mail' part of the 'clamav' group, and make user 'clamav' part of the 'mail' group, but I have not tested this configuration. Failing this you have to make 'clamd' run as user 'mail, and ensure that all its files are owned by 'mail'.

Configuring MySQL

First, set a root password in MySQL. Next, create the necessary DB and table structure:

create database Mail;
use Mail;
-- Which domains do we service
create table Domains (domain    varchar(255)) max_rows=100
        if not exists Domains;

-- The users in each domain
create table if not exists Users(
        user            varchar(255) primary key,-- $user@$domain
        password        varchar(255) not null,  -- clear text password
or 'cryptpwd'
        forward         varchar(1024),          -- where to
forward all mail
        expiry          bigint not null default 0,
        quota           varchar(64),
        homedir         varchar(1024) not null,
        maildir         varchar(1024) not null,
        uid             int not null default 8, -- User 'mail'
        gid             int not null default 12,-- Group 'mail'
        realName        varchar(255),
        force_change_pwd smallint default 1     -- New users must
change their passwords
        )  
max_rows=1000;

grant select on Mail.* 
	to mail@localhost identified by 'MailPassword';
grant select on Mail.* 
	to mail@127.0.0.1 identified by 'MailPassword';
grant update (password,force_change_pwd) on Mail.Users 
        to mail@localhost identified by 'MailPassword';
grant update (password,force_change_pwd) on Mail.Users
        to mail@127.0.0.1 identified by 'MailPassword';

We store passwords in clear, but encourage users to use a form of authentication that does not transmit clear-text passwords over the net, either CRAM-MD5 or SSL/TLS. CRAM-MD5 requires that we store clear-text passwords. For PLAIN and LOGIN authentication we can use either plain-text or encrypted passwords.

Creating SSL certificates

I shall deal here only with creating a self-signed certificate. This is adequate for small setups so long as your users are aware that they will get a warning message the first time that they connect to your system and that they need to explictly accept the certificate.

openssl genrsa  -out privkey.pem 2048
openssl req -new -x509 -key privkey.pem -out cacert.pem -days 3650
These commands will generate a private key in privkey.pem and a self-signed certificate valid for 10 years (3650 days) in cacert.pem. You can copy these files to wherever your configuration files require you to have them. They will be needed for Apache SSL, Exim SSL/TLS, IMAPS (IMAP with SSL/TLS) and for POP3 with SSL/TLS.

As you generate the files, openssl will prompt you for various data. The CN (common name) should be the true name of your server, most of the other values are obvious but their actual values are not terribly important. Just make them sensible so that you do not confuse your users.

Other sources

Configuring Exim

The local host list is hard coded in exim.conf, but the local domain list is obtained from a DB query. Authentication of users is done with the DB.

Each domain may have an alias file that is checked before we check for local user names. Aliases are private to each domain; an alias in one domain does not interfere with an identical alias in another domain.

Ensure that communication with Clamd for virus checking is done over the TCP socket.

SMTP Send requires that the user be authenticated. CRAM-MD5 authentication is permittted over an unencrypted channel; PLAIN and LOGIN authentication require encryption, either SSL or TLS.

The Exim configuration file is too large to include here verbatim. Instead, here is the diff between my exim.conf and the original distribution file:

59,76d58
< hide mysql_servers = localhost/myDatabase/myUser/myPassword
< 
< # Exim only cares whether this query succeeds, not what it returns
< DOMAIN_LIST   = select domain from Domains where domain='$domain'
< 
< # In these cases we *do* want the actual data
< USER_QUERY    = select forward, homedir from Users \
<                       where user = \
<                       concat(lcase('$local_part'), '@', lcase('$domain'))
< PLAINAUTH_QUERY       = select 1 from Users \
<                       where user = lcase('$auth2') \
<                       and password='$auth3'
< LOGINAUTH_QUERY       = select 1 from Users \
<                       where user = lcase('$auth1') \
<                       and password='$auth2'
< CRAMMD5AUTH_QUERY = select password from Users \
<                       where user = lcase('$auth1') 
< 
78d59
< domainlist virtual_domains    = mysql;DOMAIN_LIST
81d61
< #hostlist   relay_from_hosts = 127.0.0.1 : 172.16.0.0/12 : 62.49.238.224/28
142,145c122
< # av_scanner=clamd:/var/run/clamav/clamd.sock
< av_scanner = clamd:127.0.0.1 3310
< # for sophos scanner
< # av_scanner = cmdline: /usr/local/bin/sweep -all -archive -ss %s: found:'(.+)' 
---
> # av_scanner = clamd:/tmp/clamd
153c130
< spamd_address = 127.0.0.1 783
---
> # spamd_address = 127.0.0.1 783
166c143
< tls_advertise_hosts = *
---
> # tls_advertise_hosts = *
174,175c151,152
< tls_certificate = /etc/ssl/exim.crt
< tls_privatekey = /etc/ssl/exim.pem
---
> # tls_certificate = /etc/ssl/exim.crt
> # tls_privatekey = /etc/ssl/exim.pem
186,187c163,164
< daemon_smtp_ports = 25 : 465 : 587
< tls_on_connect_ports = 465
---
> # daemon_smtp_ports = 25 : 465 : 587
> # tls_on_connect_ports = 465
314c291
< split_spool_directory = true
---
> # split_spool_directory = true
363c340
<           domains       = +local_domains : +virtual_domains
---
>           domains       = +local_domains
378c355
<           domains       = !+local_domains : !+virtual_domains
---
>           domains       = !+local_domains
386c363
<           domains       = +local_domains :!+virtual_domains
---
>           domains       = +local_domains
427c404
<           domains = +local_domains : +relay_to_domains : +virtual_domains
---
>           domains = +local_domains : +relay_to_domains
443,445c420,422
<   deny    message       = rejected because $sender_host_address is in a blacklist at $dnslist_domain\n$dnslist_text
<              dnslists = dnsbl.sorbs.net : relays.ordb.org : spam.dnsrbl.net
<   
---
>   # deny    message       = rejected because $sender_host_address is in a black list at $dnslist_domain\n$dnslist_text
>   #         dnslists      = black.list.example
>   #
481,482c458,459
< deny    malware    = *
<         message    = This message contains a virus ($malware_name).
---
>   # deny    malware    = *
>   #         message    = This message contains a virus
($malware_name).
488,499c465,469
< 
< # reject spam at high scores (> 8)
< deny  spam = nobody:true
<       condition = ${if >{$spam_score_int}{8}{1}{0}}
<       message = This message scored $spam_score spam points.
< 
< 
< warn    spam       = nobody:true
<            add_header = X-Spam_score: $spam_score\n\
<                         X-Spam_score_int: $spam_score_int\n\
<                         X-Spam_bar: $spam_bar\n\
<                         X-Spam_report: $spam_report
---
>   # warn    spam       = nobody
>   #         add_header = X-Spam_score: $spam_score\n\
>   #                      X-Spam_score_int: $spam_score_int\n\
>   #                      X-Spam_bar: $spam_bar\n\
>   #                      X-Spam_report: $spam_report
547c517
<   domains = ! +local_domains : !+virtual_domains
---
>   domains = ! +local_domains
551a522
> 
555,571d525
< virtual_user:
<       driver = accept
<       domains = +virtual_domains
<       router_home_directory=${extract {homedir}{${lookup mysql{USER_QUERY}}}{$value}fail}
<       condition = ${if gt{$home}{}}
<       transport       = virtual_delivery
< 
< virtual_alias:
<         driver = redirect
<         domains = +virtual_domains
<         local_part_suffix = +*
<         local_part_suffix_optional
<       data = ${if exists{/virtual/${domain}/mail/aliases}{${lookup{$local_part}lsearch*{/virtual/${domain}/mail/aliases}}}}
<         qualify_preserve_domain 
<         file_transport = address_file
<         pipe_transport = address_pipe
< 
597,598c551
<   domains = +local_domains
<   data = ${lookup{$local_part}lsearch*{/etc/mail/aliases}}
---
>   data = ${lookup{$local_part}lsearch{/etc/mail/aliases}}
687,699d639
< # Courier-imap assumes that the delivery will be to $homedir/Maildir
< virtual_delivery:
<     driver = appendfile
<     directory=${extract {homedir}{${lookup mysql{USER_QUERY}}}{$value}fail}/Maildir
<     delivery_date_add
<     envelope_to_add
<     return_path_add
<     user =  mail
<     maildir_format
<     maildir_use_size_file
<     quota = ${lookup mysql{select quota from Domains where domain='$domain'}{$value}{20M}}
<     quota_warn_threshold = 75%
< 
762,765c702
< # Immediate bounce of over-quota messages
< *                     quota
< #Catchall rule for other errors
< *                     *           F,2h,15m; G,16h,1h,1.5; F,4d,6h
---
> *                      *           F,2h,15m; G,16h,1h,1.5; F,4d,6h
808,813c745,750
< PLAIN:
<   driver                     = plaintext
<   server_set_id              = $2
<   server_prompts             = :
<   server_condition = ${lookup mysql{PLAINAUTH_QUERY}{$value}fail}
<   server_advertise_condition = ${if def:tls_cipher }
---
> #PLAIN:
> #  driver                     = plaintext
> #  server_set_id              = $auth2
> #  server_prompts             = :
> #  server_condition           = Authentication is not yet configured
> #  server_advertise_condition = ${if def:tls_cipher }
820,830c757,762
< LOGIN:
<   driver                     = plaintext
<   server_set_id              = $1
<   server_prompts             = <| Username: | Password:
<   server_condition = ${lookup mysql{LOGINAUTH_QUERY}{$value}fail}
<   server_advertise_condition = ${if def:tls_cipher }
< 
< lookup_cram:
<     driver = cram_md5
<     public_name = CRAM-MD5
<     server_secret = ${lookup mysql{CRAMMD5AUTH_QUERY}{$value}fail}
---
> #LOGIN:
> #  driver                     = plaintext
> #  server_set_id              = $auth1
> #  server_prompts             = <| Username: | Password:
> #  server_condition           = Authentication is not yet configured
> #  server_advertise_condition = ${if def:tls_cipher }
832d763
<     server_set_id = $1

Configuring IMAP

Basic configuration

First, configure /etc/courier-imap/imapd. Ensure that IMAP_CAPABILITY contains 'AUTH=CRAM-MD5'. I ensure that rubbish does not lie around for too long by including

IMAP_EMPTYTRASH=Trash:7,Sent:30,Spam:14
The MAILDIR and MAILDIRPATH are both set to 'Maildir'. A common alternative is '.maildir', but you must ensure that courier-imap, courier-pop3, exim and the database all specify the same name.

Configuring to use SSL/TLS

For SSL you have to alter /etc/courier-imap/imapd-ssl. About all you need to do is to alter TLS_TRUSTCERTS to

TLS_TRUSTCERTS=/etc/ssl/certs/
which is where openssl has put all the standard certificates and, again, to set up MAILDIR and MAILDIRPATH.

Copy the private key and certificate files that you generated to wherever your config file says they need to go.

Configuring to authenticate using the DB

In /etc/courier/authlib ensure that you have

authmodulelist="authmysql "
Then in authmysqlrc you need
MYSQL_SERVER            localhost
MYSQL_USERNAME          mail
MYSQL_PASSWORD          MailPassword

# The socket address varies from installation to installation,
# check that it is right for your system.
MYSQL_SOCKET            /var/run/mysqld/mysqld.sock
MYSQL_PORT              3306

MYSQL_DATABASE          Mail
MYSQL_USER_TABLE        Users
MYSQL_CLEAR_PWFIELD     password
MYSQL_UID_FIELD         uid
MYSQL_GID_FIELD         gid
MYSQL_LOGIN_FIELD       user
MYSQL_HOME_FIELD        homedir
MYSQL_NAME_FIELD        realName
MYSQL_MAILDIR_FIELD     maildir
These lines are scatterd though the file. You have to find them and edit them appropriately. We do not actually care about the uid and gid fields, but Courier-imap insists on having them.

Configuring POP3

Basic configuration

Include

POP3AUTH="CRAM-MD5"
POP3AUTH_ORIG="PLAIN LOGIN CRAM-MD5 CRAM-SHA1 CRAM-SHA256"
POP3AUTH_TLS_ORIG="LOGIN PLAIN"
POP3DSTART=YES
MAILDIRPATH=Maildir
MAILDIR=Maildir
at the appropriate places in /etc/courier-imap/pop3d.

Configuring to authenticate using the DB

The configuration you set up for IMAP will also apply for POP3; there is no need to do anything extra.

Configuring to use SSL/TLS

Apart from making sure that the certificate file exists and is enabled in /etc/courier-imap/pop3d-ssl (copy the files you made earlier) there is almost nothing to do. You can decide for yourself whether to force TLS for all users, or to allow also-non-encrypted sessions. This is configured with POP3_TLS_REQUIRED.

As always, make sure you have MAILDIR and MAILDIRPATH properly configured.

Configuring Apache and SquirrelMail

SquirrelMail

Run /usr/share/webapps/squirrelmail/$version/htdocs/config/conf.pl to configure your system.

Plugins

You will need the administrator, compatibility and change_sqlpass plugins. The last two are not part of the standard Squirrelmail package and you will have to download them separately from the plugins repository. You may also decide to install other plugins such as calendar, squirrelspell and filters. 'Filters' allows users to set up rules to filter their mail and move it to private folders or perform other actions on it.

Apache 2

Basic configuration

For security, http protocol is redirected to https, however for initial testing I did everything on http. Note that all my domains are named explictly in the configuration blocks.

<VirtualHost mail.mankin.org.uk:80>
	ServerName mail.mankin.org.uk
	Redirect / https://mail.mankin.org.uk/
</VirtualHost>

<IfDefine SSL>
<IfDefine SSL_DEFAULT_VHOST>
<IfModule ssl_module>
# see bug #178966 why this is in here

# When we also provide SSL we have to listen to the HTTPS port
# Note: Configurations that use IPv6 but not IPv4-mapped addresses need two
# Listen directives: "Listen [::]:443" and "Listen 0.0.0.0:443"

<VirtualHost mail.mankin.org.uk:443>
	ServerName mail.mankin.org.uk
	ScriptAlias /cgi-bin /usr/share/webapps/squirrelmail/1.4.10a-r2/hostroot/cgi-bin
	Alias /icons /usr/share/webapps/squirrelmail/1.4.10a-r2/hostroot/icons
	DocumentRoot	/usr/share/webapps/squirrelmail/1.4.10a-r2/htdocs/
	
	<Location /cgi-bin>
	    Order deny,allow
	    Deny from all
	    Allow from 62.49.238.224/28
	    Allow from 172.16.0.0/12
	    Allow from 127.0.0.1
	</Location>
	<Location />
	    Options Indexes
	    DirectoryIndex index.php
	    Order allow,deny
	    Allow from All
	</Location>
	<Location /po>
	    Order deny,allow
	    Deny from all
	</Location>
	<Location /config>
	    Order deny,allow
	    Deny from all
	</Location>
	ErrorLog /var/log/apache2/ssl_error_log

	<IfModule log_config_module>
		TransferLog /var/log/apache2/ssl_access_log
	</IfModule>

	## SSL Engine Switch:
	# Enable/Disable SSL for this virtual host.
	SSLEngine on

	## SSL Cipher Suite:
	# List the ciphers that the client is permitted to negotiate.
	# See the mod_ssl documentation for a complete list.
	SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL

	## Server Certificate:
	# Point SSLCertificateFile at a PEM encoded certificate. If the certificate
	# is encrypted, then you will be prompted for a pass phrase. Note that a 
	# kill -HUP will prompt again. Keep in mind that if you have both an RSA
	# and a DSA certificate you can configure both in parallel (to also allow
	# the use of DSA ciphers, etc.)
	SSLCertificateFile /etc/apache2/ssl/server.crt
	#SSLCertificateFile /etc/apache2/ssl/server-dsa.crt
	
	## Server Private Key:
	# If the key is not combined with the certificate, use this directive to
	# point at the key file. Keep in mind that if you've both a RSA and a DSA
	# private key you can configure both in parallel (to also allow the use of
	# DSA ciphers, etc.)
	SSLCertificateKeyFile /etc/apache2/ssl/server.key
	#SSLCertificateKeyFile /etc/apache2/ssl/server-dsa.key
	
	## Server Certificate Chain:
	# Point SSLCertificateChainFile at a file containing the concatenation of 
	# PEM encoded CA certificates which form the certificate chain for the
	# server certificate. Alternatively the referenced file can be the same as
	# SSLCertificateFile when the CA certificates are directly appended to the
	# server certificate for convinience.
	#SSLCertificateChainFile /etc/apache2/ssl/ca.crt
	
	## Certificate Authority (CA):
	# Set the CA certificate verification path where to find CA certificates
	# for client authentication or alternatively one huge file containing all
	# of them (file must be PEM encoded).
	# Note: Inside SSLCACertificatePath you need hash symlinks to point to the
	# certificate files. Use the provided Makefile to update the hash symlinks
	# after changes.
	#SSLCACertificatePath /etc/apache2/ssl/ssl.crt
	#SSLCACertificateFile /etc/apache2/ssl/ca-bundle.crt
	
	## Certificate Revocation Lists (CRL):
	# Set the CA revocation path where to find CA CRLs for client authentication
	# or alternatively one huge file containing all of them (file must be PEM 
	# encoded).
	# Note: Inside SSLCARevocationPath you need hash symlinks to point to the
	# certificate files. Use the provided Makefile to update the hash symlinks
	# after changes.
	#SSLCARevocationPath /etc/apache2/ssl/ssl.crl
	#SSLCARevocationFile /etc/apache2/ssl/ca-bundle.crl
	
	## Client Authentication (Type):
	# Client certificate verification type and depth. Types are none, optional,
	# require and optional_no_ca. Depth is a number which specifies how deeply
	# to verify the certificate issuer chain before deciding the certificate is
	# not valid.
	#SSLVerifyClient require
	#SSLVerifyDepth  10
	
	## Access Control:
	# With SSLRequire you can do per-directory access control based on arbitrary
	# complex boolean expressions containing server variable checks and other
	# lookup directives. The syntax is a mixture between C and Perl. See the
	# mod_ssl documentation for more details.
	#<Location />
	#	#SSLRequire (    %{SSL_CIPHER} !~ m/^(EXP|NULL)/ \
	#	and %{SSL_CLIENT_S_DN_O} eq "Snake Oil, Ltd." \
	#	and %{SSL_CLIENT_S_DN_OU} in {"Staff", "CA", "Dev"} \
	#	and %{TIME_WDAY} >= 1 and %{TIME_WDAY} <= 5 \
	#	and %{TIME_HOUR} >= 8 and %{TIME_HOUR} <= 20       ) \
	#	or %{REMOTE_ADDR} =~ m/^192\.76\.162\.[0-9]+$/
	#</Location>

	## SSL Engine Options:
	# Set various options for the SSL engine.

	## FakeBasicAuth:
	# Translate the client X.509 into a Basic Authorisation. This means that the
	# standard Auth/DBMAuth methods can be used for access control. The user 
	# name is the `one line' version of the client's X.509 certificate. 
	# Note that no password is obtained from the user. Every entry in the user 
	# file needs this password: `xxj31ZMTZzkVA'.
	
	## ExportCertData:
	# This exports two additional environment variables: SSL_CLIENT_CERT and 
	# SSL_SERVER_CERT. These contain the PEM-encoded certificates of the server
	# (always existing) and the client (only existing when client 
	# authentication is used). This can be used to import the certificates into
	# CGI scripts.
	
	## StdEnvVars:
	# This exports the standard SSL/TLS related `SSL_*' environment variables. 
	# Per default this exportation is switched off for performance reasons, 
	# because the extraction step is an expensive operation and is usually 
	# useless for serving static content. So one usually enables the exportation
	# for CGI and SSI requests only.

	## StrictRequire:
	# This denies access when "SSLRequireSSL" or "SSLRequire" applied even under
	# a "Satisfy any" situation, i.e. when it applies access is denied and no
	# other module can change it.

	## OptRenegotiate:
	# This enables optimized SSL connection renegotiation handling when SSL 
	# directives are used in per-directory context.
	#SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire
	<FilesMatch "\.(cgi|shtml|phtml|php)$">
		SSLOptions +StdEnvVars
	</FilesMatch>

	<Directory "/var/www/localhost/cgi-bin">
		SSLOptions +StdEnvVars
	</Directory>

	## SSL Protocol Adjustments:
	# The safe and default but still SSL/TLS standard compliant shutdown
	# approach is that mod_ssl sends the close notify alert but doesn't wait
	# for the close notify alert from client. When you need a different
	# shutdown approach you can use one of the following variables:

	## ssl-unclean-shutdown:
	# This forces an unclean shutdown when the connection is closed, i.e. no
	# SSL close notify alert is send or allowed to received.  This violates the
	# SSL/TLS standard but is needed for some brain-dead browsers. Use this when
	# you receive I/O errors because of the standard approach where mod_ssl
	# sends the close notify alert.

	## ssl-accurate-shutdown:
	# This forces an accurate shutdown when the connection is closed, i.e. a
	# SSL close notify alert is send and mod_ssl waits for the close notify
	# alert of the client. This is 100% SSL/TLS standard compliant, but in
	# practice often causes hanging connections with brain-dead browsers. Use
	# this only for browsers where you know that their SSL implementation works
	# correctly. 
	# Notice: Most problems of broken clients are also related to the HTTP 
	# keep-alive facility, so you usually additionally want to disable 
	# keep-alive for those clients, too. Use variable "nokeepalive" for this.
	# Similarly, one has to force some clients to use HTTP/1.0 to workaround
	# their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and
	# "force-response-1.0" for this.
	<IfModule setenvif_module>
		BrowserMatch ".*MSIE.*" \
			nokeepalive ssl-unclean-shutdown \
			downgrade-1.0 force-response-1.0
	</IfModule>

	## Per-Server Logging:
	# The home of a custom SSL log file. Use this when you want a compact 
	# non-error SSL logfile on a virtual host basis.
	<IfModule log_config_module>
		CustomLog /var/log/apache2/ssl_request_log \
			"%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
	</IfModule>
</VirtualHost>
</IfModule>
</IfDefine>
</IfDefine>

# vim: ts=4 filetype=apache

Configuring to use SSL

Copy the private key and certificate files that you generated to wherever your config file says they need to go.

Utility scripts

If you are going to permit access to these scripts from outside your local network you will have to change the Allow directives for /cgi-bin in the Apache configuration.

Adding a domain

#! /usr/bin/perl
# Author: Raphael Mankin
use strict;
use DBD::mysql;
use File::Path;

use constant DB		=> "Mail";
use constant HOST	=> "localhost";
use constant USER	=> "root";
use constant PWD	=> "RootPassword";

use constant MAIL	=> "mail";

my ($login, $passwd, $uid, $gid) = getpwnam(MAIL) or
	die "MAIL user not found";;

$|	= 1;
my $dsn	= "DBI:mysql:database=".DB.";host=".HOST;
my $dbh	= DBI->connect($dsn, USER, PWD)
	or die "Cannot connect to database";


print "Domain: ";
my $domain	= <>;
chomp $domain;
$domain	= lc $domain;

my $sth	= $dbh->prepare("select domain from Domains where domain='$domain'");
$sth->execute();
my $res;
while (my $ref	= $sth->fetchrow_hashref()) {
	$res	= $ref->{domain};
}
die "Domain $domain already exists" if $res;


my $domainDir	= "/virtual/$domain/mail";
mkpath $domainDir unless -d $domainDir;
my $aliasFile	= "$domainDir/aliases";
unless (-f $aliasFile) {
    open(FH, ">", $aliasFile) or die "Cannot create alias file: $aliasFile";
    print FH "#Empty alias file for $domain\n";
    close FH;
}


my $sql	= qq[insert into Domains (domain)
values ( '$domain')
];
$dbh->do($sql) or die "Cannot insert domain into DB";
print "Created $domain\n";

$dbh->disconnect();
exit 0;

Adding a user

#! /usr/bin/perl
# Author: Raphael Mankin
use strict;
use DBD::mysql;
use File::Path;

use constant DB		=> "Mail";
use constant HOST	=> "localhost";
use constant USER	=> "root";
use constant PWD	=> "RootPassword";

use constant MAIL	=> "mail";

my ($login, $passwd, $uid, $gid) = getpwnam(MAIL) or
	die "MAIL user not found";;

$|	= 1;
my $dsn	= "DBI:mysql:database=".DB.";host=".HOST;
my $dbh	= DBI->connect($dsn, USER, PWD)
	or die "Cannot connect to database";

print "Real name of User: ";
my $realName	= <>;
chomp $realName;
my @realName	= split(/\s+/, $realName);
my $userName	= join('.', @realName);

print "Domain: ";
my $domain	= <>;
chomp $domain;
$domain	= lc $domain;

my $sth	= $dbh->prepare("select domain from Domains where domain='$domain'");
$sth->execute();
my $res;
while (my $ref	= $sth->fetchrow_hashref()) {
	$res	= $ref->{domain};
}
die "No such domain: $domain" unless $res;
$userName	= lc $userName;

print "Password:";
my $password	= <>;
chomp $password;

print "Real Name: $realName\n";
print "Login name: $userName\n";


my $homedir	= "/virtual/$domain/mail/$userName";
my $maildir	= "$homedir/Maildir";

# the login name is the full user@domain.
$userName	.=	 "\@$domain";

mkpath $homedir unless -d $homedir;
system("/usr/bin/maildirmake $maildir") unless -d $maildir;
system ("chown -R mail:mail $homedir");
my $sql	= qq[insert into Users (user, password, homedir, maildir, realName)
values (
'$userName',
'$password',
'$homedir',
'$maildir',
'$realName'
)
];
$dbh->do($sql) or die "Cannot insert user into DB";
print "Created $userName\n";

$dbh->disconnect();
exit 0;

Deleting a user

#! /usr/bin/perl
# Author: Raphael Mankin
use strict;
use DBD::mysql;
use File::Path;

use constant DB		=> "Mail";
use constant HOST	=> "localhost";
use constant USER	=> "root";
use constant PWD	=> "RootPassword";

use constant MAIL	=> "mail";

my ($login, $passwd, $uid, $gid) = getpwnam(MAIL) or
	die "MAIL user not found";;

$|	= 1;
my $dsn	= "DBI:mysql:database=".DB.";host=".HOST;
my $dbh	= DBI->connect($dsn, USER, PWD)
	or die "Cannot connect to database";

print "Real name of User: ";
my $realName	= <>;
chomp $realName;
my @realName	= split(/\s+/, $realName);
my $userName	= join('.', @realName);

print "Domain: ";
my $domain	= <>;
chomp $domain;
$domain	= lc $domain;

my $sth	= $dbh->prepare("select domain from Domains where domain='$domain'");
$sth->execute();
my $res;
while (my $ref	= $sth->fetchrow_hashref()) {
	$res	= $ref->{domain};
}
die "No such domain: $domain" unless $res;
$userName	= lc $userName;

print "Real Name: $realName\n";
print "Login name: $userName\n";

my $homedir	= "/virtual/$domain/mail/$userName";

# the login name is the full user@domain.
$userName	.=	 "\@$domain";

my $sql	= qq[delete Users where userName= '$userName') ];
$dbh->do($sql) or die "Cannot delete user from DB";
rmtree $homedir if -d $homedir;
print "Deleted $userName\n";

$dbh->disconnect();
exit 0;

Links and References

Several people have set up similar systems to this, however, I did not find their descriptions either quite matched what I was trying to do, or entirely explicit: there were gaps in their documents. I hope that this document fills in some of the gaps, but I am also providing links to sources that I found useful.

Configuring Clam AV: http://www200.pair.com/mecham/spam/clamav-amavisd-new.html

Exim 4 Documentation: http://exim.org/exim-html-4.50/doc/html/"

OpenSSL - Documents, Misc: http://www.openssl.org/docs/

Courier Mail Server: http://www.courier-mta.org/

Installing and configuring courier IMAP http://edseek.com/~jasonb/articles/exim4_courier/courierimap.html

Virtual Domains with Exim + Courier-IMAP + MySQL: http://www.tty1.net/virtual_domains_en.html

A Complete Virtual System - Web Access - Gentoo Linux Wiki: Complete Virtual Mail Server with Gentoo This is a link into the Gentoo wiki; it changes from time to time as the article gets edited. You may have to search for it, rather than following this link.

System -> Postfix+ Courier-IMAP +MySQL for multiple domains HOWTO: http://www.linuxhowtos.org/System/postfix.htm

SSL Certificate HOWTO: http://www.gtlib.cc.gatech.edu/pub/linux/docs/HOWTO/other-formats/html_single/SSL-Certificates-HOWTO.html

HOWTO: Create a self-signed (wildcard) SSL certificate

[Archived content from justinsamuel.com]

The following commands are all you need to create a self-signed (wildcard, if you want) SSL certificate:

mkdir /usr/share/ssl/certs/hostname.domain.com
cd /usr/share/ssl/certs/hostname.domain.com
(umask 077 && touch host.key host.cert host.info host.pem)
openssl genrsa 2048 > host.key
openssl req -new -x509 -nodes -sha1 -days 3650 -key host.key > host.cert
...[enter *.domain.com for the Common Name]...
openssl x509 -noout -fingerprint -text < host.cert > host.info
cat host.cert host.key > host.pem
chmod 400 host.key host.pem


Creative Commons License

This work is licenced under a Creative Commons Licence.