📜 ⬆️ ⬇️

Advanced Exim and Dovecot configuration with OpenLDAP binding

This article discusses Exim, Dovecot, and OpenLDAP advanced collaboration settings based on my experience with these applications. Perhaps, someone will find for themselves something interesting and new - this was the purpose of writing another howto on this topic.

Why Exim and OpenLDAP, and not Postfix and MySQL, for example? Postfix works fine out of the box, but if something extraordinary is needed, very soon Postfix turns into a hulking monster hung with pearl scripts, Exim has a monstrous meta configuration language and allows you to do without third-party scripts and crutches. I considered MySQL redundant for my tasks and replaced with standard OpenLDAP, especially LDAP is used for the work of the address book. Dovecot is very smart and easy to set up, plus it integrates well with both Exim and OpenLDAP.

So, install the necessary software, everything is standard here (apt-get, yum, etc.). I used Gentoo, so emerge openldap dovecot exim (exim and dovecot must have ldap support).

Used USE flags when building:
')
net-nds/openldap-2.4.40-r3::x-overlay USE="berkdb crypt gnutls overlays samba sasl ssl syslog mail-mta/exim-4.85::gentoo USE="dkim dnsdb dovecot-sasl dsn exiscan-acl gnutls ldap lmtp maildir pam pkcs11 prdr spf ssl syslog net-mail/dovecot-2.2.18::gentoo USE="bzip2 caps ldap maildir managesieve pam sieve ssl zlib 

The first in the queue will be OpenLDAP, all mail accounts, groups and aliases will be stored in its database, and OpenLDAP will also be used as an address book for mail clients. For simplicity and convenience, I do not use slapd-config, but I store all the settings in the text slapd.conf.

Since the standard OpenLDAP does not have a suitable mailing scheme, I used my modified version of phamm.schema and the default phamm-vacation.schema .

OpenLDAP configuration


I omit the primary configuration, the creation basedn dc = domain, dc = com and ssl certificates for OpenLDAP, since everything is standard here.

Slapd.conf config
 include /etc/openldap/schema/core.schema include /etc/openldap/schema/cosine.schema include /etc/openldap/schema/corba.schema include /etc/openldap/schema/inetorgperson.schema include /etc/openldap/schema/nis.schema include /etc/openldap/schema/misc.schema #    include /etc/openldap/schema/phamm.schema include /etc/openldap/schema/phamm-vacation.schema pidfile /run/openldap/slapd.pid argsfile /run/openldap/slapd.args #        TLS TLSCACertificateFile /etc/openldap/ssl/cacert.pem TLSCertificateFile /etc/openldap/ssl/newcert.pem TLSCertificateKeyFile /etc/openldap/ssl/newkey.pem TLSProtocolMin 3.1 TLSVerifyClient allow database bdb #   bdb , ,  . cachesize 100000 suffix "dc=domain,dc=com" rootdn "uid=manager,dc=domain,dc=com" #   rootpw **** directory /var/lib/openldap-data checkpoint 32 30 idletimeout 120 writetimeout 120 loglevel none overlay syncprov #    syncprov syncprov-checkpoint 100 10 syncprov-sessionlog 100 #   index uid,accountActive,vacationActive,createMaildir eq index cn,givenName,sn,mail pres,eq,sub index uidNumber,gidNumber,memberUid eq index entryCSN,entryUUID eq index objectClass,member,uniqueMember eq #      # ,    limits dn="uid=replicator,ou=services,dc=domain,dc=com" size=unlimited time=unlimited # ,  phpldapadmin limits dn="uid=ldapadmin,ou=services,dc=domain,dc=com" size=unlimited time=unlimited #  exim limits dn="uid=exim,ou=services,dc=domain,dc=com" size=unlimited time=unlimited #  nsswitch limits dn="uid=proxyagent,ou=services,dc=domain,dc=com" size=unlimited time=unlimited #       access to attrs=userPassword by dn.base="uid=ldapadmin,ou=services,dc=domain,dc=com" write by dn.base="uid=replicator,ou=services,dc=domain,dc=com" read by dn.base="uid=proxyagent,ou=services,dc=domain,dc=com" read by anonymous auth by self write by * none access to attrs=mail by dn.base="uid=ldapadmin,ou=services,dc=domain,dc=com" write by * read access to * by dn.base="uid=ldapadmin,ou=services,dc=domain,dc=com" write by users read by anonymous auth 


