27 Jun 2007, 16:13

Exim4 Konfiguration mit SMA, MySQL, IPv6, SpamAssassin und TLS

Share

Achtung: Dieser Beitrag ist schon älter und teilweise fehlerhaft. Unter dem Titel ISP Email Server mit Exim 4, Dovecot, MySQL und SpamAssassin auf Debian GNU/Linux etch habe ich eine ausführlichere und ausgereiftere Fassung veröffentlicht.

Ich möchte hier kurz meine Mailserver Konfiguration vorstellen, da ich es für gut möglich halte, dass diese Information auch für andere Betreiber von Mailservern von Intresse sein könnte.

Für den Betrieb meines Mailservers setze ich auf exim4 und Dovecot. Das ganze läuft unter Debian etch.

Unter Debian lassen sich die benötigten Pakete einfach mit folgendem Kommando installieren:

aptitude install dovecot-common dovecot-imapd dovecot-pop3d exim4-daemon-heavy mysql-server-5.0 pyzor razor spamassassin spamc dcc-client
Zu Dovecot ist eigentlich nichts weiter zu sagen, ausser das er wunderbar seinen Dienst verrichtet.

Exim greift auf eine MySQL Datenbank für die Überprüfung der Benutzer zurück, bindet SpamAssassin mit diversen Plugins ein, läuft sowohl auf IPv4 als auch IPv6 (via SixXS) und nimmt Mails auch via SMA/RFC2476 entgegen - natürlich alles auch über TLS.

Ein weiteres gute Beispiel für eine Exim4 Konfiguration gibt es hier.

Hier folgt jetzt meine kommentierte Exim 4 Konfiguration. Bei Fragen bitte die Kommentarfunktion des Blogs nutzen.

