Pull to refresh

Расширенная настройка Exim и Dovecot с привязкой к OpenLDAP

Reading time24 min
Views24K
В этой статье рассматривается расширенная настройка Exim, Dovecot и OpenLDAP для совместной работы на основе моего опыта с этими приложениями. Быть может, кто-то найдет для себя что-либо интересное и новое — в этом и была цель написания очередного howto на данную тему.

Почему Exim и OpenLDAP, а не Postfix и MySQL, например? Postfix отлично работает «из коробки», но если нужно что-то неординарное, то очень скоро Postfix превращается в неповоротливого монстра, обвешанного перл-скриптами, Exim же обладает чудовищным по силе мета-языком конфигурации и позволяет обойтись без сторонних скриптов и костылей. MySQL я посчитал избыточным для моих задач и заменил стандартным OpenLDAP, тем более для работы адресной книги используется LDAP. Dovecot очень шустрый и легкий в настройке плюс отлично интегрируется как с Exim, так и с OpenLDAP.

Итак, устанавливаем необходимый софт, тут все стандартно (apt-get, yum и тд). Я использовал Gentoo, поэтому emerge openldap dovecot exim (exim и dovecot должны иметь поддержку ldap).

Используемые USE флаги при сборке:

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

Первым в очереди будет OpenLDAP, в его базе будут храниться все почтовые аккаунты, группы и альясы, также OpenLDAP будет использоваться в качестве адресной книги для почтовых клиентов. Для простоты и удобства я не использую slapd-config, а храню все настройки в текстовом slapd.conf.

Так как стандартный OpenLDAP не имеет в наличии подходящей схемы для работы с почтой, то я использовал свою модифицированную версию phamm.schema и дефолтную phamm-vacation.schema.

Настройка OpenLDAP


Опускаю первичную настройку, создание basedn dc=domain,dc=com и ssl сертификатов для OpenLDAP, поскольку тут все стандартно.

Конфиг 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


Обязательно настраиваем репликацию для OpenLDAP (опять же тут все стандартно, поэтому опускаю детали). Единственно, что мне пришлось собирать OpenLDAP руками для поддержки sssvlv (Server Side Sorting and Virtual List View) в адресной книге (./configure --enable-ipv6=no --enable-syncprov=yes --enable-sssvlv=yes --with-tls=yes).
Также для корректной работы sssvlv в аутлюке, пришлось патчить исходники OpenLDAP.
Патч
--- 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)


Второй сервер будет работать как адресная книга в режиме только для чтения.

Конфиг slapd.conf для второго LDAP сервера
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

Для администрирования LDAP базы я использую phpldapadmin, так как он легкий, удобный и имеет поддержку XML шаблонов, что позволяет гибко настраивать необходимые шаблоны для создания аккаунтов. К сожалению, проект давно заброшен автором и больше не развивается.

Например, пример шаблона для создания почтового аккаунта.
Код
 <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>

При создании я использую следующую схему LDAP базы:

ou=people,dc=domain,dc=com — контейнер для хранения почтовых аккаунтов;
ou=groups,dc=domain,dc=com — контейнер для хранения posix групп;
ou=services,dc=domain,dc=com — контейнер для хранения аккаунтов системных сервисов (posix аккаунты);
ou=aliases,dc=domain,dc=com — контейнер для хранения почтовых альясов.

При создании почтового аккаунта используются множество атрибутов модифицированной мною phamm схемы, применение которых выходит за рамки это статьи (например, createMaildir или Backup). Отмечу только те, которые используются в фильтрах поиска.
accountActive = TRUE|FALSE — позволяет временно включать/отключать аккаунт или альяс;
vacationInfo — содержит текст Out of Office сообщения;
vacationActive — позволяет включать/отключать OoO сообщение;
quota — тут и так понятно (например: quota = 4G)

Итак, OpenLDAP настроен и запущен, репликация работает и при помощи phpldapadmin создан первый тестовый аккаунт ipupkin c адресом почты ipupkin@domain.com. В дальнейшем будет использоваться только один домен domain.com, поэтому создаем в нашем контейнере ou=groups posix группу domain и добавляем ipupkin в эту группу.

