Exim

Exim is a powerful and the most popular Mail Transfer Agent (MTA). It's reported to have a 57% market share. Also, Exim is the default MTA for Debian / Ubuntu.

Pre-installation
As only one MTA can be installed at the same time on a system, it might be required to remove an installed MTA. The package manager will report a block when another MTA is still installed. This block can resolved by manually deselecting the old mail server. For example, preparing to remove (which might have been installed as the default when a program requested a mail server to be installed) with this command:

Use flags
For a typical install, you can install with the following use flags:

If you plan to use dovecot as your POP/IMAP server and want to make exim use dovecot's authentication services, add a use flag for dovecot-sasl.

If you want to use a database to manage what mailboxes and what aliases exist, add a use flag for the database you'd like to use, e.g. postgres, sqlite, etc.

Complete mailserver
For a complete mailserver, you'll typically also want to install a POP/IMAP server, such as dovecot or courier-imap. These so-called MDAs tightly integrate with Exim (your MTA) and possibly your database.

Also, you might want to install a webmail server, such as Squirrelmail or Roundcube, which will also integrate with your MTA, MDA and database.

Configuration
The exim ebuild creates a small number of configuration files in /etc/exim, namelyː auth_conf.sub exim.conf exim.conf.exiscan-acl and system_filter.exim. Of these files, the main configuration file is /etc/exim/exim.conf.

This exim.conf file can include several sectionsː
 * General options, setting up parameters such as hostname and domains, hooking up ACL's, SSL/TLS settings, ports and logging.
 * ACL, where specific rules can be written for different phases of the SMTP protocol for incoming e-mail.
 * Routers, which determine based on the addresses of the e-mail to which exim-transport the message should be routed
 * Transports, for actually delivering e-mails (either remotely, or locally)
 * Retry, because we might not want to give up after the first attempt
 * Rewrite, in case we want to touch content of the e-mail or its headers
 * Authenticators, as we only want to send e-mails out into the world if it was submitted by someone we trust
 * Local_scan, in case *really* specific scanning hook-ups are needed

Modern mail security (SPF/DKIM/DMARC)
With the rise of security in computing, mail security is also improving. After a number of solutions have been tried, the mailserver community seems to have settled on the combination of SPF, DKIM and DMARC.

SPF:
 * SPF focuses on Envelope-From (aka Return-Path, MAIL FROM, Bounce address)
 * SPF authenticates the IP of the sending server against SPF sending server policy
 * SPF does not require alignment between Header-From and Envelope-From
 * SPF does not survive forwarding and indirect mail-flows
 * SPF does not tell the receiving server what it should do with messages that fail SPF

DKIM:
 * DKIM focuses on asymmetrical crypto and control over DNS records
 * DKIM crypto-proves the d= domain signed the e-mail
 * DKIM crypto-proves the integrity of headers and/or body
 * DKIM does not require alignment between Header-From and d= domain
 * DKIM may survive forwarding and indirect mail-flows depending on what parts were signed
 * DKIM does not tell the receiving server what it should do with messages that fail DKIM

DMARC:
 * DMARC requires alignment between Header-From and Envelope-From used by SPF and/or d= domain field in DKIM signature
 * DMARC ignores the difference in SPF between hard fail (-all) and soft fail (~all) and treats both as fail
 * DMARC allows defining a policy that receivers can follow for messages that fail SPF and/or DKIM tests
 * DMARC allows reporting mailboxes to be defined

Example for SPF, DKIM with SQLite and Maildir
This configuration enables SPF and DKIM, but only adds warning headers, it doesn't quarantine or reject e-mails. Further it uses SQLite for managing what mailboxes and aliases exist, and stores mails in a special directory in Maildir format.

{{FileBox|filename=/etc/exim/exim.conf|lang=ini|1=

primary_hostname = mailserver.example.com
 * 1) Explicitly set our HELO name

domainlist local_domains = example.com : crossexample.net : @ hostlist  relay_from_hosts = 127.0.0.1 : localhost
 * 1) Define domain lists

