An Exim 4.4x Config with Mailman

This is an example of an Exim 4.4x config file that I have been using on a server where both Apache and Mailman are running. Primarily it serves as a Mailman listserv, but some web pages also generate emails.


Please note that the config file is not shown in its entirety. Only items that have been modified or added, and a few lines to aid you in where those lines are positioned in the standard config file that comes with Exim. The Mailman configuration shown here does not use /etc/aliases and is dynamic. This is one of the many reasons I love Exim.

######################################################################
#                    MAIN CONFIGURATION SETTINGS                     #
######################################################################
#...
 
domainlist local_domains = @:lists.example.com:www.example.com
domainlist relay_to_domains =
hostlist   relay_from_hosts = 127.0.0.1
 
# Custom:
# The default list of LDAP server(s) to query if none are specified
# during an LDAP search
#ldap_default_servers = ldaps.example.com
 
# Custom:
# The following is used to get a list of domains from the LDAP directory
# that are on the internal email system.
# These domains are not used for the relay_to_domains, as that is not
# the function of this host, but for ACLs and routers below.
# Note that the '${tr...} replaces the 'ldapm' newline separator with
# colons so the return results are a properly formatted hostlist
#domainlist internal_server_domains = ${tr { ${lookup ldapm \
#  {user="cn=[bind_cn]" pass=[bind_password] \
#  ldap:///cn=domains,cn=emailserver,o=example,c=com?\
#  cn?one?(objectClass=[domain_object_class])}} }\
#  {\n}{:}}
#
# Note that the above LDAP lookup is done for every email received.
# I would only recommend using it against an LDAP that is just for
# this lookup or in a development environment. It is really only handy
# in an ISP or hosting type environment, where domains are constantly
# being added and removed on the primary email servers.
# Alternatively use a static list (for the ACLs and routers below):
domainlist internal_server_domains = example.com:example.net:example.org
 
# Custom:
# The IPs of the internal primary SMTP relays. Used in some ACLs below.
hostlist intrelays_ips = 10.1.0.1:10.1.0.2:10.1.0.3
 
# Custom:
# The IPs of the external SMTP relays. Used in some ACLs below.
hostlist extrelays_ips = 172.16.0.1:172.16.0.2:172.16.0.3
 
# The following ACL entries are used if you want to do content scanning with
# the exiscan-acl patch. When you uncomment one of these lines, you must also
# review the respective entries in the ACL section further below.
 
# acl_smtp_mime = acl_check_mime
acl_smtp_data = acl_check_content
 
# This configuration variable defines the virus scanner that is used with
# the 'malware' ACL condition of the exiscan acl-patch. If you do not use
# virus scanning, leave it commented. Please read doc/exiscan-acl-readme.txt
# for a list of supported scanners.
 
# av_scanner = sophie:/var/run/sophie
#av_scanner = clamd: 127.0.0.1 3310
av_scanner = clamd:/var/run/clamav/clamd.sock
 
# ...
 
# Custom:
## Extra configuration tweaks not set by default
#
# Incoming tweaks
#
# Set the banner for initial SMTP connections
# Default is:
# smtp_banner = $primary_hostname ESMTP Exim \
#               $version_number $tod_full
# Remove Exim version number (this is per my company's security standards):
smtp_banner = $primary_hostname ESMTP Exim $tod_full
# Use this for Exim v4.62+:
#smtp_banner = $smtp_active_hostname ESMTP Exim $tod_full
 
# Max number of simultaneous SMTP calls to accept
# and immediately start delivery on. Default is 20
#smtp_accept_max = 20
smtp_accept_max = 75 
 
# Max number of simultaneous incoming SMTP calls before messages
# are just placed on the queue. Must be less than smtp_accept_max.
# Default is 0 (no limit)
smtp_accept_queue = 30
 
# Max number of waiting SMTP connections.
# Gives some protection against denial-of-service attacks by SYN flooding
# Default is 20
#smtp_connect_backlog = 20
 