#######################################
# MACROS
#######################################
Zunächst werden einige Macros definiert um die gesamte Konfiguration übersichtlicher zu gestallten. Am Anfang stehe die Zugangsdaten zum MySQL-Server und die Tabellennamen.
MYSQL_SERVER=localhost
MYSQL_USER=user
MYSQL_PASSWORD=pass
MYSQL_DB=db
MYSQL_EMAILTABLE=exim_emailtable
MYSQL_DOMAINTABLE=exim_domains
MYSQL_WHITETABLE=exim_whitelist
MYSQL_BLACKTABLE=exim_blacklist
Dann folgt die Mailman Konfiguration. Dies wird weiter unten nochmal erläutert.
MAILMAN_HOME=/var/lib/mailman
MAILMAN_WRAP=MAILMAN_HOME/mail/wrapper
MAILMAN_UID=list
MAILMAM_GID=list
Hier folgen die aktuelle verwendeten Blacklists, genauer: DNSRBL. Leider muss diese Liste in letzter Zeit oft angepasst werden da die BLs oft unter Beschuss stehen oder den Dienst einstellen.
BL_WARN=zen.spamhaus.org:ix.dnsbl.manitu.net
BL_DENY=zen.spamhaus.org:ix.dnsbl.manitu.net
Nun zum komplizierten Teil: Den SQL Querys. Ich werde nicht auf jede einzelne Query eingehen, aber mit ein paar SQL Kentnissen sollten sie kein Problem sein.
MYSQL_Q_ISAWAY=SELECT domain FROM MYSQL_EMAILTABLE WHERE domain='${quote_mysql:$domain}' AND local_part='${quote_mysql:$local_part}' AND is_away='yes'
MYSQL_Q_AWAYTEXT=SELECT away_text FROM MYSQL_EMAILTABLE WHERE domain='${quote_mysql:$domain}' AND local_part='${quote_mysql:$local_part}'
MYSQL_Q_FORWARD=SELECT forward FROM MYSQL_EMAILTABLE WHERE domain='${quote_mysql:$domain}' AND local_part='${quote_mysql:$local_part}' AND forward != '' AND is_enabled = 'yes'
MYSQL_Q_CC=SELECT cc FROM MYSQL_EMAILTABLE WHERE domain='${quote_mysql:$domain}' AND local_part='${quote_mysql:$local_part}' AND is_enabled = 'yes'
MYSQL_Q_LOCAL=SELECT domain FROM MYSQL_EMAILTABLE WHERE domain='${quote_mysql:$domain}' AND local_part='${quote_mysql:$local_part}' AND forward = '' AND is_enabled = 'yes'
MYSQL_Q_WCLOCAL=SELECT domain FROM MYSQL_EMAILTABLE WHERE domain='${quote_mysql:$domain}' AND local_part='*' AND forward != '' AND is_enabled = 'yes'
MYSQL_Q_WCLOCFW=SELECT forward FROM MYSQL_EMAILTABLE WHERE domain='${quote_mysql:$domain}' AND local_part='*' AND forward != '' AND is_enabled = 'yes'
MYSQL_Q_DISABLED=SELECT domain FROM MYSQL_EMAILTABLE WHERE domain='${quote_mysql:$domain}' AND local_part='${quote_mysql:$local_part}' AND is_enabled = 'no'
MYSQL_Q_LDOMAIN=SELECT DISTINCT domain FROM MYSQL_DOMAINTABLE WHERE domain='$domain'
MYSQL_Q_RDOMAIN=SELECT DISTINCT domain FROM MYSQL_DOMAINTABLE WHERE domain='$domain'
MYSQL_Q_BOXPATH=SELECT CONCAT(domain,'/',local_part) AS boxpath FROM MYSQL_EMAILTABLE WHERE domain='${quote_mysql:$domain}' AND local_part='${quote_mysql:$local_part}'
MYSQL_Q_SPAMC=SELECT domain FROM MYSQL_EMAILTABLE WHERE domain='${quote_mysql:$domain}' AND local_part='${quote_mysql:$local_part}' AND spam_check='yes'
MYSQL_Q_SPAMPURGE=SELECT domain FROM MYSQL_EMAILTABLE WHERE domain='${quote_mysql:$domain}' AND local_part='${quote_mysql:$local_part}' AND spam_purge='yes'
MYSQL_Q_AUTHPLAIN=SELECT if(count(*), "1", "0") FROM MYSQL_EMAILTABLE WHERE CONCAT(local_part,'@',domain)='${quote_mysql:$2}' AND pwclear='${quote_mysql:$3}'
MYSQL_Q_AUTHLOGIN=SELECT if(count(*), "1", "0") FROM MYSQL_EMAILTABLE WHERE CONCAT(local_part,'@',domain)='${quote_mysql:$1}' AND pwclear='${quote_mysql:$2}'
MYSQL_Q_AUTHCRAM=SELECT pwclear FROM MYSQL_EMAILTABLE WHERE CONCAT(local_part,'@',domain)='$1'
MYSQL_Q_WHITELIST=SELECT DISTINCT MYSQL_WHITETABLE.address FROM MYSQL_WHITETABLE WHERE '${quote_mysql:$sender_address}' LIKE MYSQL_WHITETABLE.address
MYSQL_Q_BLACKLIST=SELECT DISTINCT MYSQL_BLACKTABLE.address FROM MYSQL_BLACKTABLE WHERE '${quote_mysql:$sender_address}' LIKE MYSQL_BLACKTABLE.address
# 'hide' damit diese Optionen nicht auf der Kommandozeile angezeigt werden.
hide mysql_servers = "MYSQL_SERVER/MYSQL_DB/MYSQL_USER/MYSQL_PASSWORD"
#######################################
# BASIC
#######################################
# Der primäre Hostname, sollte identisch mit dem RDNS Namen der IP sein.
primary_hostname = mail.gauner.org
# Welche Domains sind lokal
domainlist      local_domains = localhost:gauner.org:lists.gauner.org:mysql;MYSQL_Q_LDOMAIN
domainlist      relay_to_domains = mysql;MYSQL_Q_RDOMAIN
hostlist        relay_from_hosts = 127.0.0.1
# Definiert zu welchen Zeiten der SMTP Sitzung welche ACLs ausgeführt werden.
# Nachdem das RCPT Kommando gesendet wurde
acl_smtp_rcpt = acl_check_rcpt
# Nachdem das MAIL FROM Kommando gesendet wurd
acl_smtp_mail = acl_check_from
qualify_domain = gauner.org
never_users = root
host_lookup = *
# Trusted Users wird für SpamAssasin benötigt.
trusted_users = mail
rfc1413_hosts = *
rfc1413_query_timeout = 15s
check_spool_space = 50M
check_log_space = 20M
return_size_limit = 20k
message_size_limit = 20M
ignore_bounce_errors_after = 2d
timeout_frozen_after = 7d
deliver_queue_load_max = 8
queue_only_load = 10
remote_max_parallel = 15
# TLS Konfiguration
tls_certificate = /etc/exim4/exim.cert
tls_privatekey = /etc/exim4/exim.key
tls_advertise_hosts = *
local_interfaces = < ; 127.0.0.1 ; aaa.bbb.ccc.ddd ;
                        2001:cafe:dead:beef::2