Be sure to set up replication for OpenLDAP (again, everything is standard here, so I omit the details). The only thing I had to collect OpenLDAP with my hands to support sssvlv (Server Side Sorting and Virtual List View) in the address book (./configure - enable-ipv6 = no - enable-syncprov = yes - enable-sssvlv = yes - with-tls = yes).
Also, in order for sssvlv to work correctly in an autlux, I had to patch the OpenLDAP sources.
Patch
 --- servers/slapd/schema_prep.c 2011-11-25 20:52:29.000000000 +0200 +++ servers/slapd/schema_prep.c 2011-11-29 13:46:57.000000000 +0200 @@ -915,6 +915,7 @@ offsetof(struct slap_internal_schema, si_ad_name) }, { "cn", "( 2.5.4.3 NAME ( 'cn' 'commonName' ) " "DESC 'RFC4519: common name(s) for which the entity is known by' " + "ORDERING caseIgnoreOrderingMatch " "SUP name )", NULL, 0, NULL, NULL, @@ -924,6 +925,7 @@ "DESC 'RFC4519: user identifier' " "EQUALITY caseIgnoreMatch " "SUBSTR caseIgnoreSubstringsMatch " + "ORDERING caseIgnoreOrderingMatch " "SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", NULL, 0, NULL, NULL, (END) 


The second server will work as an address book in read-only mode.

Slapd.conf config for second LDAP server
 include /etc/ldap/schema/corba.schema include /etc/ldap/schema/core.schema include /etc/ldap/schema/cosine.schema include /etc/ldap/schema/inetorgperson.schema include /etc/ldap/schema/misc.schema include /etc/ldap/schema/nis.schema #  slapd         include /etc/ldap/schema/phamm-vacation.schema include /etc/ldap/schema/phamm.schema # Load dynamic backend modules: #modulepath /usr/lib/ldap #moduleload back_hdb.so #moduleload sssvlv.so # sssvlv     Outlook idletimeout 120 threads 8 sizelimit 1000 pidfile /var/run/slapd/slapd.pid argsfile /var/run/slapd/slapd.args loglevel 0 #        TLS TLSCACertificateFile /etc/ldap/ssl/ca.pem TLSCertificateFile /etc/ldap/ssl/ab.domain.com_crt.pem TLSCertificateKeyFile /etc/ldap/ssl/ab.domain.com_key.pem TLSProtocolMin 3.1 TLSVerifyClient allow database hdb #     hdb cachesize 100000 suffix "dc=domain,dc=com" rootdn "cn=replicator,ou=services,dc=domain,dc=com" rootpw ***** directory /var/lib/ldap checkpoint 32 30 idletimeout 120 writetimeout 120 overlay sssvlv #    TLS,  syncrepl syncrepl rid=001 # ID  provider=ldaps://domain.com #  ldaps:// ,    starttls     self-signed  type=refreshOnly interval=00:00:10:00 searchbase="dc=domain,dc=com" scope=sub schemachecking=off bindmethod=simple binddn="uid=replicator,ou=services,dc=domain,dc=com" #   posix   ou=services,   slapd.conf credentials=**** tls_cacertdir=/etc/ssl/certs tls_cacert=/etc/ldap/ssl/ca.pem tls_cert=/etc/ldap/ssl/ab.domain.com_crt.pem tls_key=/etc/ldap/ssl/ab.domain.com_key.pem tls_reqcert=allow index uid,accountActive,vacationActive eq index cn,givenName,sn,mail pres,eq,sub index uidNumber,gidNumber,memberUid eq index entryCSN,entryUUID eq index objectClass,member,uniqueMember eq #       limits dn="uid=replicator,ou=services,dc=domain,dc=com" size=unlimited time=unlimited #        limits users size=unlimited time=unlimited #    access to attrs=userPassword by dn.base="uid=ldapadmin,ou=services,dc=domain,dc=com" write by dn.base="uid=replicator,ou=services,dc=domain,dc=com" write by dn.base="uid=proxyagent,ou=services,dc=domain,dc=com" read by anonymous auth by self write by * none access to attrs=mail by dn.base="uid=ldapadmin,ou=services,dc=domain,dc=com" write by dn.base="uid=replicator,ou=services,dc=domain,dc=com" write by * read access to attrs=cn by dn.base="uid=ldapadmin,ou=services,dc=domain,dc=com" write by dn.base="uid=replicator,ou=services,dc=domain,dc=com" write by * read access to * by dn.base="uid=ldapadmin,ou=services,dc=domain,dc=com" write by dn.base="uid=replicator,ou=services,dc=domain,dc=com" write by users read by anonymous auth 

To administer the LDAP database, I use phpldapadmin, since it is lightweight, convenient and has support for XML templates, which allows the flexibility to customize the necessary templates for creating accounts. Unfortunately, the project has long been abandoned by the author and no longer develops.

For example, an example of a template for creating an email account.
Code
  <objectClasses> <objectClass id="top"></objectClass> <objectClass id="inetOrgPerson"></objectClass> <objectClass id="posixAccount"></objectClass> #  posix  <objectClass id="VirtualMailAccount"></objectClass> #      phamm  <objectClass id="Vacation"></objectClass> #    phamm-vacation  </objectClasses> <attributes> <attribute id="givenName"> <display>First Name</display> <icon>ldap-uid.png</icon> <order>1</order> <page>1</page> </attribute> <attribute id="sn"> <display>Last Name</display> <onchange>=autoFill(cn;%givenName% %sn%)</onchange> <onchange>=autoFill(uid;%givenName|0-1/l%%sn/l%)</onchange> <onchange>=autoFill(loginShell;/sbin/nologin)</onchange> <onchange>=autoFill(FTPStatus;enabled)</onchange> <order>2</order> <page>1</page> </attribute> <attribute id="cn"> <display>Common Name</display> <order>3</order> </attribute> <attribute id="uid"> <display>UID</display> <onchange>=autoFill(homeDirectory;/home/%uid%)</onchange> <onchange>=autoFill(mailbox;/home/%uid%/Maildir)</onchange> <onchange>=autoFill(mail;%uid%@domain.com)</onchange> <onchange>=autoFill(company;My Company)</onchange> <order>4</order> <spacer>1</spacer> </attribute> 

When creating, I use the following LDAP database schema:

ou = people, dc = domain, dc = com - container for storing mail accounts;
ou = groups, dc = domain, dc = com - container for storing posix groups;
ou = services, dc = domain, dc = com - container for storing system service accounts (posix accounts);
ou = aliases, dc = domain, dc = com is a container for storing mail aliases.

When creating an email account, many attributes of the phamm scheme I modified are used, the use of which is beyond the scope of this article (for example, createMaildir or Backup). I will mark only those that are used in search filters.
accountActive = TRUE | FALSE - allows you to temporarily enable / disable an account or alias;
vacationInfo - contains the text Out of Office message;
vacationActive - allows you to enable / disable the OoO message;
quota - here and so it is clear (for example: quota = 4G)

So, OpenLDAP is configured and running, replication is working, and using phpldapadmin, the first test account ipupkin with the email address ipupkin@domain.com has been created. In the future, only one domain.com domain will be used, so we create a domain group in our ou = groups posix container and add ipupkin to this group.

In this example, all services work on the same Linux server and it is logical to use the Led database to identify the user in the system, so we place this procedure on the shoulders of the Name Service Switch (nss) or System Security Services Daemon (sssd). The advantage of this solution is also easy adaptation with the Samba domain, if necessary.
First you need to make sure that the nss_ldap package (or libnss-ldapd) is installed on the system.
In /etc/nsswitch.conf we change the lines from compat to ldap (or winbind in the case of a Samba domain)
passwd: files ldap
shadow: files ldap
group: files ldap

Create a file /etc/ldap.conf with the following content:

ldap.conf
uri ldap: //127.0.0.1
uri ldap: //192.168.0.1 # Second fallback ldap server

base dc = domain, dc = com
binddn uid = proxyagent, ou = services, dc = domain, dc = com # Pre-created in ou = services posix account specified in slapd.conf
bindpw *****

pam_filter objectclass = posixAccount
pam_login_attribute uid
pam_check_host_attr no
pam_lookup_policy no
pam_member_attribute memberUid
pam_min_uid 1000
pam_max_uid 65535
ssl start_tls # use TLS instead of SSL
# Specify certificate paths
tls_cacert /etc/openldap/ssl/ca.pem
tls_key /etc/openldap/ssl/mail_crt_new.pem
tls_cert /etc/openldap/ssl/mail_key_new.pem
tls_reqcert allow
tls_checkpeer no
tls_ciphers TLSv1
scope sub
timelimit 5
bind_timelimit 5
bind_policy soft
nss_reconnect_tries 4
nss_reconnect_sleeptime 1
nss_reconnect_maxsleeptime 16
nss_reconnect_maxconntries 2

We check that nss works and the home directory is created (mkdir -m 700 / home ipupkin && chown ipupkin: domain / home / ipupkin).
> id ipupkin
uid = 1057 (ipupkin) gid = 1000 (domain) groups = 1000 (domain)
> ls -ld / home / ipupkin
drwx ------ 3 ipupkin domain 4096 Jun 22 15:50 / home / ipupkin

Now that the ipupkin user is recognized by the system, ipupkin needs to be able to receive and send mail.

Dovecot setup


When configuring Dovecot, I deleted all the nested monstrous default configs and, for convenience, created only two - dovecot.conf and dovecot-ldap.conf.

dovecot.conf
auth_cache_negative_ttl = 10 mins
auth_debug = no
auth_debug_passwords = no
auth_mechanisms = plain login # Used by TLS, so we allow plain
base_dir = / var / run / dovecot /
default_vsz_limit = 1024 M
disable_plaintext_auth = no
dotlock_use_excl = yes
lda_mailbox_autocreate = yes # Required
lda_mailbox_autosubscribe = yes # Required
listen = *
mmap_disable = yes
mail_fsync = always
mail_nfs_storage = no
mail_nfs_index = no
mail_debug = no
mail_location = maildir: ~ / Maildir # Where to search for a mailbox, the maildir value is taken from the homeDirectory attribute (in our case, this is / home / ipupkin), the full path will be / home / ipupkin / Maildir.
mail_plugins = $ mail_plugins quota notify expire
managesieve_notify_capability = mailto
managesieve_sieve_capability = fileinto reject envelope encoder; ia; ascii-numeric relational regex imap4flags
ssl_ca = </etc/dovecot/ssl/ca.pem
ssl_cert = </etc/dovecot/ssl/mail_crt_new.pem
ssl_key = </etc/dovecot/ssl/mail_key_new.pem
ssl_verify_client_cert = no
verbose_ssl = no

protocols = imap pop3 sieve

userdb {
args = /etc/dovecot/dovecot-ldap.conf # Connecting our ldap config
driver = ldap
}

passdb {
args = /etc/dovecot/dovecot-ldap-pass.conf # As recommended by the authors of dovecot, a symbolic link to dovecot-ldap.conf
driver = ldap
}

service auth {
unix_listener auth-userdb {
mode = 0666
}
}

service imap-login {
process_min_avail = 6
service_count = 0
}

service pop3-login {
process_min_avail = 6
service_count = 0
}

# Used managesieve by roundcube webmail plugin
service managesieve-login {
process_min_avail = 6
service_count = 0
inet_listener sieve {
port = 4190
}
}

service managesieve {
}

service dict {
unix_listener dict {
mode = 0666
}
}

# When filling mailbox by 90%, the following script is executed
service quota-warning {
executable = script /etc/dovecot/quota-warning.sh # The script itself looks like this
Script
 #!/bin/sh PERCENT=$1 USER=$2 cat << EOF | /usr/libexec/dovecot/dovecot-lda -d $USER -o "plugin/quota=maildir:User quota:noenforcing" From: postmaster@domain.com Subject: Your mailbox is $PERCENT% full Content-Type: text/plain; charset="UTF-8" X-Priority: 2 Warning! Your mailbox is now $PERCENT% full. EOF 



unix_listener quota-warning {
mode = 0666
}
}

protocol imap {
imap_client_workarounds = delay-newmail # Hack for Autluk
mail_plugins = quota imap_quota mail_log notify
}

protocol pop {
pop3_client_workarounds = outlook-no-nuls oe-ns-eoh # Different hacks for Autluk
# pop3_uidl_format =% 08Xu% 08Xv # Used when Autluk stores mail on the server, this format has problems with Outlook 2013
pop3_uidl_format =% g
pop3_fast_size_lookups = yes # Details here is the section Maildir perfomance.
mail_plugins =
}

# Logical Delivery Agent (LDA) service used by Exim for mail delivery.
protocol lda {
hostname = domain.com
mail_fsync = optimized
mail_plugins = sieve quota
postmaster_address = postmaster@domain.com
log_path =
info_log_path =
}

protocol sieve {
}

# Various optional additives
plugin {
quota = maildir: User quota
quota_rule = *: storage = 1M
quota_rule2 = Trash: ignore
quota_rule3 = Deleted Items: ignore
quota_rule4 = Junk E-mail: ignore
quota_rule5 = Archive: ignore
quota_rule6 = archive: ignore
quota_warning = storage = 90 %% quota-warning 90% u # Specify the percentage of filling in the mailbox at which the quota-warning.sh script is triggered
sieve = ~ / Maildir / .dovecot.sieve
sieve_dir = ~ / Maildir / sieve
expire_dict = proxy :: expire
expire = Trash
expire2 = Deleted Items
expire3 = Junk Email
expire_cache = yes
}



dovecot-ldap.conf
hosts = 127.0.0.1
dn = uid = proxyagent, ou = services, dc = domain, dc = com
dnpass = *****
tls = no
auth_bind = no
auth_bind_userdn = uid =% u, ou = people, dc = domain, dc = com
ldap_version = 3
base = dc = domain, dc = com
deref = never
scope = subtree
user_attrs = homeDirectory = home, uidNumber = uid, gidNumber = gid, quota = quota_rule = *: storage =% $
user_filter = (& (objectClass = VirtualMailAccount) (accountActive = TRUE) (uid =% n) (mail = *))
pass_attrs = uid = user, userPassword = password
pass_filter = (& (objectClass = VirtualMailAccount) (accountActive = TRUE) (uid =% u) (mail = *))
default_pass_scheme = SSHA


We check that there are no errors (dovecot -a) and run dovecot.

Final: Exim Setup


The main goal in setting up Exim was the maximum rejection of any scripts and the implementation of all the functionality using only Exim tools. Only as an exception are Debian greylistd on python and pearl barley amavisd-new used.
When configuring Exim, two files will also be used - the main exim.conf and acl_smtp for the ACL rules.
Also optionally in the config there is a router and transport for mailman.

exim.conf
CONFIG_PREFIX = / etc / exim
ACL_PREFIX = CONFIG_PREFIX / acls # all ACL configs are stored here
DB_PREFIX = / var / spool / exim / db # to increase performance, it is desirable to use tmpfs

# Templates are the strength of Exim and make it easy to replace a long line with a short word.
# Define templates for mailman
MM_HOME = / var / lib / mailman
MM_UID = mailman
MM_GID = mailman
MM_WRAP = / usr / lib / mailman / mail / mailman
MM_LISTCHK = MM_HOME / lists / $ {lc :: $ local_part} /config.pck

ldap_default_servers = /var/run/openldap/slapd.sock: 192.168.0.1 # Specify how to connect to Ledap servers, the second server will be used as fallback.

INTERFACE = your_external_ip # Specify the external ip on which Exim will hang
BASEDN = dc = domain, dc = com # basedn ldap server

# This section lists the most important templates that define the logic of Exim

# Checking the alias, whether the alias address matches the value of the mail attribute in the aliases container, the alias must also belong to the VirtualMailAlias ​​class and be TRUE for the accountActive attribute
CHECK_1 = $ {lookup ldap {user = "uid = exim, ou = services, dc = domain, dc = com" pass = *** ldap: /// ou = aliases, dc = domain, dc = com? Mail? sub? (& (objectClass = VirtualMailAlias) (accountActive = TRUE) (mail = $ {quote_ldap: $ local_part @ $ domain}))}}

# Check account if the recipient's address matches the value of the mail attribute in the people container, the account must also belong to the VirtualMailAccount class and be TRUE for the accountActive attribute
CHECK_2 = $ {lookup ldap {user = "uid = exim, ou = services, dc = domain, dc = com" pass = *** ldap: /// ou = people, dc = domain, dc = com? Mail? sub? (& (objectClass = VirtualMailAccount) (accountActive = TRUE) (mail = $ {quote_ldap: $ local_part @ $ domain}))}}

# Check the suspended account, the account can not receive mail, but remains in the system (relevant for employees on the maternity leave)
CHECK_3 = $ {lookup ldap {user = "uid = exim, ou = services, dc = domain, dc = com" pass = *** ldap: /// ou = people, dc = domain, dc = com? Mail? sub? (& (objectClass = VirtualMailAccount) (accountActive = TRUE) (accountSuspend = TRUE) (mail = $ {quote_ldap: $ local_part @ $ domain}))}}

# List of participants of the alias, to whom to send mail, the value of the maildrop attribute
CHECK_DATA = $ {lookup ldapm {user = "uid = exim, ou = services, dc = domain, dc = com" pass = *** ldap: /// ou = aliases, dc = domain, dc = com? Maildrop? sub? (& (objectClass = VirtualMailAlias) (mail = $ {quote_ldap: $ local_part @ $ domain}))}}

# Path to mailbox, mailbox attribute value
CHECK_MAILDIR = $ {lookup ldap {user = "uid = exim, ou = services, dc = domain, dc = com" pass = *** ldap: /// ou = people, dc = domain, dc = com? Mailbox? sub? (& (objectClass = VirtualMailAccount) (accountActive = TRUE) (mail = $ {quote_ldap: $ local_part @ $ domain}))}}

# Text of OoO message, value of the vacationInfo attribute
CHECK_VACATION = $ {lookup ldap {user = "uid = exim, ou = services, dc = domain, dc = com" pass = *** ldap: /// ou = people, dc = domain, dc = com? VacationInfo? sub? (& (objectClass = VirtualMailAccount) (vacationActive = TRUE) (mail = $ {quote_ldap: $ local_part @ $ domain}))}}

#Hack for double commas in OoO message, exim's bug 660 - it seems they have not fixed it
VACATION = $ {sg {$ {lookup ldap {user = "uid = exim, ou = services, dc = domain, dc = com" pass = *** ldap: /// ou = people, dc = domain, dc = com? vacationInfo? sub? (& (objectClass = VirtualMailAccount) (vacationActive = TRUE) (mail = $ {quote_ldap: $ local_part @ $ domain}))}}} {,,} {,}}

domainlist_cache virt_domains = domain.com # Since we have only one domain, we specify it. If multiple domains are used, then you need to create a domain selection template from the ldap database.
domainlist_cache local_domains = localhost: mail.domain.com
hostlist relay_from_hosts = 127.0.0.1: 192.168.0.0/16
addresslist noautoreply_senders = DB_PREFIX / autoreply.noanswer.db

sender_unqualified_hosts = 127.0.0.1: 192.168.0.0/16
recipient_unqualified_hosts = 127.0.0.1: 192.168.0.0/16

local_interfaces = 0.0.0.0.25: 0.0.0.0.26: 0.0.0.0.465: 0.0.0.0.587: 127.0.0.1.10025
tls_on_connect_ports = 465

acl_smtp_connect = acl_check_connect
acl_smtp_helo = acl_check_helo
acl_smtp_mail = acl_check_mail
acl_smtp_rcpt = acl_check_rcpt
acl_smtp_data = acl_check_data
acl_smtp_dkim = acl_check_dkim

accept_8bitmime
auth_advertise_hosts =! 127.0.0.1 # Do not offer SMTP AUTH localhost
bounce_message_file = CONFIG_PREFIX / bounce.msg # Specify the format of the bounce message, I have this:

bounce msg
Subject: Mail delivery failed $ {if eq {$ sender_address} {$ bounce_recipient} {: return message to sender}}
****
This message was created automatically by mail delivery software.

A message $ {if eq {$ sender_address} {$ bounce_recipient} {that you sent} {sent by

<$ sender_address>

}} could not be delivered to all of its recipients.
The following address (es) failed:
****
The following text was generated during the delivery attempt (s):
****
- This is a copy of the message, including all the headers. - ****
- The body of the message is $ message_size characters long; only the first
- $ return_size_limit or so are included here.
****

bounce_return_size_limit = 100K
delay_warning = 15m: 1h: 99d
deliver_queue_load_max = 40
disable_ipv6
exim_group = vmail # It is desirable that all mail services (dovecot, spamassassin, clamav, etc.) work under one gid
exim_user = vmail # It is desirable that all mail services (dovecot, spamassassin, clamav, etc.) work under one uid
headers_charset = UTF-8 # It is advisable to enable
ignore_bounce_errors_after = 0s
local_scan_timeout = 0s
message_size_limit = 50M
never_users = root
no_message_logs
no_smtp_enforce_sync
no_syslog_duplication
primary_hostname = mail.domain.com
qualify_domain = domain.com
queue_only_load = 12
queue_run_max = 5
recipients_max = 500
recipients_max_reject
remote_max_parallel = 2
return_size_limit = 10000
rfc1413_query_timeout = 0s
smtp_accept_max = 500
smtp_accept_max_per_host = 500
smtp_accept_queue = 500
smtp_accept_queue_per_connection = 1000
smtp_accept_reserve = 15
smtp_banner = $ primary_hostname ESMTP ready $ tod_full
smtp_connect_backlog = 40
smtp_load_reserve = 20
smtp_return_error_details
split_spool_directory
strip_excess_angle_brackets
strip_trailing_dot
syslog_facility = mail # Logs are sent to the syslog service
syslog_processname = exim
system_filter = DB_PREFIX / exim.filter # Global exim filter, mostly not used for me
timeout_frozen_after = 7d
tls_advertise_hosts =! 127.0.0.1 # Do not offer TLS to localhost

# Specify certificate paths
tls_certificate = /etc/exim/ssl/mail_crt_new.pem
tls_privatekey = /etc/exim/ssl/mail_key_new.pem
tls_verify_certificates = /etc/exim/ssl/ca.pem

# Determine the format of the letter header
received_header_text = "Received: \
$ {if def: sender_rcvhost {from INTERFACE \ n \ t} \
{$ {if def: sender_ident {from relay}} \
$ {if def: sender_helo_name {(helo = $ {sender_helo_name}) \ n \ t}}}} \
by $ {qualify_domain} \
id $ {message_id} \
$ {if def: received_for {\ n \ tfor <$ received_for>}}

# We connect antivirus and antispam directly, since LDAP is not used here, then everything is standard, so we omit the setting.
# av_scanner = clamd: / tmp / clamd
# spamd_address = 127.0.0.1 783

begin acl

# Connect our ACL config (see below)
.include ACL_PREFIX / acl_smtp

# Create routers
begin routers

# Router for outgoing mail
dnslookup:
driver = dnslookup
domains =! + local_domains:! + virt_domains
transport = remote_smtp
ignore_target_hosts = 0.0.0.0: 127.0.0.0/8
no_more

# Routers for incoming mail

# Opitsonalny transport for amavis (we will make one exception a pearl :))
#amavis:
# driver = manualroute
# condition = $ {if or {\
# {eq {$ interface_port} {10025}} \
# {eq {$ received_protocol} {spam-scanned}} \
# {eq {$ sender_address} {}} \
# {eq {$ sender_address_domain} {domain.com}} \
# {eq {$ sender_address_domain} {kaspersky.com}} \
# {eq {$ {lc: $ dkim_verify_status}} {pass}} \
# {match {$ sender_address_local_part} {- bounces}} \
#} {0} {1}}
# domains = + virt_domains
# senders = !:! postmaster @ *:! mailer-daemon @ *:! nagios @ *:! monit @ *
# no_verify
# no_expn
# transport = amavis
# route_list = "* localhost byname"
# self = send

autorespond:
driver = accept
domains = + virt_domains
senders = !:! + noautoreply_senders # Do not respond to the local host and senders specified in autoreply.noanswer.db
condition = $ {if and {\
{! eq {CHECK_VACATION} {}} \ # Our template
{! match {$ h_precedence:} {junk | bulk | list}} \ # Do not respond to mailings
{! def: header_Auto-Submitted:} \
{! def: header_List-Id:} \
}}
no_verify
no_expn
unseen
transport = auto_responder

# Suspended accounts, all incoming mail for a suspended account is sent to the "black hole"
suspended:
driver = redirect
domains = + virt_domains
condition = CHECK_3
forbid_file
forbid_pipe
forbid_filter_reply = true
data =: blackhole:
no_more

# Virtual alias
aliases:
driver = redirect
domains =! + local_domains
condition = CHECK_1 # There is a double check here (the first in acl_smtp), if the current account has an alias, I have not thought of anything better
forbid_file
forbid_pipe
forbid_filter_reply = true
data = CHECK_DATA # To send letters
allow_fail
allow_defer

mailman_router:
driver = accept
domains = domain.com
require_files = MM_LISTCHK # Instead of checking the file, you can check by the value of the attribute aliasType = DL, for example
local_part_suffix_optional
local_part_suffix = -admin: \
-bounces: -bounces+*: \
-confirm: -confirm+*: \
-join: -leave: \
-owner: -request: \
-subscribe: -unsubscribe
transport = mailman_transport

system_aliases:
driver = redirect
domains = +local_domains
errors_to =
no_verify
data = ${lookup{$local_part}partial0-dbm{DB_PREFIX/aliases.db}{$value}fail}
file_transport = address_file
pipe_transport = address_pipe
allow_fail
allow_defer

localuser:
driver = accept
domains = +local_domains: +virt_domains
check_local_user
transport = dovecot_lda # dovecot lda
cannot_route_message = Unknown account # Dovecot ,
no_more

###############################################################
begin transports
###############################################################

remote_smtp:
driver = smtp
helo_data = mail.domain.com
max_rcpt = 500
# DKIM
dkim_domain = domain.com
dkim_selector = dkim
dkim_private_key = DB_PREFIX/dkim.private.key
dkim_canon = relaxed

auto_responder:
driver = autoreply
from = "${local_part}@${domain}"
to = "${reply_address}"
once = "/var/spool/exim/autoreply/${local_part}@${domain}"
once_repeat = 1d # , LDAP (. phamm-vacation.schema)
headers = «Content-Type: text/plain; charset=utf-8\nContent-Transfer-Encoding: 8bit»
subject = ${rfc2047:Auto-Reply: $h_subject:}
text = VACATION # OoO
body_only
no_return_message

# dovecot
dovecot_lda:
driver = pipe
command = /usr/libexec/dovecot/dovecot-lda -f "$sender_address" -d "$local_part@$domain"
home_directory = /home/$local_part
delivery_date_add
envelope_to_add
return_path_add
log_output
log_defer_output
return_fail_output
freeze_exec_fail
temp_errors = 64: 69: 70: 71: 72: 73: 74: 75: 78

address_pipe:
driver = pipe
return_output

address_file:
driver = appendfile
current_directory = SPOOL
home_directory = SPOOL
create_directory
directory_mode = 0700
maildir_format
user = vmail
group = vmail
mode = 0600
no_check_owner
no_mode_fail_narrower

address_reply:
driver = autoreply

maillist_pipe:
driver = pipe
group = mail
return_fail_output
user = vmail

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

#amavis:
# driver = smtp
# port = 10024
# allow_localhost

begin retry
* quota
* rcpt_4xx senders=: F,1h,10m
* * F,2h,10m; G,16h,1h,1.5; F,4d,6h

#
begin rewrite
root@* collector@domain.com Ttbcr

# SMTP AUTH
begin authenticators
plain:
driver = plaintext
public_name = PLAIN
server_prompts =:
server_condition = "${lookup ldap{user=uid=${quote_ldap_dn:$auth2},ou=people,BASEDN pass=${quote:$auth3} \
ldap:///ou=people,BASEDN?uid?sub?(&(uid=$auth2)(objectClass=VirtualMailAccount)(accountActive=TRUE))}{yes}fail}"
server_set_id = $auth2

login:
driver = plaintext
public_name = LOGIN
server_prompts = «Username::: Password::»
server_condition = "${lookup ldap{user=uid=${quote_ldap_dn:$auth1},ou=people,BASEDN pass=${quote:$auth2} \
ldap:///ou=people,BASEDN?uid?sub?(&(uid=$auth1)(objectClass=VirtualMailAccount)(accountActive=TRUE))}{yes}fail}"
server_set_id = $auth1


Exim's main caliber cannon is the Access Control Lists.
Actually, the whole article was started for the sake of a single construction in the smtp_rcpt section.

acl_smtp
acl_check_connect:
accept hosts =: +relay_from_hosts: net-dbm;DB_PREFIX/whitelist_hosts.db
deny message = $sender_host_address is listed in $dnslist_domain ${if def:dnslist_text {($dnslist_text)}}
dnslists = sbl.spamhaus.org: xbl.spamhaus.org: bl.spamcop.net
accept

acl_check_dkim:
warn log_message = DKIM: Sender without DKIM signature
sender_domains = gmail.com: autodesk.com: paypal.com
dkim_signers = gmail.com: autodesk.com: paypal.com
dkim_status = none:invalid:fail
accept

acl_check_helo:
accept hosts =: +relay_from_hosts

#HELO is an open proxy
deny condition = ${if and {\
{isip{$sender_helo_name}}\
{eq{$sender_helo_name}{$sender_host_address}}\
}}
message = Open Proxy in HELO/EHLO (HELO was $sender_helo_name)
delay = 10s

#HELO is my hostname
deny condition = ${if match{$sender_helo_name}{$primary_hostname}}
message = Bad HELO — Host impersonating [$sender_helo_name]

#HELO is my address
deny condition = ${if eq{$interface_address}{$sender_helo_name}}
message = $interface_address is my address
accept

acl_check_mail:

accept hosts =: +relay_from_hosts
discard senders = dbm;DB_PREFIX/banned_senders.db: dbm;DB_PREFIX/scammers.db

#HELO required before MAIL
deny condition = ${if eq{$sender_helo_name}{}}
message = HELO/EHLO required before MAIL

accept

acl_check_rcpt:

#stub address
discard condition = ${if match{$local_part@$domain}{blackhole@domain.com}} # blackhole

deny message = Restricted characters in address
local_parts = ^[.]: ^.*[@%!/|]

#Reverse DNS check
warn condition = ${if and{{def:sender_host_address}{!def:sender_host_name}}{yes}{no}}
!hosts =: +relay_from_hosts: net-dbm;DB_PREFIX/whitelist_hosts.db
control = no_pipelining
delay = 10s # 10 ,
log_message = X-Host-Lookup-Failed: Reverse DNS lookup failed for $sender_host_address

# RATELIMIT SECTION

#Keep authenticated users under control
warn authenticated = *
ratelimit = 100 / 5m / strict / $authenticated_id
set acl_m100 = ${eval: ${sg{$sender_rate}{[.].*}{}} — $sender_rate_limit + 10}s
delay = $acl_m100
log_message = Ratelimit: Delay $acl_m100 for $authenticated_id. Rate limit $sender_rate / $sender_rate_period

#Limit local senders, exclude mailing-list agent
warn condition = ${if !match{$sender_address_local_part}{bounces}}
hosts =: 127.0.0.1
ratelimit = 1000 / 1h / per_rcpt / strict / $sender_host_address
set acl_m101 = ${eval: ${sg{$sender_rate}{[.].*}{}} — $sender_rate_limit}s
delay = $acl_m101
log_message = Ratelimit: Delay $acl_m101 for $sender_address ($sender_host_address). Rate $sender_rate / limit $sender_rate_limit

#Limit fast senders
hosts = !127.0.0.1: +relay_from_hosts
ratelimit = 100 / 5m / per_rcpt / strict
set acl_m102 = ${eval: ${sg{$sender_rate}{[.].*}{}} — $sender_rate_limit + 5}s
delay = $acl_m102
log_message = Ratelimit: Delay $acl_m102 for $sender_address ($sender_host_address). Rate $sender_rate / limit $sender_rate_limit

#Limit DSNs
warn condition = ${if and{\
{<{$recipients_count}{0}}\
{!eq{$sender_address_domain}{domain.com}}\
}}
senders =: postmaster@*: mailer-daemon@*
delay = 10s
log_message = Ratelimit: DSN delay 10s for $sender_address ($sender_host_address)

# END RATELIMIT SECTION

#Predefined acl variables for smtp_data level
warn set acl_m0 = $sender_address_domain
warn set acl_m1 = $domain
warn set acl_m2 = $sender_host_address
warn set acl_m3 = $sender_address
warn set acl_m4 = $local_part@$domain

#Verify recipient for our domains.
deny message = Unknown or disabled account
domains = +virt_domains
!local_parts = postmaster: *-admin: *-bounces: *-bounces+*: *-confirm: *-confirm+* :\
*-join: *-leave: *-owner: *-request: *-subscribe: *-unsubscribe

# , check_rcpt, .
!recipients = CHECK_1: CHECK_2

accept hosts =: +relay_from_hosts
control = dkim_disable_verify
#
accept authenticated = *
control = dkim_disable_verify

# , , 10 .
deny message = relay not permitted
!domains = +local_domains: +virt_domains
delay = 10s

# , ,
#Deny non-authorized senders with our own domain prefix
deny condition = ${if match{$sender_address_domain}{domain.com}}
!hosts =: +relay_from_hosts: +adobe_hosts: +microsoft_hosts: net-dbm;DB_PREFIX/whitelist_hosts.db
message = Sender domain is not allowed here
log_message = Sender $sender_address is not authenticated

#Dictionary attack protection
#Start
warn condition = ${if > {${eval:$rcpt_fail_count}}{4}{yes}{no}}
log_message = Ratelimit: Detected Dictionary Attack (Let $rcpt_fail_count bad recipients though before engaging)
set acl_m7 = 1

warn condition = ${if eq {${acl_m7}}{1}{1}{0}}
ratelimit = 0 / 1h / strict / per_conn
log_message = Ratelimit: Increment Connection Ratelimit — $sender_fullhost because of Dictionary Attack

drop condition = ${if eq {${acl_m7}}{1}{1}{0}}
log_message = Ratelimit: Number of failed recipients exceeded
#End

# , exim lastchange "%Y%m%d" . , .
, log_message, , .
#Alias statistic
warn
domains = +virt_domains
log_message = ALIAS: $local_part@$domain
recipients = CHECK_1

#
#Greylist section
defer message = $sender_host_address is not yet authorized to deliver \
mail from <$sender_address> to <$local_part@$domain>. Please try later
log_message = Sender $sender_address greylisted
domains = +virt_domains
!sender_domains = partial1()dbm;DB_PREFIX/whitelist_grey_domains.db
!authenticated = *
condition = ${readsocket{/var/run/greylistd/socket}\
{--grey %s $sender_address $local_part@$domain}{5s}{}{false}}

accept

acl_check_vrfy_expn_etrn:

accept hosts = 127.0.0.1

deny

acl_check_data:

# spamassassin, . SPAM Exim.

exim.filter :

exim.filter
if first_delivery then
headers remove X-Spam-Score:X-Spam-Report:X-Spam-Checker-Version:X-Spam-Status:X-Spam-Level

if "${if def:header_X-New-Subject: {there}}" is there
then
headers remove Subject
headers add «Subject: $rh_X-New-Subject:»
headers remove X-New-Subject
endif

endif


#
# no antispam check for relay hosts and authenticated users
# accept hosts =: +relay_from_hosts
# accept authenticated = *
#
# Antispam scan
# warn
# condition = ${if and {\
# {<{$message_size}{50k}}\
## {!eq{${mask:$acl_m2/16}}{192.168.0.0/16}}\
# {!eq{$sender_address}{}}\
## {!match_address{$sender_address}{dbm;DB_PREFIX/whitelist_spam_senders.db}}\
# {!match_domain{$acl_m0}{partial1()dbm;DB_PREFIX/whitelist_grey_domains.db}}\
## {match_domain{$acl_m1}{dbm;DB_PREFIX/domains_spam.db}}\
# }}
# spam = nobody:true/defer_ok
# set acl_m6 = $spam_score_int

# add new subj for global exim filter
# message = X-New-Subject: SPAM[$spam_score_int/80]: $rh_subject:
# condition = ${if and {\
# {def:spam_score_int}\
# {>{$spam_score_int}{80}}\
# }}

accept


That's all.

We run Exim, send an email to ipupkin, look at the logs ...

Source: https://habr.com/ru/post/262101/


All Articles