# Max number of simultaneous connections per each IP
# Give 421 when limit is reached. Default is unset
# Can be set per host using lsearch like:
# smtp_accept_max_per_host = \
#   ${lookup{$sender_host_address}lsearch{/path/to/file} {$value}\
#      { ${lookup{${mask:$sender_host_address/24}}lsearch*{/path/to/file}} }\
#   }
# With the file [/path/to/file] containing lines like:
# 192.168.1.25:    4
# 172.21.34.0/24:  2
# *:               1
smtp_accept_max_per_host = 50
 
# Max number of delivery processes that Exim starts automatically when
# receiving messages via SMTP before starting to queue. Default is 10
#smtp_accept_queue_per_connection = 15
 
# When smtp_accept_max is set greater than zero, this option specifies a
# number of SMTP connections that are reserved for connections from the
# hosts that are specified in smtp_reserve_hosts. Default is 0
#smtp_accept_reserve = 0
smtp_accept_reserve = 30
 
# Hosts for which SMTP connections are reserved (host list, expanded)
# Default is unset
#smtp_reserve_hosts =
smtp_reserve_hosts = 127.0.0.1
 
# Max system load before not accepting SMTP calls,
# except for hosts defined in smtp_reserve_hosts
# Default is unset (fixed point)
#smtp_load_reserve
 
# Check disk resources before accepting mail
# A 452 temporary error response for SMTP.
# A message to stderr and return of non-zero for BSMTP.
# Check spool partition (make larger than message_size_limit)
# Default for all is 0
#check_spool_inodes = 100
check_spool_space = 40M
# Check log file partition
# Set only if on a different disk than the spool
#check_log_inodes = 2048
#check_log_space = 3M
 
# Max number of MAIL commands that Exim is prepared to accept over a
# single SMTP connection, after which a 421 is given. Default is 1000
smtp_accept_max_per_connection = 100
 
# Max number of RCPT TO commands per connection before 452
# Provides DoS protection, but RFCs specify min of 100.
# Default is 0 (unlimited)
recipients_max = 600
 
# Max number of non-mail commands per session before dropping
# connection. Provides some DoS protection. Default is 10
#smtp_accept_max_nonmail = 5
 
# Max message size to accept
# Default is 50M
#message_size_limit = 20M
 
# Max bounce message size to send
# Default is 100K
#return_size_limit = 10K
 
# Alternative to return_size_limit is to set
#bounce_return_message = false
 
# Give detailed rejections (ie '550-Rejected after DATA:...',
# vs just 'Administrative prohibition'). Default is false
#smtp_return_error_details
 
# Adjust what is logged. Add (+) or remove (-) options.
# See ch48 for defaults and what is available.
# The following adds subject line logging (T=)
# and the SMTP text after final "." on deliveries (C=)
log_selector = +subject +smtp_confirmation
 
#
# Clean up tweaks
#
# Redundant pairs of angle brackets around addresses are removed
# Default is false
strip_excess_angle_brackets = true
 
# Ignore a trailing dot at the end of a domain in an address
# Default is false
strip_trailing_dot = true
 
#
# Processing tweaks
#
# Intervals a warning message to the sender when there is a delay
# Default is 24h
#delay_warning = 2h:8h:24h:48h
 
# Time before a queue runner will try a new delivery attempt
# on any frozen message. Default is 0s
auto_thaw = 4d
 
# Set a reply-to for bounce messages from your host
# Default is unset
#errors_reply_to = Reply-To: postmaster@$qualify_domain
 
# Abandon queue runs if system load is greater than this
# Default is unset
deliver_queue_load_max = 16
 
# If system load is higher than this queue incoming messages
# Default is unset
queue_only_load = 16 
 
# Max queue-runner processes to run simultaneously
# Default is 5
#queue_run_max = 30
 
# Max parallel delivery of one message to multiple hosts
# Default is 2, 1 would be 'serial'
remote_max_parallel = 5
 