acl_smtp_connect = acl_check_connect acl_smtp_helo = acl_check_helo acl_smtp_mail = acl_check_mail_from acl_smtp_dkim = acl_check_dkim acl_smtp_rcpt = acl_check_rcpt acl_smtp_data = acl_check_data
 * 1) Connect access control lists to their hooks

tls_advertise_hosts = *
 * 1) Allow any client to use TLS

tls_certificate = /etc/ssl/mailserver.example.com-signed.crt tls_privatekey = /etc/ssl/private/mailserver.example.com.key
 * 1) Specify the location of the Exim server's TLS certificate and private key

tls_require_ciphers = TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-AES-128-GCM-SHA256:EECDH+CHACHA20:EECDH+AESGCM:EECDH+AES tls_dhparam = /etc/ssl/private/dhparam.pem openssl_options = +no_sslv2 +no_sslv3
 * 1) Set modern ciphers (assuming gentoo default of exim with openssl)

daemon_smtp_ports = 25 : 587
 * 1) Support acting as smtp.example.com server for users outside our network
 * 1) tls_on_connect_ports = 465 (required for Microsoft clients)

qualify_domain = example.com
 * 1) Add our domain to 'unqualified addresses', instead of our server name

host_lookup = *
 * 1) Perform rDNS lookups for incoming connections, so we can confirm their HELO

prdr_enable = true
 * 1) Advertise PRDR to accept/reject multi-recipient mails per-user

log_selector = +smtp_protocol_error +smtp_syntax_error \ +tls_certificate_verified
 * 1) Specify what should be logged.

ignore_bounce_errors_after = 2d timeout_frozen_after = 7d
 * 1) Prevent clogging the queue when mail delivery and return-to-sender both fail

keep_environment =
 * 1) If we don't set the following option, Exim will complain mildly

begin acl
 * 1) ACL CONFIGURATION
 * 1) ACL CONFIGURATION


 * 1) Work through the SMTP protocol

acl_check_connect:
 * 1) Trigger on an incoming connection

# Record the current timestamp, in order to delay crappy senders warn set acl_m0 = $tod_epoch

# Warn if reverse DNS lookup failed. Note: later we compare rDNS with HELO warn !verify    = reverse_host_lookup set acl_c1 = X-DNS-Warning: Couldn't lookup reverse DNS for ${sender_host_address}

# Set up for finalisation: write to log warn condition  = ${if def:acl_c1 {true}{false} } logwrite   = $acl_c1

# On error: accept but stall (good actors tend to be patient) accept set acl_m0 = ${if def:acl_c1 {${eval:20 + $acl_m0 - $tod_epoch} }{0} } delay      = ${if >{$acl_m0}{0}{$acl_m0}{0} }s

acl_check_helo:
 * 1) Trigger on the HELO/EHLO of incoming connections.

# Record the current timestamp, in order to delay crappy senders warn set acl_m0 = $tod_epoch

# Warn if sender did not provide a HELO/EHLO greeting warn condition  = ${if !def:sender_helo_name {true}{false} } set acl_c2 = X-HELO-Warning: Remote host $sender_host_address \ did not present HELO/EHLO greeting.

# Warn if sender greeted with an IP address (technically correct, but crappy) warn condition  = ${if !def:acl_c2 {true}{false} } condition  = ${if isip {$sender_helo_name}{true}{false} } set acl_c2 = X-HELO-Warning: Remote host $sender_host_address \ used IP address instead of hostname in HELO/EHLO

# Warn if sender pretended to be us (they may think that would trick us) warn condition  = ${if !def:acl_c2 {true}{false} } condition  = ${if match_domain{$sender_helo_name}\ {$primary_hostname:+local_domains}{true}{false} } set acl_c2 = X-HELO-Warning: Remote host $sender_host_address \ used our name in HELO/EHLO but it is not defined in \ our +local_domains

# Warn if HELO verification fails, e.g. rDNS of IP != HELO domain warn condition  = ${if !def:acl_c2 {true}{false} } !verify    = helo set acl_c2 = X-HELO-Warning: Remote host ${if def:sender_host_name \ {$sender_host_name ($sender_host_address) }\ {$sender_host_address } }\ incorrectly presented itself as $sender_helo_name

