Есть категория программ, которые кажутся простыми снаружи и бесконечными внутри. Exim из этой категории. Отправить письмо с его помощью можно за десять минут. Настроить его так, чтобы письма доходили до адресатов, не попадали в спам, проверялись на входе и подписывались на выходе, это уже другая история. История, в которой важна каждая деталь: порядок роутеров, условия в ACL, путь к приватному ключу и то, как именно SpamAssassin получает сообщение на анализ.

Exim обрабатывает каждое сообщение последовательно через три стадии: роутеры определяют, куда доставить письмо, транспорты определяют, как именно его доставить, и ACL определяют, принимать ли его вообще. Эти три слоя работают вместе, и понимание их взаимодействия отделяет работающую конфигурацию от той, которая работает до первого нестандартного случая.

Установка и выбор между light и heavy

На Debian и Ubuntu Exim поставляется в двух вариантах. Лёгкий вариант exim4-daemon-light подходит для простой доставки без контентной фильтрации. Тяжёлый вариант exim4-daemon-heavy включает поддержку SpamAssassin через content scanning extension, LDAP-поиски, встроенный Perl и SASL-аутентификацию. Для полноценного почтового сервера с проверкой спама нужен именно тяжёлый вариант.

apt-get install exim4-daemon-heavy spamassassin \
  spf-tools-perl sasl2-bin

# Начальная конфигурация через debconf
dpkg-reconfigure exim4-config

# Проверить текущую конфигурацию
cat /etc/exim4/update-exim4.conf.conf

На Debian конфигурация хранится либо в одном файле /etc/exim4/exim4.conf.template, либо разделена по директориям в /etc/exim4/conf.d/. Разделённый вариант удобнее для сложных конфигураций: файлы из каждой поддиректории объединяются утилитой update-exim4.conf в алфавитном порядке. Собственные файлы принято именовать с префиксом 00_, чтобы они включались первыми.

# Пересобрать конфигурацию из фрагментов и перезапустить
update-exim4.conf
systemctl restart exim4

# Проверить синтаксис конфигурации без перезапуска
exim4 -bV
exim4 -C /etc/exim4/exim4.conf.template -bV

Роутеры и логика принятия решений о доставке

Роутеры в Exim обрабатываются строго по порядку. Каждый роутер проверяет условие domains, senders, condition и другие предикаты. Если условие выполнено и роутер принял адрес, обработка останавливается. Если нет, Exim переходит к следующему роутеру. Именно поэтому порядок определения роутеров критичен: более специфичные условия должны идти раньше общих.

Стандартный роутер для DNS-доставки внешней почты:

dnslookup:
  debug_print = "R: dnslookup for $local_part@$domain"
  driver = dnslookup
  domains = ! +local_domains
  transport = remote_smtp
  same_domain_copy_routing = yes
  ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8 : \
    192.168.0.0/16 : 172.16.0.0/12 : 10.0.0.0/8
  no_more

Директива no_more означает, что если роутер принял адрес, но доставка не удалась, следующие роутеры не проверяются. Для релеинга через smart host вместо прямого DNS-разрешения используется manualroute:

smarthost_relay:
  debug_print = "R: smarthost for $local_part@$domain"
  driver = manualroute
  domains = ! +local_domains
  transport = remote_smtp_smarthost
  route_list = * smtp.relay.example.com::587
  host_find_failed = defer
  same_domain_copy_routing = yes
  no_more

Роутер для условного релеинга конкретных доменов через выделенный хост:

domain_relay:
  driver = manualroute
  domains = example.org : example.net
  transport = remote_smtp
  route_list = example.org  mx1.example.org  byname \
             : example.net  mx1.example.net  byname
  no_more

Разрешение релеинга для аутентифицированных клиентов и доверенных хостов задаётся в ACL-блоке acl_check_rcpt. Список доверенных хостов, которым разрешён релеинг:

# В секции main
hostlist relay_from_hosts = 127.0.0.1 : ::1 : 10.0.0.0/8

# В acl_check_rcpt
accept  hosts = +relay_from_hosts
        control = submission
        control = dkim_disable_verify

accept  authenticated = *
        control = submission
        control = dkim_disable_verify

Директива dkim_disable_verify для релейных источников критически важна: без неё Exim будет проверять DKIM-подписи в письмах, пришедших от внутренних серверов, что не имеет смысла и может блокировать легитимную почту.