# If DNS lookups give 'try again' for DNS errors 'non-authoritative
# host not found' and 'serverfail' then this would cause that error
# to mean a fail. Default unset (domain list type)
#dns_again_means_nonexist = *.in-addr.arpa
 
# When retry data collected is considered old and ignored.
# Default is 7d
#retry_data_expire = 7d
 
# Allow non-root users to list the queue
# (We use this to allow a protected web page show info)
no_queue_list_requires_admin
 
#
# Use a system-wide filter file
#
#system_filter = /etc/exim/system_filter
#system_filter_user = exim
 
#
# Important address settings
#
 
# Who to send a mail to when a message is frozen
freeze_tell = postmaster@example.com
 
# Process IDs allowed to use '-f [address]' option
# and not have the Sender header line included.
# Not recommended for security reasons
#trusted_users = apache
 
# Allow untrusted users to to use '-f [address]' option
# where [address] matches the pattern below.
# Still sets the Sender header line and is
# safer than 'trusted_users'.
# (We use this from address in some web pages that generate emails)
untrusted_set_sender = ^webmaster@example.com
 
 
################################################################################
# Mailman configuration. Taken from the Exim How To documentation.             #
################################################################################
# Home dir for your Mailman installation -- aka Mailman's prefix
# directory.
# By default this is set to "/usr/local/mailman"
# On a Red Hat/Fedora system using the RPM use "/var/mailman"
# On Debian using the deb package use "/var/lib/mailman"
# This is normally the same as ~mailman
MM_HOME = /usr/local/mailman
 
# User and group for Mailman, should match your --with-mail-gid
# switch to Mailman's configure script.
# Value is normally "mailman"
MM_UID = mailman
MM_GID = mail
#MM_UID = exim
#MM_GID = mail
 
# Domains that your lists are in - colon separated list
# you may wish to add these into local_domains as well
domainlist mm_domains = +local_domains
 
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
#
# These values are derived from the ones above and should not need
# editing unless you have munged your mailman installation
#
# The path of the Mailman mail wrapper script
MM_WRAP = MM_HOME/mail/mailman
 
# The path of the list config file (used as a required file when
# verifying list addresses)
MM_LISTCHK = MM_HOME/lists/${lc::$local_part}/config.pck
# MM_LISTCHK = MM_HOME/lists/$local_part/config.pck
# MM_LISTCHK = MM_HOME/lists/${lc}/config.pck
################################################################################
# End of mailman configuration settings                                        #
################################################################################
 
 
######################################################################
#                       ACL CONFIGURATION                            #
#         Specifies access control lists for incoming SMTP mail      #
######################################################################
 
begin acl
 