# Set up for finalisation: write to log warn condition  = ${if def:acl_c2 {true}{false} } logwrite   = $acl_c2

# On error: accept but stall (good actors tend to be patient) accept set acl_m0 = ${if def:acl_c2 {${eval:20 + $acl_m0 - $tod_epoch} }{0} } delay      = ${if >{$acl_m0}{0}{$acl_m0}{0} }s

acl_check_mail_from:
 * 1) Trigger on MAIL FROM: command. Here we perform SPF checks.

# Skip SPF checks for all authenticated connections (probably MUAs) accept authenticated = *

# Record the current timestamp, in order to delay crappy senders warn set acl_m0 = $tod_epoch

# Query if we can find an SPF policy for the sending domain warn spf        = none:temperror:permerror set acl_c3 = X-SPF-Warning: can't find SPF policy for \ $sender_address_domain

# Query if the sending domain has set a useful SPF policy warn condition  = ${if !def:acl_c3 {true}{false} } condition  = ${if match {${lookup dnsdb{txt=$sender_address_domain}{$value} } }{\\+all} } set acl_c3 = X-SPF-Warning: $sender_address_domain allows +all in SPF policy

# Query the SPF policy whether sender was authorized to deliver this message # Note: fail = '-all', softfail = '~all', neutral = '?all' warn spf        = fail:softfail:neutral condition  = ${if !def:acl_c3 {true}{false} } set acl_c3 = X-SPF-Warning: $sender_host_address is not allowed to \ send mail for $sender_address_domain

# Add a SPF-Received: line to the message header (regardless of SPF status) warn add_header = $spf_received

# Set up for finalisation: add all headers and write to log warn condition  = ${if def:acl_c3 {true}{false} } logwrite   = $acl_c3 add_header = $acl_c1 add_header = $acl_c2 add_header = $acl_c3

# On error: accept but stall (good actors tend to be patient) accept set acl_m0 = ${if or { {def:acl_c1}{def:acl_c2}{def:acl_c3} } {${eval:20 + $acl_m0 - $tod_epoch} }{0} } delay      = ${if >{$acl_m0}{0}{$acl_m0}{0} }s

acl_check_dkim:
 * 1) This access control list is used to process DKIM status.

# Skip DKIM checks for all authenticated connections (probably MUAs) accept authenticated = *

# Record the current timestamp, in order to delay crappy senders warn set acl_m0 = $tod_epoch

# Warn no DKIM warn dkim_status = none set acl_c4 = X-DKIM-Warning: No signature found

# RFC 8301 requires 'permanently failed evaluation' for DKIM signatures signed with 'historic algorithms (currently, rsa-sha1)' # @SEE: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-dkim_and_spf.html warn condition             = ${if !def:acl_c4 {true}{false} } condition             = ${if eq {$dkim_verify_status}{pass} } condition             = ${if eq {${length_3:$dkim_algo} }{rsa} } condition             = ${if or { {eq {$dkim_algo}{rsa-sha1} } \ {< {$dkim_key_length}{1024} } } } set acl_c4            = X-DKIM-Warning: forced DKIM failure (weak hash or short key) set dkim_verify_status = fail set dkim_verify_reason = hash too weak or key too short

# RFC6376 requires that verification fail if the From: header is not included in the signature # @SEE: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-dkim_and_spf.html warn condition  = ${if !def:acl_c4 {true}{false condition  = ${if !inlist{from}{$dkim_headernames}{true}{false set acl_c4 = X-DKIM-Warning: From: header not included in the \ signature, this defies the purpose of DKIM

# Warn invalid or failed signatures warn condition  = ${if !def:acl_c4 {true}{false} } dkim_status = fail:invalid set acl_c4 = X-DKIM-Warning: verifying signature of $dkim_cur_signer \ failed for $sender_address because $dkim_verify_reason

# Add a DKIM-Received: line to the message header (regardless of DKIM status) warn add_header = Received-DKIM: $dkim_verify_status ${if \ def:dkim_cur_signer {($dkim_cur_signer with \                       $dkim_algo for $dkim_headernames)} }

