Advanced Exim and Dovecot configuration linked to OpenLDAP
This article discusses advanced settings for Exim, Dovecot, and OpenLDAP to work together based on my experience with these applications. Perhaps someone will find something interesting and new for himself - this was the purpose of writing the next howto on this topic.
Why Exim and OpenLDAP, not Postfix and MySQL, for example? Postfix works fine “out of the box”, but if you need something extraordinary, very soon Postfix turns into a clumsy monster, hung with pearl scripts, Exim has a monstrous power meta-language configuration and allows you to do without third-party scripts and crutches. I considered MySQL redundant for my tasks and replaced it with standard OpenLDAP, all the more LDAP is used for the address book. Dovecot is very fast and easy to configure, plus it integrates perfectly with both Exim and OpenLDAP.
So, we 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).
USE flags used during assembly:
The first in line will be OpenLDAP, all mail accounts, groups and alliances will be stored in its database, and OpenLDAP will also be used as an address book for email clients. For simplicity and convenience, I do not use slapd-config, but store all the settings in the text slapd.conf.
Since the standard OpenLDAP does not have a suitable scheme for working with mail, I used my modified version of phamm.schema and the default phamm-vacation.schema .
I omit the initial configuration, creating basedn dc = domain, dc = com and ssl certificates for OpenLDAP, since everything is standard here.
Be sure to configure replication for OpenLDAP (again, everything is standard here, so I omit the details). The only thing I had to build 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, for sssvlv to work correctly in Outlook, I had to patch OpenLDAP sources.
The second server will act as a read-only address book.
I use phpldapadmin to administer the LDAP database, as it is lightweight, convenient and has support for XML templates, which allows you to flexibly configure the necessary templates for creating accounts. Unfortunately, the project has long been abandoned by the author and is no longer developing.
For example, an example template for creating an email account.
When creating, I use the following LDAP database scheme:
ou = people, dc = domain, dc = com - a container for storing mail accounts;
ou = groups, dc = domain, dc = com - a container for storing posix groups;
ou = services, dc = domain, dc = com - a container for storing system service accounts (posix accounts);
ou = aliases, dc = domain, dc = com - a container for storing mail alliances.
When creating an email account, many attributes of the phamm scheme modified by me are used, the use of which is beyond the scope of this article (for example, createMaildir or Backup). I’ll only mention 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 messages;
vacationActive - allows you to enable / disable OoO message;
quota - here it’s clear (for example: quota = 4G)
So, OpenLDAP is configured and started, replication works and using phpldapadmin the first ipupkin test account was created with the ipupkin@domain.com mail address. In the future, only one domain.com domain will be used, therefore, create the domain group in our ou = groups posix container and add ipupkin to this group.
In this example, all services run on the same Linux server and it is logical to use the ldap database to identify the user in the system, so we shift this procedure to the shoulders of Name Service Switch (nss) or System Security Services Daemon (sssd). Also, the advantage of this solution is the easy adaptation with the Samba domain, if necessary.
First you need to make sure that the package is installed nss_ldap (or libnss-ldapd).
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 the file /etc/ldap.conf with the following contents:
We check that nss is working and the home directory has been 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 when the ipupkin user is recognized by the system, ipupkin needs to be able to receive and send mail.
When configuring Dovecot, I deleted all the nested monstrous default configs and for convenience I created only two - dovecot.conf and dovecot-ldap.conf.
Check that there are no errors (dovecot -a) and run dovecot.
The main goal when configuring Exim was to maximize the rejection of any scripts and implement all the functionality only using Exim. Only as an exception are Debian greylistd in python and pearl barley amavisd-new used.
When configuring Exim, two files will also be used - the main exim.conf and acl_smtp for ACL rules.
Also optionally in the config there is a router and transport for mailman.
Exim's main caliber weapon is Access Control Lists.
Actually, the whole article was conceived for the sake of a single construction in the smtp_rcpt section.
That, in fact, is all.
We start Exim, we send the letter to ipupkin, we look at logs ...
Why Exim and OpenLDAP, not Postfix and MySQL, for example? Postfix works fine “out of the box”, but if you need something extraordinary, very soon Postfix turns into a clumsy monster, hung with pearl scripts, Exim has a monstrous power meta-language configuration and allows you to do without third-party scripts and crutches. I considered MySQL redundant for my tasks and replaced it with standard OpenLDAP, all the more LDAP is used for the address book. Dovecot is very fast and easy to configure, plus it integrates perfectly with both Exim and OpenLDAP.
So, we 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).
USE flags used during assembly:
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 line will be OpenLDAP, all mail accounts, groups and alliances will be stored in its database, and OpenLDAP will also be used as an address book for email clients. For simplicity and convenience, I do not use slapd-config, but store all the settings in the text slapd.conf.
Since the standard OpenLDAP does not have a suitable scheme for working with mail, I used my modified version of phamm.schema and the default phamm-vacation.schema .
Configure OpenLDAP
I omit the initial configuration, creating basedn dc = domain, dc = com and ssl certificates for OpenLDAP, since everything is standard here.
Config slapd.conf
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 configure replication for OpenLDAP (again, everything is standard here, so I omit the details). The only thing I had to build 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, for sssvlv to work correctly in Outlook, I had to patch 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 act as a read-only address book.
Config slapd.conf for the 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
I use phpldapadmin to administer the LDAP database, as it is lightweight, convenient and has support for XML templates, which allows you to flexibly configure the necessary templates for creating accounts. Unfortunately, the project has long been abandoned by the author and is no longer developing.
For example, an example template for creating an email account.
The code
# Базовый posix аккаунт
# Указываем класс из нашей почтовой phamm схемы
# Указываем класс из phamm-vacation схемы
First Name ldap-uid.png 1 1 Last Name =autoFill(cn;%givenName% %sn%) =autoFill(uid;%givenName|0-1/l%%sn/l%) =autoFill(loginShell;/sbin/nologin) =autoFill(FTPStatus;enabled) 2 1 Common Name 3 UID =autoFill(homeDirectory;/home/%uid%) =autoFill(mailbox;/home/%uid%/Maildir) =autoFill(mail;%uid%@domain.com) =autoFill(company;My Company) 4 1
When creating, I use the following LDAP database scheme:
ou = people, dc = domain, dc = com - a container for storing mail accounts;
ou = groups, dc = domain, dc = com - a container for storing posix groups;
ou = services, dc = domain, dc = com - a container for storing system service accounts (posix accounts);
ou = aliases, dc = domain, dc = com - a container for storing mail alliances.
When creating an email account, many attributes of the phamm scheme modified by me are used, the use of which is beyond the scope of this article (for example, createMaildir or Backup). I’ll only mention 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 messages;
vacationActive - allows you to enable / disable OoO message;
quota - here it’s clear (for example: quota = 4G)
So, OpenLDAP is configured and started, replication works and using phpldapadmin the first ipupkin test account was created with the ipupkin@domain.com mail address. In the future, only one domain.com domain will be used, therefore, create the domain group in our ou = groups posix container and add ipupkin to this group.
In this example, all services run on the same Linux server and it is logical to use the ldap database to identify the user in the system, so we shift this procedure to the shoulders of Name Service Switch (nss) or System Security Services Daemon (sssd). Also, the advantage of this solution is the easy adaptation with the Samba domain, if necessary.
First you need to make sure that the package is installed nss_ldap (or libnss-ldapd).
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 the file /etc/ldap.conf with the following contents:
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 the slapd.conf
bindpw *****
pam_filter objectclass = posixAccount
pam_login_attribute the uid
pam_check_host_attr the no
pam_lookup_policy the no
pam_member_attribute memberUid
pam_min_uid 1000
pam_max_uid 65535
ssl start_tls # use TLS instead of the SSL
# Specify the path to the certificate
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
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 the slapd.conf
bindpw *****
pam_filter objectclass = posixAccount
pam_login_attribute the uid
pam_check_host_attr the no
pam_lookup_policy the no
pam_member_attribute memberUid
pam_min_uid 1000
pam_max_uid 65535
ssl start_tls # use TLS instead of the SSL
# Specify the path to the certificate
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 is working and the home directory has been 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 when the ipupkin user is recognized by the system, ipupkin needs to be able to receive and send mail.
Configure Dovecot
When configuring Dovecot, I deleted all the nested monstrous default configs and for convenience I created only two - dovecot.conf and dovecot-ldap.conf.
dovecot.conf
auth_cache_negative_ttl = 10 mins
auth_debug = the no
auth_debug_passwords = the no
auth_mechanisms = plain the login # Use TLS, so allow plain
'base_dir' = / var / the run / dovecot /
default_vsz_limit = 1024 M
disable_plaintext_auth = the no
dotlock_use_excl = yes or The
lda_mailbox_autocreate = yes or The # Required
lda_mailbox_autosubscribe = yes # Always
listen = *
mmap_disable = yes
mail_fsync = always
mail_nfs_storage = no
mail_nfs_index = no
mail_debug = no
mail_location = maildir: ~ / Maildir # Where to look for a mailbox, the maildir value is taken from the homeDirectory attribute (in our case it is / home / ipupkin), accordingly 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 encoded-character vacation subaddress comparator-i; ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date
ssl_ca = ssl_cert = ssl_key = ssl_verify_client_cert = no
verbose_ssl = no
protocols = imap pop3 sieve
userdb {
args = /etc/dovecot/dovecot-ldap.conf # Connect our ldap config
driver = ldap
}
passdb {
args = /etc/dovecot/dovecot-ldap-pass.conf # According to the recommendations of the authors 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 by managesieve webmail plugin roundcube
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 the mailbox is 90% full, the following
service quota-warning
script is executed { executable = script /etc/dovecot/quota-warning.sh # The script itself looks like this
unix_listener quota-warning {
mode = 0666
}
}
protocol imap {
imap_client_workarounds = delay-newmail # Haq for Autluk
mail_plugins = quota imap_quota mail_log notify
}
protocol pop {
pop3_client_workarounds = outlook-no-nuls oe-ns-eoh # Various hacks for Autluk
# pop3_uidl_format =% 08Xu% 08Xv # used when storing Autluk mail on the server, this format has a problem with Autluk 2013
pop3_uidl_format =% g
pop3_fast_size_lookups = # yes or the More here section Maildir perfomance.
mail_plugins =
}
# Logical Delivery Agent (LDA) service used by Exim to deliver mail.
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 add-
ons 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 mailbox filling that triggers the script quota-warning.sh
sieve = ~ / Maildir / .dovecot.sieve
sieve_dir = ~ / Maildir / sieve
expire_dict = proxy :: expire
expire = Trash
expire2 = Deleted Items
expire3 = Junk E-mail
expire_cache = yes
}
auth_debug = the no
auth_debug_passwords = the no
auth_mechanisms = plain the login # Use TLS, so allow plain
'base_dir' = / var / the run / dovecot /
default_vsz_limit = 1024 M
disable_plaintext_auth = the no
dotlock_use_excl = yes or The
lda_mailbox_autocreate = yes or The # Required
lda_mailbox_autosubscribe = yes # Always
listen = *
mmap_disable = yes
mail_fsync = always
mail_nfs_storage = no
mail_nfs_index = no
mail_debug = no
mail_location = maildir: ~ / Maildir # Where to look for a mailbox, the maildir value is taken from the homeDirectory attribute (in our case it is / home / ipupkin), accordingly 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 encoded-character vacation subaddress comparator-i; ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date
ssl_ca = ssl_cert = ssl_key = ssl_verify_client_cert = no
verbose_ssl = no
protocols = imap pop3 sieve
userdb {
args = /etc/dovecot/dovecot-ldap.conf # Connect our ldap config
driver = ldap
}
passdb {
args = /etc/dovecot/dovecot-ldap-pass.conf # According to the recommendations of the authors 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 by managesieve webmail plugin roundcube
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 the mailbox is 90% full, the following
service quota-warning
script is executed { 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 # Haq for Autluk
mail_plugins = quota imap_quota mail_log notify
}
protocol pop {
pop3_client_workarounds = outlook-no-nuls oe-ns-eoh # Various hacks for Autluk
# pop3_uidl_format =% 08Xu% 08Xv # used when storing Autluk mail on the server, this format has a problem with Autluk 2013
pop3_uidl_format =% g
pop3_fast_size_lookups = # yes or the More here section Maildir perfomance.
mail_plugins =
}
# Logical Delivery Agent (LDA) service used by Exim to deliver mail.
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 add-
ons 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 mailbox filling that triggers the script quota-warning.sh
sieve = ~ / Maildir / .dovecot.sieve
sieve_dir = ~ / Maildir / sieve
expire_dict = proxy :: expire
expire = Trash
expire2 = Deleted Items
expire3 = Junk E-mail
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
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
Check that there are no errors (dovecot -a) and run dovecot.
Final: Exim Setup
The main goal when configuring Exim was to maximize the rejection of any scripts and implement all the functionality only using Exim. Only as an exception are Debian greylistd in python and pearl barley amavisd-new used.
When configuring Exim, two files will also be used - the main exim.conf and acl_smtp for 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
DB_PREFIX = / var / spool / exim / db # are stored here; it is advisable to use tmpfs to increase performance
# Templates are Exim's strength 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 # We specify how to connect to the ldap 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 server lap
# This section shows the most important templates that specify Exim logic
# Check the alias if the alias address matches the mail attribute value in the aliases container, the alias must also belong to the VirtualMailAlias class and have a value 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}))}}
# Account check if the recipient address matches the value of the mail attribute in the people container, account must also belong to class Virtual MailAccount and set to 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 for a suspended account, the account cannot receive mail, but remains in the system (relevant for employees on 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 members of the alliance to whom to forward mail, value 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 the mailbox, value of the mailbox attribute
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 the OoO message, attribute value vacationInfo
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
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 indicate it. If you use many domains, then you need to create a template for selecting domains from the 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 lokalhostu
bounce_message_file = CONFIG_PREFIX / bounce.msg # specifies the format of bounce messages that I have this:
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 to do allass clamav, etc.) working under one the uid
the headers_charset = the UTF-8 # It is advisable to include
ignore_bounce_errors_after = 0s
local_scan_timeout for = 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 syslog service
syslog_processname = exim
system_filter = DB_PREFIX / exim.filter # Global exim filter, I mostly do not use
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_pc_pc = /etc/exim/ssl/mail_key_new.pem
tls_verify_certificates = /etc/exim/ssl/ca.pem
# Define the format of the message 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 the 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
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
# Incoming routers
# An optional transport for amavis (we will make one exception to the 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 localhost and senders specified in autoreply.noanswer.db
condition = $ {if and {\
{! eq {CHECK_VACATION} {}} \ # Our template
{! match {$ h_precedence:} {junk | bulk | list}} \ # Do not reply to newsletters
{! 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
aliases aliases:
driver = redirect
domains =! + local_domains
condition = CHECK_1 # There is a double check (the first in acl_smtp), if the current account also has an alias, I didn’t come up with anything better
forbid_file
forbid_pipe
forbid_filter_reply = true
data = CHECK_DATA # To whom to forward letters
allow_fail
allow_defer
mailman_router:
driver = accept
domains = domain.com
require_files = MM_LISTCHK # Instead of checking against a file, you can check the value of the aliasType = DL attribute, 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 # We shift the delivery of the letter to the mailbox on the shoulders dovecot
lda cannot_route_message = Unknown account # Dovecot answered no, give up
no_more
####################### ######################################################
begin transports
###################################################### ##############
remote_smtp:
driver = smtp
helo_data = mail.domain.com
max_rcpt = 500
# connect DKIM
dkim_domain = domain.com
dkim_selector = dkim
dkim_private_key = DB_PREFIX / dkim.private.key
dkim_can = relaxed
auto_responder:
driver = autoreply
from = "$ {local_part} @ $ {domain}"
to = "$ {reply_address}"
once = "/ var / spool / exim / autoreply / $ {local_part} @ $ {domain}"
once_repeat = 1d # Answer the sender once a day, although the logic can also be transferred to LDAP (see phamm-vacation.schema)
headers = “Content-Type: text / plain; charset = utf-8 \ nContent-Transfer-Encoding: 8bit »
subject = $ {rfc2047: Auto-Reply: $ h_subject:}
text = VACATION # Our template with OoO text message
body_only
no_return_message
# The same transport for
dovecot dovecot_lda:
driver = pipe
command = / usr / libexec / dovecot / dovecot-lda -f "$ sender_address" -d "
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
# All system root messages are sent to one box
begin rewrite
root @ * collector@domain.com Ttbcr
# SMTP
authenticators 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
ACL_PREFIX = CONFIG_PREFIX / acls # all ACL configs
DB_PREFIX = / var / spool / exim / db # are stored here; it is advisable to use tmpfs to increase performance
# Templates are Exim's strength 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 # We specify how to connect to the ldap 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 server lap
# This section shows the most important templates that specify Exim logic
# Check the alias if the alias address matches the mail attribute value in the aliases container, the alias must also belong to the VirtualMailAlias class and have a value 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}))}}
# Account check if the recipient address matches the value of the mail attribute in the people container, account must also belong to class Virtual MailAccount and set to 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 for a suspended account, the account cannot receive mail, but remains in the system (relevant for employees on 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 members of the alliance to whom to forward mail, value 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 the mailbox, value of the mailbox attribute
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 the OoO message, attribute value vacationInfo
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
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 indicate it. If you use many domains, then you need to create a template for selecting domains from the 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 lokalhostu
bounce_message_file = CONFIG_PREFIX / bounce.msg # specifies the format of bounce messages that I have this:
bounce msg
Subject: Mail delivery failed $ {if eq {$ sender_address} {$ bounce_recipient} {: returning 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.
****
****
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 to do allass clamav, etc.) working under one the uid
the headers_charset = the UTF-8 # It is advisable to include
ignore_bounce_errors_after = 0s
local_scan_timeout for = 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 syslog service
syslog_processname = exim
system_filter = DB_PREFIX / exim.filter # Global exim filter, I mostly do not use
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_pc_pc = /etc/exim/ssl/mail_key_new.pem
tls_verify_certificates = /etc/exim/ssl/ca.pem
# Define the format of the message 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 the 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
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
# Incoming routers
# An optional transport for amavis (we will make one exception to the 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 localhost and senders specified in autoreply.noanswer.db
condition = $ {if and {\
{! eq {CHECK_VACATION} {}} \ # Our template
{! match {$ h_precedence:} {junk | bulk | list}} \ # Do not reply to newsletters
{! 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
aliases aliases:
driver = redirect
domains =! + local_domains
condition = CHECK_1 # There is a double check (the first in acl_smtp), if the current account also has an alias, I didn’t come up with anything better
forbid_file
forbid_pipe
forbid_filter_reply = true
data = CHECK_DATA # To whom to forward letters
allow_fail
allow_defer
mailman_router:
driver = accept
domains = domain.com
require_files = MM_LISTCHK # Instead of checking against a file, you can check the value of the aliasType = DL attribute, 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 # We shift the delivery of the letter to the mailbox on the shoulders dovecot
lda cannot_route_message = Unknown account # Dovecot answered no, give up
no_more
####################### ######################################################
begin transports
###################################################### ##############
remote_smtp:
driver = smtp
helo_data = mail.domain.com
max_rcpt = 500
# connect DKIM
dkim_domain = domain.com
dkim_selector = dkim
dkim_private_key = DB_PREFIX / dkim.private.key
dkim_can = relaxed
auto_responder:
driver = autoreply
from = "$ {local_part} @ $ {domain}"
to = "$ {reply_address}"
once = "/ var / spool / exim / autoreply / $ {local_part} @ $ {domain}"
once_repeat = 1d # Answer the sender once a day, although the logic can also be transferred to LDAP (see phamm-vacation.schema)
headers = “Content-Type: text / plain; charset = utf-8 \ nContent-Transfer-Encoding: 8bit »
subject = $ {rfc2047: Auto-Reply: $ h_subject:}
text = VACATION # Our template with OoO text message
body_only
no_return_message
# The same transport for
dovecot dovecot_lda:
driver = pipe
command = / usr / libexec / dovecot / dovecot-lda -f "$ sender_address" -d "
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
# All system root messages are sent to one box
begin rewrite
root @ * collector@domain.com Ttbcr
# SMTP
authenticators 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 weapon is Access Control Lists.
Actually, the whole article was conceived 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 .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 account
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 # Make a delay of 10 seconds from harm if the host has no return
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
= ratelimit log_message: the DSN delay The 10s for $ sender_address ($ sender_host_address)
# the END ratelimit the SECTION
#Predefined the acl variables An smtp_data level for
a warn set acl_m0 = $ sender_address_domain
a warn set acl_m1 = $ domain
a warn set acl_m2 = $ sender_host_address
a warn set acl_m3 = $ sender_address
a 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
# This is the same line, checking the validity of accounts and alliances at the check_rcpt stage, an exception is only for the service addresses of mailings.
! recipients = CHECK_1: CHECK_2
accept hosts =: + relay_from_hosts
control = dkim_disable_verify
# Allow authenticated senders to do anything else
accept authenticated = *
control = dkim_disable_verify
# Who can relay mail, if not, then 10 seconds of session delay.
deny message = relay not permitted
! domains = + local_domains: + virt_domains
delay = 10s
# Here we prohibit sending mail from foreign hosts using our domain name, optional
#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
# Here I implemented an alias check for activity, once a day the parser selects from the log Exim records and updates the value of the lastchange attribute in the format "% Y% m% d" for this alliance. Then, by the value of this attribute, you can check whether the alias is used or not.
The same is done for accounts, but an additional log_message, as for an alliance, is not required.
#Alias statistic
warn
domains = + virt_domains
log_message = ALIAS: $ local_part @ $ domain
recipients = CHECK_1
# Send an email to the greylist
#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:
# Who to send to spamassassin, optional. In this example, Exim takes care of placing the SPAM tag in the message header.
The global exim.filter will then look like this:
#
# 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
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 .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 account
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 # Make a delay of 10 seconds from harm if the host has no return
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
= ratelimit log_message: the DSN delay The 10s for $ sender_address ($ sender_host_address)
# the END ratelimit the SECTION
#Predefined the acl variables An smtp_data level for
a warn set acl_m0 = $ sender_address_domain
a warn set acl_m1 = $ domain
a warn set acl_m2 = $ sender_host_address
a warn set acl_m3 = $ sender_address
a 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
# This is the same line, checking the validity of accounts and alliances at the check_rcpt stage, an exception is only for the service addresses of mailings.
! recipients = CHECK_1: CHECK_2
accept hosts =: + relay_from_hosts
control = dkim_disable_verify
# Allow authenticated senders to do anything else
accept authenticated = *
control = dkim_disable_verify
# Who can relay mail, if not, then 10 seconds of session delay.
deny message = relay not permitted
! domains = + local_domains: + virt_domains
delay = 10s
# Here we prohibit sending mail from foreign hosts using our domain name, optional
#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
# Here I implemented an alias check for activity, once a day the parser selects from the log Exim records and updates the value of the lastchange attribute in the format "% Y% m% d" for this alliance. Then, by the value of this attribute, you can check whether the alias is used or not.
The same is done for accounts, but an additional log_message, as for an alliance, is not required.
#Alias statistic
warn
domains = + virt_domains
log_message = ALIAS: $ local_part @ $ domain
recipients = CHECK_1
# Send an email to the greylist
#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:
# Who to send to spamassassin, optional. In this example, Exim takes care of placing the SPAM tag in the message header.
The global exim.filter will then look like this:
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
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, in fact, is all.
We start Exim, we send the letter to ipupkin, we look at logs ...