В данном примере все сервисы работают на одном линукс сервере и логично использовать лдап базу для идентификации пользователя в системе, поэтому перекладываем это процедуру на плечи Name Service Switch (nss) или System Security Services Daemon (sssd). Также плюсом данного решения является легкая адаптация с Samba доменом, при необходимости.
Предварительно нужно убедиться что в системе установлен пакет nss_ldap (или libnss-ldapd).
В /etc/nsswitch.conf меняем строки с compat на ldap (или winbind в случае Samba домена):
passwd: files ldap
shadow: files ldap
group: files ldap

Создаем файл /etc/ldap.conf следующего содержания:

ldap.conf
uri ldap://127.0.0.1
uri ldap://192.168.0.1 # Второй fallback лдап-сервер

base dc=domain,dc=com
binddn uid=proxyagent,ou=services,dc=domain,dc=com # Предварительно созданный в ou=services posix аккаунт, указанный в 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 #используем TLS вместо SSL
# Указываем пути к сертификатам
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

Проверяем, что nss работает и создана домашная директория (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

Теперь, когда пользователь ipupkin распознается системой, нужно чтобы ipupkin смог получать и отсылать почту.

Настройка Dovecot


При настройке Dovecot я удалил все вложенные монструозные дефолтные конфиги и для удобства создал только два — dovecot.conf и dovecot-ldap.conf.

dovecot.conf
auth_cache_negative_ttl = 10 mins
auth_debug = no
auth_debug_passwords = no
auth_mechanisms = plain login # Используется TLS, поэтому разрешаем plain
base_dir = /var/run/dovecot/
default_vsz_limit = 1024 M
disable_plaintext_auth = no
dotlock_use_excl = yes
lda_mailbox_autocreate = yes # Обязательно
lda_mailbox_autosubscribe = yes # Обязательно
listen = *
mmap_disable = yes
mail_fsync = always
mail_nfs_storage = no
mail_nfs_index = no
mail_debug = no
mail_location = maildir:~/Maildir # Где искать почтовый ящик, значение maildir берется из атрибута homeDirectory (в нашем случае это /home/ipupkin), соответственно полный путь будет /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 = </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 # Подключаем наш ldap конфиг
driver = ldap
}

passdb {
args = /etc/dovecot/dovecot-ldap-pass.conf #По рекомендации авторов dovecot символическая ссылка на 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
}

# Используется managesieve плагином вебпочты 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
}
}

# При заполнении почтового ящика на 90%, выполняется следующий скрипт
service quota-warning {
executable = script /etc/dovecot/quota-warning.sh # Сам скрипт выглядит так
Скрипт
#!/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 # Хак для Аутлука
mail_plugins = quota imap_quota mail_log notify
}

protocol pop {
pop3_client_workarounds = outlook-no-nuls oe-ns-eoh # Различные хаки для Аутлука
#pop3_uidl_format = %08Xu%08Xv # Используется при хранении Аутлуком почты на сервере, этот формат имеет проблемы с Аутлуком 2013
pop3_uidl_format = %g
pop3_fast_size_lookups=yes # Подробнее здесь раздел Maildir perfomance.
mail_plugins =
}

# Logical Delivery Agent (LDA) сервис, используемый Exim для доставки почты.
protocol lda {
hostname = domain.com
mail_fsync = optimized
mail_plugins = sieve quota
postmaster_address = postmaster@domain.com
log_path =
info_log_path =
}

protocol sieve {
}

# Различные опциональные добавки
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 # Указываем процент заполнения почтового ящика, при котором срабатывает скрипт 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


Проверяем что ошибок нет (dovecot -a) и запускаем dovecot.

Финал: Настройка Exim


Основной целью при настройке Exim был максимальный отказ от любых скриптов и реализация всего функционала только средствами Exim. Только как исключение используются дебиановский greylistd на питоне и перловый amavisd-new.
При настройке Exim также будут использоваться два файла — основной exim.conf и acl_smtp для ACL правил.
Также опционально в конфиге присутствует роутер и транспорт для mailman.

exim.conf
CONFIG_PREFIX=/etc/exim
ACL_PREFIX=CONFIG_PREFIX/acls #здесь хранятся все ACL конфиги
DB_PREFIX=/var/spool/exim/db #для увеличения быстродействия желательно использовать tmpfs

# Шаблоны являются сильной стороной Exim и позволяют легко заменить длинную строку коротким словом.
# Определяем шаблоны для 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 # Указываем как соединяться к лдап-серверам, второй сервер будет использоваться как fallback.

