Sqwebmail Worksheet

1. Install sqwebmail

webmail is a very useful service to offer your clients - although you may need to be careful of the extra CPU load and bandwidth it might use.

Unlike many other webmail solutions, which use POP3 or IMAP to talk to the mail store, sqwebmail reads and writes Maildir directories directly. This makes it efficient in the case where POP/IMAP and webmail run on the same box, or where there is an NFS-shared mailstore.

sqwebmail is feature-rich, very customisable through HTML templates and stylesheets, supports multiple languages, and is simple to install (it runs as a single CGI). Note however that it is still under very active development and hence subject to change quite frequently.

Check your Apache install is working by pointing a web browser at http://localhost/

Now install sqwebmail:

# cd /usr/ports/mail/sqwebmail
# make WITH_CHARSET=all MASTER_SITE_OVERRIDE=ftp://noc.e0.ws.afnog.org/pub/FreeBSD/distfiles/
# make install
# make clean       (optional step)

The option "WITH_CHARSET=all" allows sqwebmail to view messages in a wide range of character sets. This increases the size of the binary by about one megabyte with the extra translation tables which are included.

One other change is required: add the following line to /etc/crontab to periodically clean out old sessions:

0 * * * * bin /usr/local/share/sqwebmail/cleancache.pl

Sqwebmail comes in two parts: a small CGI stub which sends HTTP requests down a socket; and a pool of daemons which perform the actual work. The CGI stub is installed in /usr/local/www/cgi-bin-dist by default, and there are some graphics installed in /usr/local/www/data-dist/sqwebmail/*. These locations will work for a default Apache install, but if you have changed the normal Apache configuration (e.g. DocumentRoot) then you may need to copy these somewhere else.

2. Configure and start sqwebmail

sqwebmail's main configuration file is /usr/local/etc/sqwebmail/sqwebmaild - however you almost certainly don't need to change it.

As usual, you will need to enable the sqwebmail daemon in /etc/rc.conf, and then call its startup script.

# vi /etc/rc.conf

# /usr/local/etc/rc.d/sqwebmail-sqwebmaild.sh start
Starting sqwebmaild.

3. Test sqwebmail

If everything is working correctly, you should be able to point a web browser at http://localhost/cgi-bin/sqwebmail/sqwebmail and be presented with a login screen, where you can enter a username and password and login.

If this does not work:

Further documentation for sqwebmail can be found at http://www.courier-mta.org/sqwebmail/ and installed in /usr/local/share/doc/sqwebmail/

4. Optional extra exercises

4.1 Give your neighbour a mail account on your system. Let them check that they can collect mail using POP3, IMAP and Webmail.

4.2 When users send mail via sqwebmail, we would like their IP address and login name to be recorded in one of the Received: headers to provide a security audit. This can be done by modifying the script 'sendit.sh' which sqwebmail uses to send all outgoing mail.

# vi /usr/local/share/sqwebmail/sendit.sh
exec /usr/sbin/sendmail -oi -t $DSN -f "$1"
exec /usr/sbin/sendmail -oi -t -f "$1" -oMr "$SERVER_PROTOCOL" -oMa "$REMOTE_ADDR" -oMt "$2"
# /usr/local/etc/rc.d/sqwebmail-sqwebmaild.sh restart

After making this change, compose a mail via the sqwebmail interface. When it is delivered, check the full headers and look for the bottom Received: header which should record the source of the mail.

Received: from [] (ident=fred@flintstone.org)
        by billdog.local.linnet.org with HTTP/1.1 (Exim 4.43 (FreeBSD))
        id 1Cln2K-0000Pd-EP
        for wilma@flintstone.org; Tue, 04 Jan 2005 11:39:40 +0000

4.3 A number of behaviours of courier-imap and sqwebmail can be changed by means of "account options". These can be set globally, and overridden for individual accounts (although not for Unix system accounts). Try the following:

# vi /usr/local/etc/authlib/authdaemonrc
# /usr/local/etc/rc.d/courier-authdaemond.sh restart

You can see account options for an account using "authtest username", and list all accounts together with their options using "authenumerate -o"

The available options are:


Disable shared folder functionality (hides the 'key' icon in sqwebmail). Shared folders need additional setting up, and only work for systems with virtual accounts.


Disable these types of access from this account


Webmail: disable ability for users to set the From: header on outgoing mail


Webmail: disable ability for users to change their passwords


Webmail: add an X-Sender: header to outgoing mail


Webmail: use a text-only interface


Webmail: disable the "return receipt" functionality (Exim does not support this so we must disable it anyway)

5. Security and virtual accounts (advanced)

In the simple examples above, we have been using the system password file to authenticate users. When creating new "E-mail only" accounts on your system, you probably don't want your users to be able to login to Unix using ssh or telnet. To disable this, you can simply create their accounts with a nonexistent shell.

# pw useradd username -m -s /nonexistent

To improve security and scalability further, you may wish to keep all your mail accounts in a completely separate password file or database; these users won't be known to Unix at all. The mail directories and messages have to be owned by some Unix user, so we can choose to make them all owned by the 'mailnull' user.

You have many choices of authentication module: for example an LDAP database, a mysql or postgresql database, or a local dbm file (courier supports a format called 'userdb'). These databases will contain the mail login usernames and passwords, and the directories where the mail will be stored. You'll need to configure Courier to use this new database as a login source, and also configure Exim to read this source to determine whether a user exists and where to deliver mail to.

The following example shows how to do this with authuserdb, which is described in "man makeuserdb" and "man userdb". We'll create two users in the table, and two empty maildirs. We'll make them under directory /var/vmail01, which we'll assume is a fast SCSI hard drive. To support many domains we'll make the POP3 login be user@domain instead of just a username. We'll have a separate directory for each domain, and also use the first two characters of the username as subdirectories, so that if we have ten thousand users for one domain we don't end up with ten thousand accounts within the same directory.

First we make an empty userdb, and make sure it's not world readable

# touch /usr/local/etc/authlib/userdb
# chmod 600 /usr/local/etc/authlib/userdb

Next we create some accounts and empty maildirs:

# userdb fred@flintstone.org set uid=26 gid=6 home=/var/vmail01/flintstone.org/f/r/fred
# userdbpw -md5 | userdb fred@flintstone.org set systempw
Password: wibble
Reenter password: wibble
# mkdir -p /var/vmail01/flintstone.org/f/r/fred
# maildirmake /var/vmail01/flintstone.org/f/r/fred/Maildir
# chown -R mailnull:mail /var/vmail01/flintstone.org/f/r/fred

# userdb wilma@flintstone.org set uid=26 gid=6 home=/var/vmail01/flintstone.org/w/i/wilma
# userdbpw -md5 | userdb wilma@flintstone.org set systempw
Password: boing
Reenter password: boing
# mkdir -p /var/vmail01/flintstone.org/w/i/wilma
# maildirmake /var/vmail01/flintstone.org/w/i/wilma/Maildir
# chown -R mailnull:mail /var/vmail01/flintstone.org/w/i/wilma

(In real life you'd write a script to automate this process for creating new accounts). The compulsory fields we need to provide are "home" and numeric "uid" and "gid"; these are documented in "man makeuserdb".

Now we check the userdb contents - it's just a plain text file - then convert it into userdb.dat which is the fast indexed version that authuserdb reads. Note that authuserdb requires encrypted passwords.

# cat /usr/local/etc/authlib/userdb
fred@flintstone.org     home=/var/vmail01/f/flintstone.org/fred|systempw=$1$96YgsKCe$0oey3dzw0mztdby6ICFxR0|gid=6|uid=26
wilma@flintstone.org    home=/var/vmail01/f/flintstone.org/wilma|systempw=$1$nXNJyXcB$1mItZjaFmOV/3YHby8SGu0|gid=6|uid=26
# makeuserdb
# ls -l /usr/local/etc/authlib/userdb.dat
-rw-r--r--  1 root  wheel  65536 May 14 16:23 /usr/local/etc/authlib/userdb.dat

Now you can configure courier-imap and sqwebmail to login using these new accounts. If you leave authpam in the configuration then you can login with both the userdb accounts and the system accounts. If you removed authuserdb from the list of authentication modules earlier, then put it back now:

# vi /usr/local/etc/authlib/authdaemonrc
authmodulelist="authuserdb authpam"
# /usr/local/etc/rc.d/courier-authdaemond.sh stop
# /usr/local/etc/rc.d/courier-authdaemond.sh start

At this point you should be able to login using one of the new accounts, and see the (empty) mail directory.

# telnet localhost 110
Connected to localhost.presanog.org.bt.
Escape character is '^]'.
+OK Hello there.
user fred@flintstone.org
+OK Password required.
pass wibble
+OK logged in.
+OK 0 0
+OK Bye-bye.
Connection closed by foreign host.

If this doesn't work, follow the instructions for debugging authentication problems given earlier. Remember the authtest and authenumerate commands, and look in /var/log/maillog and /var/log/debug.log

Now all that is necessary is for Exim to know how to deliver messages to these users. There are a couple of ways this can be done; Exim can be configured to read /usr/local/etc/authlib/userdb.dat directly, or it can be configured to talk to courier's authdaemond process. Both can be set up using Exim's general-purpose configuration language.

It's a tribute to the flexibility of Exim that this can be done even though Exim does not have any specific features for using userdb or authdaemond, although it does mean that the configuration looks complicated at first glance. If you want to find out in detail how these configurations work, you will need to read the Exim documentation carefully.

Firstly, we need a separate list of which domains which need lookups in the userdb database; for simplicitly we will make this a plain text file which is searched linearly. If this gets large it can be converted into an indexed database.

# vi /usr/local/etc/exim/vdomains

# vi /usr/local/etc/exim/configure
Change the local domains setting to say:
domainlist local_domains = @ : lsearch;/usr/local/etc/exim/vdomains

To read /usr/local/etc/authlib/userdb.dat directly, we use the "dbmnz" lookup type. It's made a bit awkward because courier uses a vertical bar to separate fields, whereas Exim's "extract" operator expects fields separated by spaces, but we can use the regular expression substitution operator (sg) to convert this into the form we want.

[put this immediately after 'begin routers']
  driver = accept
  transport = local_delivery_userdb
  domains = lsearch;/usr/local/etc/exim/vdomains
  address_data = ${lookup{$local_part@$domain}dbmnz{/usr/local/etc/authlib/userdb.dat}\
     {${sg{$value}{([^=]+)=([^|]+)\\|?}{\$1="\$2" }}}fail}
  # note the space between "\$2" and }}}

# If the address_data lookup succeeds, then we'll go to the transport.
# But if the address_data lookup fails, then we fall through to here; all
# remaining addresses in vdomains need to be bounced.

  driver = redirect
  domains = lsearch;/usr/local/etc/exim/vdomains
  data = :fail:unknown user

[put this anywhere after 'begin transports']
  driver = appendfile
  directory = ${extract{home}{$address_data}}/${extract{mail}{$address_data}{$value}{Maildir}}/
  user = ${extract{uid}{$address_data}}
  group = ${extract{gid}{$address_data}}
  maildir_tag = ,S=$message_size
  quota_size_regex = ,S=(\d+)
  quota = ${if match{${extract{quota}{$address_data}}}{([0-9]+)S}{$1}{}}
  quota_filecount = ${if match{${extract{quota}{$address_data}}}{([0-9]+)C}{$1}{}}
  quota_warn_threshold = 90%

Once you've done this, test using

# /usr/local/sbin/exim -bt brian@flintstone.org
brian@flintstone.org is undeliverable:
  unknown user
# /usr/local/sbin/exim -bt fred@flintstone.org
  router = userdb, transport = local_delivery_userdb
# /usr/local/sbin/exim -v fred@flintstone.org
Subject: test

  <= root@noc.presanog.org.bt U=root P=local S=302
  => fred <fred@flintstone.org> R=userdb T=local_delivery_userdb

It's even possible for Exim to send a request to courier's authdaemond process to perform the lookup, which has the advantage that it will work for any courier authentication module or combination of modules. However there are similar difficulties with parsing the response properly, and the debug output you get from courier is not as good as Exim produces.

[put this immediately after 'begin routers']

# We use manualroute with empty route_data as a dummy router, just to
# set address_data to the value read from the socket.

  driver = manualroute
  route_data =
  domains = lsearch;/usr/local/etc/exim/vdomains
  address_data = ${sg{${readsocket{/usr/local/var/spool/authdaemon/socket}\
        {PRE . exim $local_part@$domain\n}}}{([^=]+)=([^\n]+)\n}{\$1="\$2" }}
  # note the space between "\$2" and }}

# Next, if the response contains HOME= then we know this address is valid
# and we can send it to the local delivery transport

  driver = accept
  transport = local_delivery_courier
  domains = lsearch;/usr/local/etc/exim/vdomains
  condition = ${extract{HOME}{$address_data}{1}{0}}

# Otherwise, the address was bad. If it contains FAIL then it's a permanant
# failure, otherwise it's a temporary failure

  driver = redirect
  domains = lsearch;/usr/local/etc/exim/vdomains
  data = ${if match{$address_data}{FAIL}{:fail:unknown user}fail}

  driver = appendfile
  directory = ${extract{HOME}{$address_data}}/${extract{MAILDIR}{$address_data}{$value}{Maildir}}/
  user = ${extract{UID}{$address_data}}
  group = ${extract{GID}{$address_data}}
  maildir_tag = ,S=$message_size
  quota_size_regex = ,S=(\d+)
  quota = ${if match{${extract{QUOTA}{$address_data}}}{([0-9]+)S}{$1}{}}
  quota_filecount = ${if match{${extract{QUOTA}{$address_data}}}{([0-9]+)C}{$1}{}}
  quota_warn_threshold = 90%

Finally, if you are building a box where you know that all the mail will be owned by the 'exim' or 'mailnull' user, then in fact the daemons no longer need to run with root privileges - they can run as the exim user only, as they never have to change to any other userid. This in theory should make your system more secure.

There are some steps you can take to do this for each daemon:

Set the 'deliver_drop_privilege' option in /usr/local/etc/exim/configure. You'll probably also have to adjust permissions so that the exim user has access to the authdaemon socket:
# chown mailnull:mail /usr/local/var/spool/authdaemon
Edit /usr/local/etc/courier-imap/pop3d and imapd and add "-user=mailnull" to couriertcpd options, i.e.
TCPDOPTS="-nodnslookup -noidentlookup -user=mailnull"
In principle you can run the sqwebmaild startup script as user 'mailnull', using su:
# su mailnull -c "/usr/local/etc/rc.d/sqwebmail-sqwebmaild.sh start"
However, for this to work, you first need to sort out some permissions issues: you need to recompile and reinstall sqwebmail using
# make WITH_CACHEOWNER=mailnull
and change the entry in /etc/crontab to run cleancache.pl as user 'mailnull' not user 'bin'. You also need to change ownerships of the configuration files and temporary directory:
# cd /usr/local/etc
# chown mailnull:mail sqwebmail/sqwebmaild authlib/authdaemonrc
# chown -R mailnull:mail /usr/local/var/sqwebmail