# Set up for finalisation: add header and write to log warn condition  = ${if def:acl_c4 {true}{false} } add_header = $acl_c4 logwrite   = $acl_c4

# On error: accept but stall (good actors tend to be patient) accept set acl_m0 = ${if def:acl_c4 {${eval:20 + $acl_m0 - $tod_epoch} }{0} } delay      = ${if >{$acl_m0}{0}{$acl_m0}{0} }s

acl_check_rcpt:
 * 1) Trigger on RCPT of incoming message.

# Test empty sending host field to accept local SMTP (i.e. not over network) accept hosts        = : control      = dkim_disable_verify

# Strange non-alphanumeric characters may indicate a user trying to trick us; # 1/2: incoming mail address may not begin with. or contain @ % ! / or {{!}} deny message	= Restricted characters in address domains	= +local_domains local_parts  = ^[.] : ^.*[@%!/{{!}}]

# 2/2: outgoing mail address may not begin with. / or {{!}} and neither may it #      contain @ % ! or the sequence /../ (this is slightly looser than 1/2) deny message	= Restricted characters in address domains	= !+local_domains local_parts  = ^[./{{!}}] : ^.*[@%!] : ^.*/\\.\\./

# Deny unless we have a route to deliver mail back to the sender address require verify       = sender

# Skip other checks for all authenticated connections (probably MUAs), set # submission mode and assume MUAs send address in From: that includes @domain accept authenticated = * control	= submission/sender_retain control	= dkim_disable_verify add_header   = X-Authenticated-Sender: ${sender_address} set acl_c_authenticated = 1

# Insist that a HELO/EHLO was accepted. require message	= nice hosts say HELO first condition    = ${if def:sender_helo_name}

# Insist that any other recipient address is in one of our local domains require message      = relay not permitted domains      = +local_domains

# Ensure we can deliver the message before we accept it require verify       = recipient

# If it got through to this point, accept it without further checks accept

acl_check_data:
 * 1) Message has been received, now we can check its contents (headers, body, etc)
 * 2) Note: here we can invoke external virus or spam scanners, if available

# Deny if the message contains an overlong line, as per the standards deny message      = maximum allowed line length is 998 octets, \ got $max_received_linelength condition    = ${if > {$max_received_linelength}{998} }

# Deny if the headers contain badly-formed addresses. deny !verify      = header_syntax message      = header syntax logwrite     = header syntax ($acl_verify_message)

# 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).

# Add headers to a message if it is judged to be spam. Before enabling this, # you must install SpamAssassin. You may also need to set the spamd_address # option above. # 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

# Accept the message. accept logwrite   = Received message from $sender_address_domain


 * 1) ROUTERS
 * 1) ROUTERS

begin routers


 * 1) Routers that handle addresses in remote domains (! +local_domains).
 * 1) Routers that handle addresses in remote domains (! +local_domains).

dnslookup: driver = dnslookup driver = dnslookup domains = ! +local_domains transport = remote_smtp ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8 no_more
 * 1) Route addresses by doing a DNS lookup on the domain name.
 * 2) Domains that resolve to 0.0.0.0 or to loopback (127.0.0.0/8) are treated as
 * 3) if they have no DNS entry: lookup fails, 'no_more' intervenes and the
 * 4) address is handled as unrouteable.


 * 1) Routers that handle addresses in local domains ("domainlist local_domains").
 * 2) If none of these routers picks up the message, "Unknown user" is returned to
 * 3) sender.
 * 1) If none of these routers picks up the message, "Unknown user" is returned to
 * 2) sender.


 * 1) This section is heavily dependent on your setup, especially how you wish to
 * 2) manage the 'local_parts', i.e. what aliases exist, what mailboxes exist and
 * 3) where mailboxes are stored.
 * 4) Traditionally answers to these questions were files that have existed since
 * 5) dawn of Unix tradition, but nowadays it's typical to put this in a database.
 * 6) If you already administer one or more databases, you can use it here as well.
 * 7) But if you don't, you can consider sqlite or flat files, so you don't need to
 * 8) learn about database-upgrades, etc. This example uses an sqlite database.