# Listen for SMTP on Port 25 and for SMA on Port 587
daemon_smtp_port = 25 : 587
#######################################
# ACL
#######################################
begin acl
acl_check_from:
  # drop connections on the SMA Port that did not auth
  drop condition = ${if={$interface_port}{587} {1}{0}}
  !authenticated = *
  # accept everything else (policy checks are in rcpt acl)
  accept
acl_check_rcpt:
  accept  hosts = :
  deny    domains       = +local_domains
          local_parts   = ^[.] : ^.*[@%!/|]
  deny    domains       = !+local_domains
          local_parts   = ^[./|] : ^.*[@%!] : ^.*/../
  accept  local_parts   = postmaster
          domains       = +local_domains
  require verify        = sender
  accept  authenticated = *
  # Add a warning header if the sending host is in theses
  # DNSBLs but accept the message
  # see http://www.exim.org/howto/rbl.html
  warn    message       = X-blacklisted-at: $dnslist_domain
          dnslists      = BL_WARN
  # Reject messages from senders listed in these DNSBLs
  deny    dnslists      = BL_DENY
  # Consult "greylistd" to obtain greylisting status for this
  # particulat peer/sender/recipient triplet.
  #
  # We do not greylist messages with a NULL sender,
  # because sender callout verification would break (and we
  # mitght not be able to send mail to a host that performs
  # callouts).
  #
  defer
          message       = $sender_host_address is not yet authorized to
                          deliver mail from <$sender_address> to < $local_part@$domain>.
                          Please try later.
          log_message   = greylisted.
          domains       = +local_domains : +relay_to_domains
          !senders      = : postmaster@*
    !hosts         = : +relay_from_hosts :
                     ${if exists {/etc/greylistd/whitelist-hosts}
                                 {net-lsearch;/etc/greylistd/whitelist-hosts}{}} :
                     ${if exists {/var/lib/greylistd/whitelist-hosts}
                                 {net-lsearch;/var/lib/greylistd/whitelist-hosts}{}}
          set acl_m9    = $sender_host_address $sender_address $local_part@$domain
          set acl_m9    = ${readsocket{/var/run/greylistd/socket}{$acl_m9}{5s}{}{}}
          condition     = ${if eq {$acl_m9}{grey}{true}{false}}
  accept  domains       = +local_domains
          endpass
          verify        = recipient
  accept  domains       = +relay_to_domains
          endpass
          verify        = recipient
  accept  hosts         = +relay_from_hosts
  deny    message       = relay not permitted
#######################################
# ROUTERS
#######################################
# ORDER MATTERS!
#######################################
# Die Router. Hier ist es wichtig darauf zu achten in welcher
# Reihenfolge die Einträge angegeben werden, da eine Mail
# von oben nach unten an jeden Router übergeben wird
# bis sie von einem akzeptiert wird.
begin routers
# In der Datenbank deaktivierte Adressen werden gleich zu beginn rausgeworfen.
fail_router:
  driver = redirect
  domains = ${lookup mysql {MYSQL_Q_DISABLED}{$value}}
  data = ":fail:"