#...
  #accept  authenticated = *
  #        control       = submission
 
  # Custom:
  # Do not accept HELO/EHLO from hosts using our IP(s) in HELO
  # Could exclude internal IPs, but they should never HELO with our inet IP
  # Note: To test with 'exim -bh [ip]', also use '-oMi [ip]' were ip is
  #       this host's ip, otherwise it will be blank and always return true.
  #deny message     = Forged name in HELO.
  warn message     = X-Spam-HELO: Forged name in HELO
       log_message = invalid HELO (our IP)
       !hosts      = +relay_from_hosts
       condition   = ${if match{$sender_helo_name}{$interface_address}{yes}{no}}
 
  # Custom:
  # Do not accept HELO/EHLO from hosts saying they are us
  #deny message     = Forged name in HELO.
  warn message     = X-Spam-HELO: Forged name in HELO
       log_message = invalid HELO (our local domain)
       !hosts      = +relay_from_hosts
       condition   = ${if match_domain{$sender_helo_name}{+local_domains}{yes}{no}}
 
  # Custom:
  # Do not accept HELO/EHLO name of 'localhost'
  #deny message     = Forged name in HELO.
  warn message     = X-Spam-HELO: Forged name in HELO
       log_message = invlaid HELO (localhost)
       !hosts      = +relay_from_hosts
       condition   = ${if match{$sender_helo_name}{localhost}{yes}{no}}
 
  # Custom:
  # Do not accept HELO/EHLO name that doesn't contain at least one period
  #deny message     = Forged name in HELO.
  warn message     = X-Spam-HELO: Forged name in HELO
       log_message = invalid HELO (bad hostname)
       !hosts      = +relay_from_hosts
       condition   = ${if match{$sender_helo_name}{\\.}{no}{yes}}
 
  #############################################################################
  # There are no checks on DNS "black" lists because the domains that contain
  # these lists are changing all the time. However, here are two examples of
  # how you could get Exim to perform a DNS black list lookup at this point.
  # The first one denies, while the second just warns.
  #
  # deny    message       = rejected because $sender_host_address is in a black list at $dnslist_domain\n$dnslist_text
  #         dnslists      = black.list.example
  #
  # warn    message       = X-Warning: $sender_host_address is in a black list at $dnslist_domain
  #         log_message   = found in $dnslist_domain
  #         dnslists      = black.list.example
  #############################################################################
  # Don't check blacklist when the connection is coming from specific host ips
  deny !hosts      = +relay_from_hosts:+intrelays_ips:+extrelays_ips
       message     = Spam filter: Mail from $sender_host_address refused via $dnslist_domain\n$dnslist_text
       log_message = $sender_host_address listed in (deny) $dnslist_domain
       dnslists    = [dns_hostname_for_blacklist1] : [dns_hostname_for_blacklist2]
 
  # Only warn because [blacklist3] tends to be overly quick to BL hosts
  warn !hosts      = +relay_from_hosts:+intrelays_ips:+extrelays_ips
       message     = X-Spam-RBL: Yes - $sender_host_address is blacklisted at $dnslist_domain
       log_message = $sender_host_address listed in (warn) $dnslist_domain
       dnslists    = [dns_hostname_for_blacklist3]
 
  # Accept if the address is in a local domain, but only if the recipient can
#...
 
# These access control lists are used for content scanning with the exiscan-acl
# patch. You must also uncomment the entries for acl_smtp_data and acl_smtp_mime
# (scroll up), otherwise the ACLs will not be used. IMPORTANT: the default entries here
# should be treated as EXAMPLES. You MUST read the file doc/exiscan-acl-spec.txt
# to fully understand what you are doing ...
 
acl_check_mime:
 
  # Decode MIME parts to disk. This will support virus scanners later.
  warn decode = default
 
  # Reject messages with serious MIME container errors
  deny  message = Found MIME error ($demime_reason).
        demime = *
        condition = ${if >{$demime_errorlevel}{2}{1}{0}}
 
  # File extension filtering.
  deny message = Blacklisted file extension detected
       condition = ${if match \
                        {${lc:$mime_filename}} \
                        {\N(\.exe|\.pif|\.bat|\.scr|\.lnk|\.com)$\N} \
                     {1}{0}}
 
  # Reject messages that carry chinese character sets.
  # WARNING: This is an EXAMPLE.
  #deny message = Sorry, noone speaks chinese here
  #     condition = ${if eq{$mime_charset}{gb2312}{1}{0}}
 
  accept
 
acl_check_content:
 
  # Custom:
  # Check cryptographic header. If it matches, accept the
  # message because it has already been checked by us.
  # Saves some processing
  accept condition = ${if eq {${hmac{md5}\
                                    {salt_word_here}\
                                    {$body_linecount}}}\
                             {$h_X-SeenV-Signature:} {1}{0}}
 
  # Custom:
  # Add our cryptographic header.
  warn message = X-SeenV-Signature: ${hmac{md5}{salt_word_here}\
                                         {$body_linecount}}
 
  # Deny if the message contains a virus. Before enabling this check, you
  # must install a virus scanner and set the av_scanner option above.
  #
   deny    malware   = *
           message   = This message contains a virus ($malware_name).
 
  # Always add X-Spam-Score and X-Spam-Report headers, using SA system-wide settings
  # (user "nobody"), no matter if over threshold or not.