INTERFACE = your_external_ip # Указываем внешний айпи, на котором будет висеть exim
BASEDN = dc=domain,dc=com # basedn лдап сервера

# В этой секции указаны самые важные темплейты, задающие логику работы Exim

# Проверка альяса, соответствует ли адрес альяса значению атрибута mail в контейнере aliases, альяс также должен принадлежать классу VirtualMailAlias и иметь значение TRUE для атрибута accountActive
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}))} }

# Проверка аккаунта, соответствует ли адрес получателя значению атрибута mail в контейнере people, аккаунт также должен принадлежать к классу VirtualMailAccount и иметь значение TRUE для атрибута accountActive
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}))} }

#Проверка suspended аккаунта, аккаунт не может получать почту, но остается в системе (актуально для сотрудниц в материнском отпуске)
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}))} }

# Список участников альяса, кому пересылать почту, значение аттрибута maildrop
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}))}}

# Путь к почтовому ящику, значение аттрибута mailbox
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}))} }

# Текст OoO сообщения, значение аттрибута 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 — кажется так и не исправили
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 # Так как у нас только один домен, то указываем его. Если используется множество доменов, то нужно создать темплейт выборки доменов из лдап базы.
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 # Не предлагать SMTP AUTH локалхосту
bounce_message_file = CONFIG_PREFIX/bounce.msg # Указываем формат bounce сообщения, у меня так:

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.
****

bounce_return_size_limit = 100K
delay_warning = 15m:1h:99d
deliver_queue_load_max = 40
disable_ipv6
exim_group = vmail # Желательно чтобы все почтовые сервисы (dovecot, spamassassin, clamav и тд) работали под одним gid
exim_user = vmail # Желательно чтобы все почтовые сервисы (dovecot, spamassassin, clamav и тд) работали под одним uid
headers_charset = UTF-8 # Желательно включить
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 # Логи отсылаются syslog сервису
syslog_processname = exim
system_filter = DB_PREFIX/exim.filter # Глобальный exim фильтер, у меня в основном не используется
timeout_frozen_after = 7d
tls_advertise_hosts = !127.0.0.1 # Не предлагать TLS локалхосту

# Указываем пути к сертификатам
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

# Определяем формат заголовка письма
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>}}»

# Подключаем антивирус и антиспам напрямую, так как LDAP здесь не используется, то все стандартно, поэтому опускаем настройку.
# av_scanner = clamd:/tmp/clamd
# spamd_address = 127.0.0.1 783

begin acl

# Подключаем наш ACL конфиг (см ниже)
.include ACL_PREFIX/acl_smtp

# Создаем роутеры
begin routers

# Роутер для исходящей почты
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

# Роутеры для входящей почты

#Опицональный транспорт для amavis (сделаем одно исключение перлу :) )
#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 # Не отвечать локалхосту и отправителям, указанным в autoreply.noanswer.db
condition = ${if and {\
{!eq{CHECK_VACATION}{}}\ # Наш шаблон
{!match{$h_precedence:}{junk|bulk|list}}\ # Не отвечать рассылкам
{!def:header_Auto-Submitted:}\
{!def:header_List-Id:}\
}}
no_verify
no_expn
unseen
transport = auto_responder

# Приостановленные аккаунты, вся приходящая почта для suspended аккаунта отправляется в «черную дыру»
suspended:
driver = redirect
domains = +virt_domains
condition = CHECK_3
forbid_file
forbid_pipe
forbid_filter_reply = true
data = :blackhole:
no_more

# Виртуальные альясы
aliases:
driver = redirect
domains = !+local_domains
condition = CHECK_1 # Тут происходит двойная проверка (первая в acl_smtp), если действующий аккаунт имеет еще и альяс, ничего лучше не придумал
forbid_file
forbid_pipe
forbid_filter_reply = true
data = CHECK_DATA # Кому пересылать письма
allow_fail
allow_defer

mailman_router:
driver = accept
domains = domain.com
require_files = MM_LISTCHK # Вместо проверки по файлу, можно сделать поверку по значению атрибута aliasType=DL, например
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 — это Access Control Lists.
Собственно, вся статья затевалась ради одной стройки в секции smtp_rcpt.

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


Вот, собственно, и все.

Запускаем Exim, отсылаем письмо ipupkin-у, смотрим логи…
Tags:
Hubs:
+7
Comments2

Articles