allow_fail
# Hier werden Mailman Adressen behandelt
mailman_aliases:
  driver = redirect
  allow_fail
  allow_defer
  data = ${lookup{$local_part}lsearch{/etc/aliases.mailman}}
  file_transport = address_file
  pipe_transport = address_pipe
  domains = lists.gauner.org
  user = list

# DnsLookup sind externe Nachrichten, d.h. Mails von diesem System an andere.
dnslookup:
  driver = dnslookup
  domains = ! +local_domains
  transport = remote_smtp
  ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8
  no_more

blacklist_router:
  driver = manualroute
  senders = ${lookup mysql {MYSQL_Q_BLACKLIST}{$value}}
  condition = "${if !def:h_X-Spam-Flag: {1}{0}}"
  headers_add = X-Spam-Flag: YES
  route_list = * localhost
  self = pass

# System Aliase
system_aliases:
  driver = redirect
  allow_fail
  allow_defer
  data = ${lookup{$local_part}lsearch{/etc/aliases}}
  file_transport = address_file
  pipe_transport = address_pipe

# SpamAssassin Integration (Scannen und Markieren)
spamcheck_director:
  driver = manualroute
  domains = ${lookup mysql {MYSQL_Q_SPAMC}{$value}}
  senders = ! ${lookup mysql {MYSQL_Q_WHITELIST}{$value}}
  condition = ${if and {
    {!eq {$received_protocol}{spam-scanned}}
    {!eq {$received_protocol}{local}}
    } {1}{0}}
  headers_remove = X-Spam-Flag
  route_list = "* localhost byname"
  transport = spamcheck
  verify = false

# SpamAssassin Integration (Spam löschen)
spampurge_director:
  driver = manualroute
  domains = ${lookup mysql {MYSQL_Q_SPAMPURGE}{$value}}
  condition = "${if eq{$h_X-Spam-Flag:}{YES} {1}{0}}"
  route_list = "* localhost byname"
  transport = devnull_transport
  verify = false

vacation_director:
  driver = accept
  domains = ${lookup mysql {MYSQL_Q_ISAWAY}{$value}}
  transport = vacation_autoreply
  unseen

virtual_cc_director:
  driver = redirect
  data = ${lookup mysql {MYSQL_Q_CC}{$value}}
  unseen

virtual_forward_director:
  driver = redirect
  data = ${lookup mysql {MYSQL_Q_FORWARD}{$value}}

# Lokale Zustellung für Benutzer aus der Datenbank
virtual_local_mailbox:
  driver = accept
  domains = ${lookup mysql {MYSQL_Q_LOCAL}{$value}}
  transport = virtual_local_md_delivery

virtual_wclocal_redirect:
  driver = redirect
  domains = ${lookup mysql {MYSQL_Q_WCLOCAL}{$value}}
  data = ${lookup mysql {MYSQL_Q_WCLOCFW}{$value}}

local_user:
  debug_print = "R: local_user for $local_part@$domain"
  driver = accept
  domains = +local_domains
  check_local_user
  local_parts = ! root
  transport = local_delivery

#######################################
# TRANSPORTS
#######################################
# ORDER DOES NOT MATTER
#######################################

begin transports

# Remote Deliveries
remote_smtp:
  driver = smtp
  # Use Interface aaa.bbb.ccc.ddd for Outgoing Communiction
  interface = aaa.bbb.ccc.ddd

devnull_delivery:
  driver = appendfile
  file = /dev/null
  group = mail

address_pipe:
  driver = pipe
  return_output

address_file:
  driver = appendfile
  delivery_date_add
  envelope_to_add
  return_path_add

address_directory:
  driver = appendfile
  #no_from_hack
  message_prefix = ""
  message_suffix = ""
  maildir_format

address_reply:
  driver = autoreply

# SpamAssassin Integration
spamcheck:
  driver = pipe
  command = /usr/sbin/exim4 -oMr spam-scanned -bS
  use_bsmtp = true
  transport_filter = "/usr/bin/spamc -u $local_part@$domain"
  home_directory = "/tmp"
  current_directory = "/tmp"
  user = mail
  group = mail
  log_output = true
  return_fail_output = true
  return_path_add = false
  message_prefix =
  message_suffix =