aliases: debug_print = "Redir: virtual_aliases for ${local_part}@${domain}" driver = redirect domains = +local_domains local_part_suffix = -* local_part_suffix_optional file_transport = redir_virtual_directory directory_transport = redir_virtual_directory
 * 1) If recepient is an alias, redirect it to the appropriate mailbox
 * 1) Change the following two lines to reflect your database setup
 * 2)  condition = ${if eq{${local_part} }{${lookup sqlite {/path/to/db.sqlite SELECT alias FROM aliases WHERE domain='${quote_sqlite:$domain}' AND alias='${quote_sqlite:$local_part}'; }  }
 * 3)  data = /path/to/mailboxes/${domain}/${lookup sqlite {/path/to/db.sqlite SELECT userid FROM aliases WHERE domain='${quote_sqlite:$domain}' AND alias='${quote_sqlite:$local_part}'; } }

users: debug_print = "Acc: userid for ${local_part}@${domain}" driver = accept no_check_local_user domains = +local_domains local_part_suffix = -* local_part_suffix_optional transport = accept_virtual_directory cannot_route_message = Unknown user
 * 1) If recepient is a mailbox on its own, deliver the message.
 * 2) This router treats local parts with suffixes introduced by "-" as if the
 * 3) suffixes did not exist. E.g. xxxx-foo@your.domain will be treated
 * 4) in the same way as xxxx@your.domain by this router.
 * 1) Change the following line to reflect your database setup
 * 2)  condition = ${if eq{${local_part} }{${lookup sqlite {/path/to/db.sqlite SELECT userid FROM users WHERE domain='${quote_sqlite:$domain}' AND userid='${quote_sqlite:$local_part}'; }  }


 * 1) TRANSPORTS
 * 2) Transports can only be called by a router picking up the message
 * 1) Transports can only be called by a router picking up the message
 * 1) Transports can only be called by a router picking up the message

begin transports


 * 1) Transport for delivering over SMTP (e.g. outgoing e-mail)
 * 1) Transport for delivering over SMTP (e.g. outgoing e-mail)

remote_smtp: driver = smtp message_size_limit = ${if > {$max_received_linelength}{998} {1}{0} } dkim_domain = ${sender_address_domain} dkim_selector = dkim dkim_private_key = /etc/ssl/private/dkim-mailserver.example.com.key dkim_canon=relaxed
 * 1) Refuse messages with over-long lines as per standard
 * 2) Note: the use of message_size_limit is a red herring.


 * 1) Transport for delivering to local mailbox
 * 1) Transport for delivering to local mailbox

accept_virtual_directory: driver = appendfile maildir_format user = vmail group = vmail mode = 0600 directory = /path/to/mailboxes/${domain}/${local_part}/ delivery_date_add
 * 1) This transport is used for local delivery to user mailboxes w/ Maildir

redir_virtual_directory: driver = appendfile maildir_format user = vmail group = vmail mode = 0600


 * 1) RETRY
 * 1) RETRY

begin retry

*                   *           F,2h,15m; G,16h,1h,1.5; F,4d,6h
 * 1) Because we're not going to give up after our first attempt
 * 2) Address or Domain    Error       Retries


 * 1) AUTHENTICATION
 * 1) AUTHENTICATION

begin authenticators

dovecot_login: driver = dovecot public_name = LOGIN server_socket = /var/run/dovecot/auth-client server_set_id = $auth1
 * 1) Authentication can be finicky to set up, but will also be needed by the
 * 2) POP/IMAP server you will probably also be setting up. This example makes use
 * 3) of dovecot to handle authentication for our SMTP clients.

dovecot_plain: driver = dovecot public_name = PLAIN server_socket = /var/run/dovecot/auth-client server_set_id = $auth1

}}

Example for SpamAssasin, ClamAV with PostgreSQL and Maildir
This possibly outdated configuration uses SpamAssasin to filter spam, ClamAV to detect if e-mails contain virusses, PostgreSQL for managing what mailboxes exist, and stores mails in users' home directories in Maildir format.

Testing Exim
Once the configuration file is ready we can test the the file with the following command:

If everything goes right we can now start exim

Now we must be able to test how exim will route some addresses

Finally we can do the bunny test