#  warn  message = X-Spam-Score: $spam_score ($spam_bar)
#        spam = nobody:true
#  warn  message = X-Spam-Report: $spam_report
#        spam = nobody:true
#
#  # Add X-Spam-Flag if spam is over system-wide threshold
#  warn message = X-Spam-Flag: YES
#       spam = nobody
#
#  # Reject spam messages with score over 10, using an extra condition.
#  deny  message = This message scored $spam_score points. Congratulations!
#        spam = nobody:true
#        condition = ${if >{$spam_score_int}{100}{1}{0}}
 
  # finally accept all the rest
  accept
 
 
######################################################################
#                      ROUTERS CONFIGURATION                         #
#               Specifies how addresses are handled                  #
######################################################################
#     THE ORDER IN WHICH THE ROUTERS ARE DEFINED IS IMPORTANT!       #
# An address is passed to each router in turn until it is accepted.  #
######################################################################
 
begin routers
 
#...
# domain_literal:
#   driver = ipliteral
#   domains = ! +local_domains
#   transport = remote_smtp
 
# Custom:
# This router detects email bound for our hosted email domains. The reason
# for this router is our MX points to our external SMTP relays, whereas this
# host is internal and can skip the extra processing. However the second call of
# $domain/MX will do an MX lookup for the domain should the first host
# specified soft fail in some way (ie busy, network issue, etc).
# Note that this configuration does not make this host a relay for the
# domains obtained in the lookup. To do that you would need to set the
# relay_to_hosts (don't for this setup).
 
internaldomains:
  driver = manualroute
  domains = ! +local_domains:+internal_server_domains
  transport = remote_smtp
  route_list = * smtp.example.com:$domain/MX
  no_more
 
#...
dnslookup:
  driver = dnslookup
  domains = ! +local_domains
  transport = remote_smtp
  ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8
  no_more
 
 
# The remaining routers handle addresses in the local domain(s).
 
################################################################################
# Mailman configuration. Taken from the Exim How To documentation.             #
################################################################################
# This router can figure out what addresses provisioned mailman lists
# exist on the system, so that you don't have to add lines to '/etc/aliases'
# for every mailman list.
  mailman_router:
    driver = accept
    domains = +mm_domains
    require_files = MM_LISTCHK
    local_part_suffix_optional
    local_part_suffix = -admin     : \
 -bounces   : -bounces+* : \
 -confirm   : -confirm+* : \
 -join      : -leave     : \
 -owner    : -request   : \
 -subscribe : -unsubscribe
    transport = mailman_transport
 
# This router handles aliasing using a linearly searched alias file with the
# name SYSTEM_ALIASES_FILE. When this configuration is installed automatically,
#...
 
######################################################################
#                      TRANSPORTS CONFIGURATION                      #
######################################################################
#                       ORDER DOES NOT MATTER                        #
#     Only one appropriate transport is called for each delivery.    #
######################################################################
 
# A transport is used only when referenced from a router that successfully
# handles an address.
 
begin transports
 
#...
local_delivery:
  driver = appendfile
  file = /var/mail/$local_part
  delivery_date_add
  envelope_to_add
  return_path_add
  group = mail
  mode = 0660
 
###############################################################################
# Mailman configuration. Taken from the Exim How To documentation.             #
################################################################################
# This transport is used for delivery to mailman lists.
  mailman_transport:
    driver = pipe
    command = MM_WRAP \
              '${if def:local_part_suffix \
                    {${sg{$local_part_suffix}{-(\\w+)(\\+.*)?}{\$1}}} \
                    {post}}' \
              $local_part
    current_directory = MM_HOME
    home_directory = MM_HOME
    user = MM_UID
    group = MM_GID
 
 
# This transport is used for handling pipe deliveries generated by alias or
# .forward files. If the pipe generates any standard output, it is returned
#...
 
######################################################################
#                      RETRY CONFIGURATION                           #
######################################################################
 
begin retry
 