Генерация ключей и настройка DKIM-подписи

DKIM-подпись в Exim настраивается на уровне транспорта, а не роутера. Это означает, что одна конфигурация подписи применяется ко всем письмам, проходящим через данный транспорт. Exim проверяет наличие приватного ключа динамически через переменные, что позволяет хранить ключи для разных доменов в одной директории и подписывать письма автоматически нужным ключом.

Сгенерировать ключевую пару для домена:

mkdir -p /etc/exim4/dkim
cd /etc/exim4/dkim

# Сгенерировать приватный ключ RSA 2048 бит
openssl genpkey -algorithm RSA \
  -out example.com.pem \
  -pkeyopt rsa_keygen_bits:2048

# Извлечь публичный ключ
openssl rsa -in example.com.pem \
  -pubout -out example.com.pub

# Установить права: читать может только exim
chown root:Debian-exim /etc/exim4/dkim/example.com.pem
chmod 640 /etc/exim4/dkim/example.com.pem

# Получить публичный ключ в формате для DNS
openssl rsa -in example.com.pem -pubout -outform DER 2>/dev/null \
  | openssl base64 -A

Полученный base64-ключ добавляется в DNS-запись TXT. Селектор mail это произвольная строка, которая должна совпадать с параметром dkim_selector в конфигурации Exim:

mail._domainkey.example.com. IN TXT \
  "v=DKIM1; k=rsa; p=MIIBIjANBgkq..."

Конфигурация транспорта с динамическим выбором ключа по домену отправителя:

remote_smtp:
  driver = smtp
  message_size_limit = ${if > {$max_received_linelength}{998} \
    {1}{0}}
  dkim_domain = ${lc:$sender_address_domain}
  dkim_selector = mail
  dkim_private_key = ${if exists \
    {/etc/exim4/dkim/${lc:$sender_address_domain}.pem} \
    {/etc/exim4/dkim/${lc:$sender_address_domain}.pem} \
    {0}}
  dkim_canon = relaxed
  dkim_sign_headers = Date:From:To:Subject:Message-ID:\
    Content-Type:MIME-Version

Когда dkim_private_key возвращает 0, Exim отправит письмо без подписи, что лучше, чем отказ доставки при отсутствии ключа. Алгоритм relaxed канонизации рекомендован для большинства случаев: он устойчив к незначительным изменениям пробелов и переносов строк, которые иногда вносят промежуточные серверы.

Проверить что подпись корректна после настройки:

# Отправить тестовое письмо и проверить заголовки
echo "DKIM test" | mail -s "Test DKIM" Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.

# Или через exim напрямую с verbose-выводом
exim4 -v Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. <<< "Subject: DKIM test

Test body"

Установка SpamAssassin и подключение к Exim

SpamAssassin в Exim работает через content scanning extension, доступное только в exim4-daemon-heavy. Демон spamd принимает сообщения через сокет или TCP-порт, анализирует их и возвращает оценку вместе с добавленными заголовками. Exim получает результат и принимает решение на основе настроенных порогов.

# Включить spamd в автозапуск
systemctl enable spamassassin
systemctl start spamassassin

# Проверить что spamd слушает
ss -tlnp | grep 783

Настройка SpamAssassin в /etc/spamassassin/local.cf:

# Путь к базе данных Bayes
bayes_path /var/spamassassin/bayes

# Не изменять тему письма
rewrite_subject 0

# Добавлять заголовки, но не оборачивать письмо
report_safe 0

# Минимальный балл для пометки как спам
required_score 5.0

# Использовать сетевые проверки
use_razor2 1
use_pyzor 1
mkdir -p /var/spamassassin
chown Debian-exim:Debian-exim /var/spamassassin

В конфигурации Exim подключение к SpamAssassin задаётся в секции main:

# Адрес демона spamd
spamd_address = 127.0.0.1 783

В ACL-блоке acl_check_data добавляется проверка содержимого:

# В /etc/exim4/conf.d/acl/40_exim4-config_check_data

# Не проверять исходящую почту от аутентифицированных клиентов
warn    condition = ${if def:authenticated_id}
        set acl_m_skipspam = yes

# Запустить SpamAssassin для входящей почты
warn    condition = ${if !def:acl_m_skipspam}
        spam = nobody:true
        add_header = X-Spam-Score: $spam_score
        add_header = X-Spam-Bar: $spam_bar
        add_header = X-Spam-Report: $spam_report

