This is a summary of how to configure an email system on a computer running [[http://debian.org/|Debian GNU/Linux]] 5.0 ("lenny"). <> == Design == The ingredients used are: ClamAV:: scans messages for viruses, worms and other malware Dovecot:: IMAP server Exim:: mail transport agent Mailman:: mailing list manager PostgreSQL:: database SpamAssassin:: scans messages for spam Mail will be delivered to Maildir directories located at `/srv/mail/$domain/$user`. These directories will be created by Exim as it receives messages. All mail will be owned by the system `mail` user. To set that up: {{{ # mkdir -m 0775 /srv/mail # chgrp mail /srv/mail }}} == TODO == /!\ This document is not quite finished yet... things to change: * strip spamassassin's headers from incoming mail * stop `real_local_router` from accepting mail for virtual domains (move virtual stuff, etc, to before real_local router) * move aliases before accounts * explain how to clean up after vacation * don't autoreply to bulk mail, etc: http://wiki.exim.org/EximAutoReply == Database setup == === PostgreSQL configuration === Install the `postgresql` and `postgresql-contrib` packages. Grant permission for the users `root`, `Debian-exim` and `mail` to connect to the `mail` database using the `maildaemon` database role. In `/etc/postgresql/8.3/main/pg_hba.conf`, add: {{{ local mail maildaemon ident mailmap }}} In `/etc/postgresql/8.3/main/pg_ident.conf`, add: {{{ mailmap root maildaemon mailmap Debian-exim maildaemon mailmap mail maildaemon }}} Run `/etc/init.d/postgresql-8.3 reload` to bring these changes into effect. === Mail database creation === Become the `postgres` user (using `su`, `sudo`, etc.), and then: {{{ $ createdb mail $ createuser maildaemon $ createuser mailadmin $ createlang plpgsql mail }}} For the security of the system's users, we do not store account passwords in plaintext in the database. Instead, we store hashes of the passwords. In order to calculate the hashes, we use the [[http://www.postgresql.org/docs/8.3/static/pgcrypto.html|pgcrypto]] contrib module. We add the functions from `pgcrypto` in their own schema. This will ease updating the database to work with future versions of PostgreSQL. {{{ $ psql mail mail=# CREATE SCHEMA pgcrypto; mail=# GRANT USAGE ON SCHEMA pgcrypto TO PUBLIC; }}} Copy `/usr/share/postgresql/8.3/contrib/pgcrypto.sql` to `/tmp/` and edit it. Change the fourth line to read: `SET search_path = pgcrypto;`. Then tell `psql` to read the file and execute the statements within it with a command such as: `\i /tmp/pgcrypto.sql`. Finally, we populate a schema called `mail` with our tables, views, functions, etc. Download the [[attachment:sql.txt|SQL commands]] and feed the file through `psql` as you did with `pgcrypto.sql`. == Mail account management == Use the `create_account` and `change_password` function to mange users: {{{ $ psql mail mail=# SET search_path = mail; mail=# SELECT create_account ('user', 'domain', 'password'); mail=# SELECT change_password ('user', 'domain', 'new_password'); }}} Other management tasks can be performed by updating/deleting from the `accounts` table directly. == Initial Exim configuration == Install `exim4-daemon-heavy`. Run `dpkg-reconfigure exim4-config`. Choose these options: * split configuration into small files * "internet site" mail configuration * system mail name: the [[WikiPedia:FQDN]] of the default IP address assigned to the network interface from which outgoing connections will be made (usually the same as your system's hostname followed by a dot and the domain name) * ip-addresses to listen on: leave blank unless you know better * other destinations for which mail is accepted: `+virtual_domains' Exim will then be reloaded, and will probably fail since we have not yet defined the `virtual_domains` domainlist. This is normal at this stage. After creating the exim config files as detailed below, run `/etc/init.d/exim4 reload` to get Exim to re-read its configuration, and it should begin processing mail. {i} If you use the [[http://www.vim.org/|vim]] editor, put `# vim: filetype=exim` at the end of exim's config files to have vim automatically enable syntax highlighting when you open them. Create `/etc/exim4/conf.d/main/db`. If your PostgreSQL server runs on a different port, change `5432` to match. {{{ hide pgsql_servers = (/var/run/postgresql/.s.PGSQL.5432)/mail/maildaemon/ }}} /!\ If you are using password authentication to connect to the database, make sure this file is ''not'' world-redable! Also make sure that the generated `/var/lib/exim4/config.autogenerated` file is protected by changing `CFILEMODE` in `/etc/exim4/update-exim4.conf` to `600` and reloading Exim. == Incoming mail—"virtual" domains == The setup revolves around creating a `virtual_domains` [[http://exim.org/exim-html-current/doc/html/spec_html/ch10.html|domain list]]. The list is populated by a database lookup. When we said `+virtual_domains` when asked for a list of destinations for which mail is accepted, we told Exim to check the `virtual_domains` domainlist when deciding whether a message is destined for local delivery or not. We will now actually specify this domainlist. Create `/etc/exim4/conf.d/main/virtual`: {{{ localpartlist virtual_localparts_ignore = postmaster : abuse domainlist virtual_domains = ${lookup pgsql {SELECT domain FROM mail.exim_domains WHERE domain = '${quote_pgsql:$domain}'}} }}} Create `/etc/exim4/conf.d/router/350_virtual_alias`: {{{ virtual_alias: debug_print = "R: virtual_alias for $local_part@$domain" local_parts = ! +virtual_localparts_ignore domains = +virtual_domains driver = redirect data = ${lookup pgsql {SELECT mail.lookup_alias ('${quote_pgsql:$local_part}', '${quote_pgsql:$domain}');}} user = mail allow_filter #forbid_filter_dlfunc forbid_file forbid_filter_existstest forbid_filter_logwrite forbid_filter_lookup forbid_filter_perl forbid_filter_readfile forbid_filter_readsocket forbid_filter_reply forbid_filter_run forbid_file forbid_include forbid_pipe }}} Create `/etc/exim4/conf.d/router/351_virtual_account`: {{{ virtual_account: debug_print = "R: virtual_account for $local_part@$domain" local_parts = ! +virtual_localparts_ignore domains = +virtual_domains driver = redirect data = ${lookup pgsql {SELECT maildir FROM mail.exim_accounts \ WHERE "user" = '${quote_pgsql:$local_part}' \ AND domain = '${quote_pgsql:$domain}'}} more = false user = mail group = mail directory_transport = virtual_delivery cannot_route_message = "Unknown user $local_part@$domain" }}} Create `/etc/exim4/conf.d/transport/virtual_delivery`: {{{ virtual_delivery: debug_print = "T: virtual_delivery for $local_part@$domain" driver = appendfile envelope_to_add = true return_path_add = true check_string = "" escape_string = "" maildir_format quota = ${lookup pgsql {SELECT quota * 1024 * 1024 \ FROM mail.accounts \ WHERE "user" = '${quote_pgsql:$local_part}' AND domain = '${quote_pgsql:$domain}'}} quota_warn_threshold = 90% maildir_use_size_file = true quota_warn_message = "\ To: $local_part@$domain\n\ From: postmaster@$primary_hostname\n\ Subject: You have reached 90% of your mail quota\n\ \n\ This message is automatically created by mail delivery software.\n\ \n\ Your mailbox has reached 90% of its capacity. If you do not free\n\ up some space by deleting old mail, you will not receive any new\n\ mail that may be sent to you.\n\ \n\ Regards,\n\ postmaster@$primary_hostname" }}} Edit `/etc/exim4/conf.d/retry/30_exim4-config`. Before the final line, add: {{{ # Bounce over-quota warnings immediatly * quota }}} == Incoming mail—IMAP == Install `dovecot-imapd`. === dovecot.conf === Edit `/etc/dovecot/dovecot.conf` and set the following options: {{{ protocols = imaps disable_plaintext_auth = yes shutdown_clients = yes ssl_cert_file = /etc/dovecot/cert.pem ssl_key_file = /etc/dovecot/key.pem mail_location = maildir:%h dotlock_use_excl = yes first_valid_uid = 8 first_valid_gid = 8 maildir_copy_with_hardlinks = yes mail plugins = quota imap_quota login_greeting_capability = yes }}} Some of them should not be set if your `/srv/mail` directory is on NFS. Read the comments above each option for more information. Find the `auth default` section of the control file. The order of options in this section is important, as Dovecot will try each one in turn when authenticating users. After the initial `mechanisms = plain` option, add: {{{ passdb sql { args = /etc/dovecot/dovecot-sql.conf } }}} <> Scroll down and comment out the `passdb pam` section. This will prevent system users from picking up their mail via IMAP. This is done to prevent users exposing their system login credentials due to misconfigurations, accidents, etc. You can still use `/etc/aliases` and/or `.forward` files in the home directories of system users to redirect their mail to a virtual mailbox where they can pick it up. {i} If you do want your system users to be able to use IMAP, you should not set `mail_location` as above; instead, set `mail_executable` to `/usr/local/lib/dovecot/imap`; then create that file, with the following contents. Make sure the file is executable. {{{#!plain #!/bin/bash # For more info, see "Custom mailbox location detection" at # if [[ "$HOME" =~ ^/srv/mail/ ]] then # 'virtual' mail accounts have their mail stored in their actual home # directory. MAIL=maildir:$HOME/ else # Real user accounts have their mail stored in a subdirectory of their # home directory. MAIL=maildir:$HOME/Maildir/ fi export MAIL exec /usr/lib/dovecot/imap }}} You must also re-run `dpkg-reconfigure` and set 'delivery method for local mail' to 'Maildir format in home directory'. Now, scroll down and uncomment the `userdb prefetch` section. Finally, look for the `plugin` section, and within it, set `quota = maildir`. === dovecot-sql.conf === Set the following options in `/etc/dovecot/dovecot-imap.conf`: {{{ driver = pgsql connect = dbname=mail user=maildaemon default_pass_scheme = CRYPT password_query = SELECT user, password, userdb_home, userdb_uid, userdb_gid FROM mail.dovecot_auth WHERE "user" = '%u' }}} Finally, reload Dovecot with `/etc/init.d/dovecot reload`. == TLS (aka SSL) encryption == Put your certificate at `/etc/exim4/exim.crt` and your private key at `/etc/exim4/exim.key`. Ensure that the private key can only be read by Exim by changing its group owner to `Debian-exim` and its permissions to `0640`. Edit `/etc/exim4/conf.d/main/00_localmacros`: {{{ MAIN_TLS_ENABLE = yes MAIN_LOG_SELECTOR = +tls_certificate_verified +tls_cipher +tls_peerdn }}} == Outgoing mail—SMTP authentication == Create `/etc/exim4/conf.d/auth/postgres`: {{{{ # $2 is the supplied username; $3 is the supplied password virtual_auth_server_plain: driver = plaintext public_name = PLAIN server_advertise_condition = ${if eq{$tls_cipher}{}{no}{yes}} server_condition = ${if crypteq{$3} \ {${lookup pgsql {SELECT password FROM mail.exim_auth WHERE email = '${quote_pgsql:$2}'}}} \ {yes} \ {no}} server_set_id = $2 }}}} Clients can then send mail via your server, using their email addresses as their username. == Mailing lists == === Mailman configuration === Install the `mailman` package. Read `/usr/share/doc/mailman/README.Debian.gz`. Disregard the paragraphs about Exim; the following files configure Exim to automatically recognise mailing lists. Stop the `newlist` command from printing irrelevant info about how to configure your MTA to recognise a newly-created list. Edit `/etc/mailman/mm_cfg.py`: {{{ MTA=None }}} === Exim configuration === Create `/etc/exim4/conf.d/main/mailman`: {{{ MAILMAN_HOME = /var/lib/mailman MAILMAN_WRAP = MAILMAN_HOME/mail/mailman MAILMAN_UID = list MAILMAN_GID = list MAILMAN_LOCALPART_SUFFIXES = -admin: -bounces: -confirm : -join : -leave \ : -owner : -request : -subscribe : -unsubscribe }}} The mailman router is run `after` the various virtual routers are run. This ensures that a mailing list named `foo` does not clobber delivery to `foo@virtual.example.com`. It also means that mailing list addresses live 'in' the mail server's primary hostname. For example, the list `mylist`'s address is `mylist@hostname.example.com`. If you want to create a list for a virtual domain, create a virtual alias that redirects to the list's real domain name. Create `/etc/exim4/conf.d/router/360_mailman`: {{{ mailman: debug_print = "R: mailman for $local_part@$domain" driver = accept require_files = MAILMAN_HOME/lists/$local_part/config.pck local_part_suffix_optional local_part_suffix = MAILMAN_LOCALPART_SUFFIXES transport = mailman }}} Create `/etc/exim4/conf.d/transport/mailman`: {{{{ mailman: debug_print = "T: mailman for $local_part@$domain" driver = pipe command = MAILMAN_WRAP \ '${if def:local_part_suffix \ {${sg {$local_part_suffix} {-(\\w+)(\\+.*)?} {\$1}}} \ {post}}' \ $local_part user = MAILMAN_UID group = MAILMAN_GID home_directory = MAILMAN_HOME current_directory = MAILMAN_HOME }}}} == Content scanning—antispam/malware == Install ``clamav-daemon``. Allow ClamAV permission to scan files in Exim's mail spool: {{{ # adduser clamav Debian-exim }}} Install `spamassassin`. By default, `spamd` is configured to run as `root` so that it can run on behalf of any user. We don't want this; we will run it as its own user. {{{ # adduser --system --group --home /var/run spamassassin }}} Edit `/etc/default/spamassassin`: {{{ ENABLED=1 OPTIONS="-u spamassassin --nouser-config --max-children 3" }}} Edit `/etc/spamassassin/local.cf`: {{{ # These are not suitable for system-wide scanning use_auto_whitelist 0 use_bayes 0 clear_report_template report "hits=_HITS_: _TESTSSCORES(, )_ with SpamAssassin _VERSION_ (_SUBVERSION_) on _HOSTNAME_" }}} Start !SpamAssassin and ClamAV: {{{ # /etc/init.d/spamassassin start # /etc/init.d/clamav-daemon restart }}} === Exim configuration === Edit `/etc/exim4/conf.d/main/00_localmacros`: {{{ CHECK_RCPT_LOCAL_ACL_FILE = /etc/exim4/acl-rcpt CHECK_DATA_LOCAL_ACL_FILE = /etc/exim4/acl-data # enables sender address verification for messages from remote domains # see acl_check_rcpt and section 39.31 of the Exim specification CHECK_RCPT_VERIFY_SENDER = yes }}} Anything that recieves more than 10 points from !SpamAssassin will be rejected at SMTP-time. Users can filter off mail with lower scores by matching the `X-Spam-Bars` header. Create `/etc/exim4/conf.d/main/antispam`: {{{ # Desired values are multiplied by ten SPAM_FLAG_SCORE = 50 SPAM_REJECT_SCORE = 100 av_scanner = clamd:/var/run/clamav/clamd.ctl }}} Create `/etc/exim4/acl-rcpt`: {{{{ # Greylist hosts known to send spam and exploits # defer !acl = acl_local_deny_exceptions dnslists = dnsbl.sorbs.net : sbl-xbl.spamhaus.org : bl.spamcop.net condition = ${if eq {${lookup pgsql{SELECT COUNT(*) FROM mail.greylist_disable \ WHERE address = '${quote_pgsql:$local_part}@${quote_pgsql:$domain}'}}} {0}} condition = ${if eq {${lookup pgsql{SELECT mail.greylist_process \ ('${quote_pgsql:$sender_host_address}', \ '${quote_pgsql:$sender_address}', \ '${quote_pgsql:$local_part@$domain}')}}} {t}} message = Please try later log_message = greylisted }}}} Create `/etc/exim4/acl-data`: {{{ # malware deny message = Message contains malware ($malware_name) malware = * # spam deny message = Message classified as spam ($spam_score points) spam = mail:true condition = ${if >{$spam_score_int}{SPAM_REJECT_SCORE}} warn spam = mail:true add_header = X-Spam-Bars: $spam_bar warn spam = mail:true add_header = X-Spam-Report: $spam_report }}} == Vacation == Used for out-of-office messages, etc. The vacation auto-reply won't be sent more than once every three days. Create `/etc/exim4/conf.d/router/347_virtual_vacation`: {{{ virtual_vacation: debug_print = "R: virtual_vacation for $local_part@$domain" local_parts = ! +virtual_localparts_ignore domains = +virtual_domains driver = accept condition = ${lookup pgsql {SELECT DISTINCT message FROM mail.vacation WHERE "user" = '${quote_pgsql:$local_part}' AND domain = '${quote_pgsql:$domain}'}} transport = virtual_vacation unseen }}} Create `/etc/exim4/conf.d/transport/virtual_vacation`: {{{ virtual_vacation: debug_print = "T: virtual_vacation for $local_part@$domain" driver = autoreply user = mail group = mail return_message once = /srv/mail/$domain/$local_part/vacation.dbm once_repeat = 3d from = $local_part@$domain to = $sender_address subject = Re: $h_subject text = ${lookup pgsql{SELECT message FROM mail.vacation \ WHERE "user" = '${quote_pgsql:$local_part}' \ AND domain = '${quote_pgsql:$domain}'}} }}} == Subaddressing == It is useful to redirect mail for `sam+foo@example.com` to `sam@example.com`. This makes it easy for users to create arbitrary mailboxes on the fly. The original destination address is preserved in the message's `To` header, so users can use it for filtering, or to see which sites sold their email addresses to spammers, etc. Create `/etc/exim4/conf.d/router/305_subaddress`: {{{ subaddress: debug_print = "R: subaddress for $local_part$local_part_suffix@$domain" driver = redirect data = $local_part@$domain local_part_suffix = +* }}} == Mail for system users == We want mail for system users to be directed to `root`'s mailbox. We do this so that such mail does not languish unread in `/var/mail` for years. The Debian Policy Manual [[http://www.debian.org/doc/debian-policy/ch-opersys.html#s9.2.2|defines]] a system user as one whose UID is outside of the range 1000‒29999. Create `/etc/exim4/conf.d/router/405_system_user`: {{{{{ # Redirect messages for users with a uid outside of the range 1000 - 29999 to # root. The range is defined in Policy section 9.2.2: # system_user: debug_print = "R: system_user for $local_part@$domain [$local_user_uid]" driver = redirect domains = +local_domains check_local_user data = root condition = ${if or {{< {$local_user_uid} {1000}} {> {$local_user_uid} {29999}}}} }}}}} == Mail for human users == As noted [[#human_users|earlier]], mail for real, human users should be redirected by means of a `.forward` file or similar. If a user does not have a forward file, we want to prevent their mail from clogging up `/var/mail`. Instead, we will bounce any mail for a human user who has not specified a forwarding address. However, this router should not run when performing address verification--the user exists, they just don't want to actually receive anything. Create `/etc/exim4/conf.d/router/875_no-local-mail`: {{{{{ no_local_mail: debug_print = "R: no_local_mail for $local_part@$domain" domains = +local_domains check_local_user driver = redirect no_verify allow_fail data = :fail: no forwarding address specified for this user # vim: ft=exim }}}}} == Mail for groups == I configured my server so that sending mail to `groupname.group` would cause the mail to be delivered to all the members of the local group `groupname`, as defined in the `/etc/group` file. This is pretty esoteric, but may be useful to someone else: Create `/etc/exim4/conf.d/router/910_local_group`: {{{{{{ local_group: debug_print = "R: local_group for $local_part@$domain" driver = redirect domains = +local_domains data = ${extract{3}{:}{${lookup{$local_part}lsearch{/etc/group}}}} local_part_suffix = .group }}}}}} The `local_part_suffix` serves to namespace the groups off from other `local_part`s. == Testing == Fake SMTP session from specified IP address: `exim4 -bh 1.2.3.4` Address testing ("what will Exim do with this address?"): `exim4 -bt` Configuration options testing ("what is the effective value of an option?: `exim4 -bP` == Sources == * [[http://www.postgresql.org/docs/8.3/interactive/index.html|PostgreSQL 8.3 documentation]] * [[http://exim.org/exim-html-current/doc/html/spec_html/index.html|Exim Specification]] ---- CategoryTechnote