# This single retry rule applies to all domains and all errors. It specifies
# retries every 15 minutes for 2 hours, then increasing retry intervals,
# starting at 1 hour and increasing each time by a factor of 1.5, up to 16
# hours, then retries every 6 hours until 4 days have passed since the first
# failed delivery.
 
# Address or Domain    Error       Retries
# -----------------    -----       -------
 
example.com	       *           F,15m,15m; G,16h,1h,1.5; F,2d,6h
*                      *           F,2h,15m; G,16h,1h,1.5; F,2d,6h
 
#...

If you need to setup multiple instances of Mailman, say one to run static lists and one that can get list members via an LDAP query, it is very possible to do this. (Note: I'm going from memory at the moment and need to verify my notes from a test configuration we did awhile ago.)

  • First, add new variables that points to the second Mailman installation:
    # Home dir for your Mailman installation -- aka Mailman's prefix
    # directory.
    #...
    MM_HOME = /usr/local/mailman
    MM_HOME_LDAP = /usr/local/mailman-ldap
     
    # User and group for Mailman, should match your --with-mail-gid
    # switch to Mailman's configure script.
    # Value is normally "mailman"
    MM_UID = mailman
    MM_GID = mail
     
    #...
    # The path of the Mailman mail wrapper script
    MM_WRAP = MM_HOME/mail/mailman
    MM_WRAP_LDAP = MM_HOME_LDAP/mail/mailman
     
    # The path of the list config file (used as a required file when
    # verifying list addresses)
    MM_LISTCHK = MM_HOME/lists/${lc::$local_part}/config.pck
    MM_LISTCHK_LDAP = MM_HOME_LDAP/lists/${lc::$local_part}/config.pck
  • In the single Mailman instance config, above, you'll notice that mm_domains equals local_domains. This has to change so the routers for each Mailman instance know which domain is to be handled by which instance. For example:
    # Domains that your lists are in - colon separated list
    # you may wish to add these into local_domains as well
    domainlist mm_domains = lists.example.com:lists.example.net
    domainlist mm_ldap_domains = dynlist.example.com:dynlist.example.net
  • Next, in the routers, define another router to handle the second installation:
      mailman_ldap_router:
        driver = accept
        domains = +mm_ldap_domains
        require_files = MM_LISTCHK_LDAP
        local_part_suffix_optional
        local_part_suffix = -admin     : \
     -bounces   : -bounces+* : \
     -confirm   : -confirm+* : \
     -join      : -leave     : \
     -owner    : -request   : \
     -subscribe : -unsubscribe
        transport = mailman_ldap_transport

    Technically, if you are using the LDAP patch for Mailman, the join/leave/subscribe/unsubscribe addresses will not work as the membership list is obtained from an LDAP lookup. Also the list will not be able to remove addresses after bounces. I wanted to leave those addresses in, just in case you have multiple instances of Mailman installed for other reasons.
  • Finally, in transports, define another transport for the second installation:
      mailman_ldap_transport:
        driver = pipe
        command = MM_WRAP_LDAP \
                  '${if def:local_part_suffix \
                        {${sg{$local_part_suffix}{-(\\w+)(\\+.*)?}{\$1}}} \
                        {post}}' \
                  $local_part
        current_directory = MM_HOME_LDAP
        home_directory = MM_HOME_LDAP
        user = MM_UID
        group = MM_GID

A bit about this server and how it sits in relationship with our other email servers:

  • The MX record for the email domains we host, including the listserv domains, points to one set of servers. These servers basically do nothing but virus scanning and spam filtering for incoming email.
  • For the email service there is another set of SMTP servers. These handle authenticated connections from our customers, so they can send, and handle inbound email, destined for our customer's mailboxes, that have past the checks on the MX servers or were sent by an authenticated user.
  • Then there is this server, which primarily runs the listservs our customers want using Mailman.

If you are wondering why internal_relays and external_relays, in the config file above, are showing private IPs it is because I am only giving them as an example. I would not want to show a publicly accessible IP, even if it was not one of ours.