local_delivery:
  driver = appendfile
  directory = /home/users/${local_part}/Maildir
  delivery_date_add
  envelope_to_add
  return_path_add
  maildir_format

# Lokale Zustellung für Benutzer aus der Datenbank
virtual_local_md_delivery:
  driver = appendfile
  directory = /home/mail/${lookup mysql {MYSQL_Q_BOXPATH}{$value}}/Maildir
  maildir_format
  user = mail
  group = mail
  mode = 0660
  directory_mode = 0770
  check_string = ""
  message_prefix = ""
  message_suffix = ""

vacation_autoreply:
  driver = autoreply
  to = ${sender_address}
  from = "vacation@${domain}"
  subject = "Ihre Nachricht an ${local_part}@${domain}"
  text = ${lookup mysql {MYSQL_Q_AWAYTEXT}{$value}}

disabled_bounce:
  driver = autoreply
  from = ${local_part}@${domain}
  to = ${sender_address}
  user = mail
  subject = "Re $h_Subject:"
  text = "Your message to ${local_part}@${domain} was rejected due to an
    disabled account. Please try again later."

devnull_transport:
  driver = appendfile
  file = /dev/null
  user = mail

#######################################
# RETRY
#######################################
begin retry
*                      *           F,2h,15m; G,16h,1h,1.5; F,4d,6h
#######################################
# REWRITE
#######################################
begin rewrite
*@gauner.org    ${lookup{$1}lsearch{/etc/email-addresses}
                                                {$value}fail} frFs
#######################################
# AUTHENTICATORS
#######################################
begin authenticators

plain:
  driver = plaintext
  public_name = PLAIN
  server_condition = ${lookup mysql{MYSQL_Q_AUTHPLAIN}}
  server_set_id = $2

login:
  driver = plaintext
  public_name = LOGIN
  server_prompts = "Username:: : Password::"
  server_condition = ${lookup mysql{MYSQL_Q_AUTHLOGIN}}
  server_set_id = $1

cram:
   driver = cram_md5
   public_name = CRAM-MD5
   server_secret = ${lookup mysql{MYSQL_Q_AUTHCRAM}{$value}fail}
   server_set_id = $1
Die passenden Datenbankschemas für MySQL:
-- ----------------------------------------------------------
-- Tabellenstruktur für Tabelle `exim_emailtable`
--
CREATE TABLE `exim_emailtable` (
  `id` int(9) NOT NULL auto_increment,
  `local_part` varchar(255) NOT NULL default '',
  `domain` varchar(255) NOT NULL default '',
  `forward` varchar(255) default NULL,
  `cc` varchar(255) default NULL,
  `name` varchar(255) NOT NULL default '',
  `pwclear` varchar(255) NOT NULL default '',
  `pwcrypt` varchar(255) NOT NULL default '',
  `is_away` enum('yes','no') NOT NULL default 'no',
  `away_text` text,
  `spam_check` enum('yes','no') NOT NULL default 'no',
  `spam_purge` enum('yes','no') NOT NULL default 'no',
  `virus_check` enum('yes','no') NOT NULL default 'no',
  `is_enabled` enum('yes','no') NOT NULL default 'yes',
  `customer_id` int(9) NOT NULL default '0',
  `created_at` int(16) NOT NULL default '0',
  `updated_at` int(16) NOT NULL default '0',
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='Alle virtuellen Mail-Accounts' AUTO_INCREMENT=1;

--
-- Tabellenstruktur für Tabelle `exim_domains`
--
CREATE TABLE `exim_domains` (
  `id` int(9) NOT NULL auto_increment,
  `domain` varchar(255) NOT NULL default '',
  `is_enabled` enum('yes','no') NOT NULL default 'no',
  `customer_id` int(9) NOT NULL default '0',
  `created_at` int(16) NOT NULL default '0',
  `updated_at` int(16) NOT NULL default '0',
  PRIMARY KEY  (`id`),
  UNIQUE KEY `domain` (`domain`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='Tabelle aller von exim erkannten Domains' AUTO_INCREMENT=1;