# Пометить как спам при оценке выше 5
warn    condition = ${if !def:acl_m_skipspam}
        condition = ${if >{$spam_score_int}{50}}
        add_header = X-Spam-Flag: YES

# Отклонить при очень высокой оценке (явный спам)
deny    condition = ${if !def:acl_m_skipspam}
        condition = ${if >{$spam_score_int}{140}}
        message = Message rejected as spam (score $spam_score)

Переменная $spam_score_int содержит оценку, умноженную на 10. Порог 50 соответствует оценке 5.0, порог 140 соответствует 14.0. Это разграничение важно: умеренный спам помечается заголовком для клиентской фильтрации, откровенный спам отклоняется на уровне SMTP с кодом ошибки.

Обучение Bayes и мониторинг очереди

SpamAssassin становится значительно точнее после обучения на реальной почте конкретного сервера. Байесовский фильтр требует нескольких сотен примеров и спама, и чистых писем, прежде чем начнёт давать надёжные результаты. Обучение запускается вручную или по расписанию через cron.

# Обучить на папке со спамом
sa-learn --spam /home/user/Maildir/.Spam/cur/

# Обучить на чистых письмах
sa-learn --ham /home/user/Maildir/cur/

# Посмотреть статистику базы Bayes
sa-learn --dump magic

# Синхронизировать базу вручную
sa-learn --sync

Мониторинг состояния очереди и анализ проблемных доставок:

# Посмотреть очередь
exim4 -bp

# Количество писем в очереди
exim4 -bpc

# Запустить немедленную обработку очереди
exim4 -qf

# Посмотреть лог доставки конкретного письма
exim4 -Mvl <message_id>

# Принудительно отправить конкретное письмо
exim4 -M <message_id>

# Очистить retry-кэш для хоста с которым были проблемы
exim_tidydb -t 0d /var/spool/exim retry
exim_tidydb -t 0d /var/spool/exim wait-remote_smtp

Последняя пара команд незаменима при ситуации, когда удалённый сервер был недоступен, Exim закэшировал провальное состояние, а сервер уже восстановлен, но Exim продолжает ждать истечения таймера перед следующей попыткой. Это распространённая причина задержек доставки, которая не имеет ничего общего с реальными проблемами соединения.

Проверка SPF, DMARC и итоговая диагностика конфигурации

Полноценный почтовый сервер без SPF-проверки входящей почты и правильно опубликованной SPF-записи для исходящей работает лишь вполовину своих возможностей. SPF-проверка в Exim включается через пакет spf-tools-perl и флаг CHECK_RCPT_SPF в макросах конфигурации.

# Включить SPF-проверку через локальные макросы
cat >> /etc/exim4/exim4.conf.localmacros << 'EOF'
CHECK_RCPT_SPF = yes
DKIM_CANON = relaxed
EOF

update-exim4.conf
systemctl reload exim4

Тестирование конфигурации DKIM через внешние инструменты после запуска:

# Отправить тест на специальный адрес проверки
echo "DKIM check" | mail -s "DKIM Test" \
  Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.

# Проверить DKIM-запись в DNS
dig TXT mail._domainkey.example.com +short

# Проверить SPF-запись
dig TXT example.com +short | grep spf

# Полная диагностика Exim с трассировкой конкретного адреса
exim4 -bt Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.
exim4 -bh 1.2.3.4 <<< "EHLO test.com"

Команда exim4 -bt показывает, через какой роутер и транспорт пройдёт письмо для данного адреса, без реальной отправки. Это самый быстрый способ убедиться, что роутеры настроены правильно и письмо не уйдёт туда, куда не планировалось. На почтовом сервере хорошая диагностика важнее хорошего дизайна конфигурации: ошибки в SMTP-инфраструктуре часто невидимы до момента, когда важное письмо тихо исчезает в очереди или возвращается с непонятной ошибкой.

Exim честен со своими пользователями: он делает ровно то, что написано в конфигурации. Это и его сила, и его ловушка. Написать конфигурацию, которая делает правильные вещи в правильном порядке, и при этом подписывает исходящие письма и фильтрует входящие без ложных срабатываний, значит понять, как думает этот MTA. После этого понимания он становится одним из самых управляемых почтовых серверов в мире.