Exim и Dovecot без SQL в Debian 12 Bookworm

Содержание

Введение

Эта статья является обновлённой версией статьи Exim и Dovecot без SQL, написанной ранее для Debian 7 Wheezy. В эту статью также были добавлены наработки из статьи Exim и Dovecot quota-status.

Среди системных администраторов и инженеров распространено мнение, что чем система проще, тем она надёжнее. Конечно, не стоит воспринимать это утверждение буквально. Но, как говорится, умному - достаточно. Чем меньше компонентов в системе, чем проще взаимосвязи между компонентами, тем меньше вероятность того, что она сломается и тем выше вероятность того, что её будет просто починить.

Именно поэтому большинство системных администраторов Unix любят использовать текстовые файлы, которые легко редактируются при помощи любого текстового редактора (файлы XML к таковым обычно не относятся). Здесь я опишу настройку почтовой системы на основе Dovecot и Exim без использования базы данных. Вместо этого все данные будут помещаться в текстовых файлах.

Описание настройки основано на моих предыдущих заметках Установка и настройка Dovecot и Настройка Exim, практически повторяя их. Прошу прощения за самоплагиат - я считаю, что так будет удобнее.

Настройка Dovecot

Начнём с настройки Dovecot, поскольку для настройки Exim необходимо иметь уже настроенный Dovecot.

Установка Dovecot

Установим пакеты Dovecot, содержащие поддержку серверов POP3 и IMAP:

# apt-get install dovecot-core dovecot-imapd dovecot-pop3d

Подготовка системы

Создадим группу и пользователя vmail, от имени которого будет работать Dovecot и дадим этому пользователю доступ к каталогу, в котором будет храниться почта пользователей почтовой системы. На серверах для размещения почтовых ящиков, как и другой часто меняющейся информации, обычно используется раздел /var, который заранее делается достаточно большим. Я размещу данные в каталоге /srv:

# groupadd -g 120 -r vmail
# useradd -g 120 -r -u 120 vmail
# mkdir /srv/vmail
# chown vmail:vmail /srv/vmail
# chmod u=rwx,g=rx,o= /srv/vmail

Базовая настройка Dovecot

Я буду использовать защищённые версии протоколов IMAP и POP3, поэтому настрою в файле /etc/dovecot/conf.d/10-auth.conf механизмы PLAIN и LOGIN, чтобы хранить пароли в базе данных в хэшированном виде:

disable_plaintext_auth = no
auth_default_realm = domain.tld
auth_mechanisms = plain login
#!include auth-system.conf.ext
!include auth-passwdfile.conf.ext

Настроим использование учётных данных из файла, подобного /etc/passwd, прописав в файле /etc/dovecot/conf.d/auth-passwdfile.conf.ext следующие секции:

passdb {
  driver = passwd-file
  args = scheme=CRYPT username_format=%u /etc/dovecot/users
}

userdb {
  driver = passwd-file
  args = username_format=%u /etc/dovecot/users

  # Поля по умолчанию, которые могут быть заменены значениями из файла passwd
  default_fields = uid=vmail gid=vmail userdb_home=/srv/vmail/%Ld/%Ln userdb_location=maildir:/srv/vmail/%Ld/%Ln userdb_quota_rule=*:storage=1G 

  # Поля, значения которых заменяют значения из файла passwd
  #override_fields = home=/srv/vmail/%Ld/%Ln
}

Создадим в каталоге /etc/dovecot файл users и проставим права доступа:

# cd /etc/dovecot
# touch users
# chown root:dovecot users
# chmod u=rw,g=r,o= users

В файле /etc/dovecot/users могут быть следующие поля:

user:{plain}password:uid:gid:gecos:home:shell:extra_fields

Назначение полей:

  • user - почтовый ящик (в данном случае - вместе с доменом),
  • password - пароль (можно явным образом указывать алгоритм хэширования пароля),
  • uid - системный идентификатор владельца файлов почты,
  • gid - системный идентификатор группы владельца файлов почты,
  • gecos - справочная информация о почтовом ящике (игнорируется),
  • home - путь к каталогу почты,
  • shell - интерпретатор (игнорируется),
  • extra_fields - дополнительные настройки (квота, например).

Любое из полей может быть не определено в файле, если в настройках Dovecot указаны значения этих полей по умолчанию. При указании дополнительных полей, используемых в секции userdb необходимо перед именем поля указывать префикс userdb_, как в примере выше, в настройках default_fields. Имеется возможность зафиксировать часть настроек почтового ящика при помощи настройки override_fields, так что значения из файла будут игнорироваться.

Подробнее о формате файла и других настройках можно прочитать на официальной wiki-странице Dovecot: Passwd-file

Изменим форматирование отметок времени, вписав в файл /etc/dovecot/conf.d/10-logging.conf следующую настройку:

log_timestamp = "%Y-%m-%d %H:%M:%S "

На время отладки также можно включить другие опции из этого файла:

auth_verbose = yes
auth_verbose_passwords = yes
auth_debug = yes
mail_debug = yes

В файле /etc/dovecot/conf.d/10-mail.conf настроим путь к почтовым ящикам и пользователя, от имени которого Dovecot будет работать с ящиками:

mail_home = /srv/vmail/%Ld/%Ln
mail_location = maildir:/srv/vmail/%Ld/%Ln
mail_uid = vmail
mail_gid = vmail
first_valid_uid = 120
last_valid_uid = 120
first_valid_gid = 120
last_valid_gid = 120

Сейчас настроим сервис, при помощи которого Exim будет проверять учётные данные почтовых клиентов. Для этого отредактируем файл /etc/dovecot/conf.d/10-master.conf и впишем в него настройки сервиса:

service auth {
  unix_listener auth-userdb {
    mode = 0660
    user = Debian-exim
    #group = 
  }
}

Начальная настройка сервера окончена.

Настройка плагина acl

Плагин acl позволяет пользователям предоставлять друг другу доступ к папкам в своих почтовых ящиках. Это может быть полезно для корпоративных пользователей. Например, для директора и его заместителя. Или для директора и его секретаря. Или для сотрудников из одного отдела, которые подменяют друг друга на время обеда или отпуска. Эта возможность, естественно, доступна только при использовании протокола IMAP.

В файле /etc/dovecot/conf.d/10-mail.conf включаем использование плагина:

mail_plugins = acl

В файле /etc/dovecot/conf.d/20-imap.conf включаем использование плагина в IMAP-сервере:

protocol imap {
  mail_plugins = $mail_plugins imap_acl
}

В файле /etc/dovecot/conf.d/10-mail.conf прописываем следующие настройки:

namespace inbox {
  separator = /
}

namespace {
  type = shared
  separator = /
  prefix = shared/%%u/
  location = maildir:%%h:INDEX=%h/shared/%%u
  list = children
}

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

Поясню смысл настроек location для пространства имён общих каталогов:

  • maildir:%%h - означает место расположения чужого почтового ящика в формате Maildir,
  • %%h - полный путь к Maildir-каталогу чужого ящика,
  • INDEX=%h/shared/%%u - задаёт каталог, в который как бы монтируются каталоги чужой почты, к которым её владелец дал нам доступ,
  • %h - путь к Maildir-каталогу нашего ящика,
  • %%u - имя другого пользователя в виде box@domain.tld.

В файл /etc/dovecot/conf.d/90-acl.conf прописываем настройки плагина:

plugin {
  acl = vfile
}

plugin {
  acl_shared_dict = file:/srv/vmail/%Ld/shared-mailboxes.db
}

Значение vfile предписывает создавать внутри почтового ящика файл dovecot-acl, в котором и будут прописываться права доступа к нему со стороны других пользователей.

Значение acl_shared_dict указывает путь к файлу словаря, который позволит пользователям узнавать, к каким каталогам в чужих почтовых ящиках у них имеется доступ. В данном случае для каждого домена будет создан отдельный файл словаря, расположенный в каталоге домена, на одном уровне с ящиками.

Заодно опишем в файле /etc/dovecot/conf.d/15-mailboxes.conf назначение различных каталогов внутри пространства имён, в котором хранится личная почта пользователя:

namespace inbox {
  mailbox Drafts {
    special_use = \Drafts
  }
  mailbox Junk {
    special_use = \Junk
  }
  mailbox Trash {
    special_use = \Trash
  }
  mailbox Sent {
    special_use = \Sent
  }
}

Назначение каталогов:

  • Drafts - каталог черновиков,
  • Junk - каталог для спама,
  • Trash - каталог для удалённых писем,
  • Sent - каталог для отправленных писем.

Современные почтовые программы смогут прямо по протоколу IMAP узнать назначение каждого из специальных каталогов, вне зависимости от их названия. Это бывает полезно, если каталог имеет нестандартное название или название на языке пользователя ящика, например "Входящие" или "Спам".

Настройка плагина quota

Плагин quota позволяет назначить для почтового ящика ограничения на объём хранящихся в нём писем или даже на их общее количество. На мой взгляд, ограничение на общее количество писем может защитить от исчерпания inode'ов файловой системы и от долгой отдачи списка писем при проверке почты, а какого-то другого практического смысла не имеет.

Если нужно изменить значение квоты какого-то пользователя, то в поле extra_fields файла /etc/dovecot/users нужно указать квоту в следующем виде:

userdb_quota_rule=*:storage=10G 

Включим использование плагина в файле /etc/dovecot/conf.d/10-mail.conf:

mail_plugins = acl quota

Плагин acl был включен в предыдущем разделе. Если вы его не включали, то указывать его не нужно.

В файл /etc/dovecot/conf.d/15-lda.conf впишем, что в случае превышения квоты Dovecot должен сообщать о временной ошибке, но не отклонять письмо окончательно. Почтовый сервер отправителя (или наш MTA) будет периодически предпринимать повторные попытки в надежде на то, что адресат почистит свой ящик от ненужных писем.

quota_full_tempfail = yes

В файл /etc/dovecot/conf.d/20-imap.conf добавим поддержку квот в IMAP-сервере:

protocol imap {
  mail_plugins = $mail_plugins imap_acl imap_quota
}

Плагин imap_acl был включен в предыдущем разделе. Если вы его не включали, то указывать его не нужно.

Плагин imap_quota позволит почтовым клиентам, работающим по протоколу IMAP, узнавать квоту почтового ящика и её текущее использование.

Укажем в файле /etc/dovecot/conf.d/90-quota.conf, что значения квот берутся из словаря и зададим пустое правило по умолчанию:

plugin {
  quota = dict:user::file:%h/dovecot-quota
  quota_rule = *:
}

В каталоге каждого почтового ящика будет создаваться файл dovecot-data, внутри которого будет вестись учёт текущего количества сообщений в ящике и их объёма. Чтобы принудительно пересчитать квоты всех почтовых ящиков, можно воспользоваться следующей командой:

# doveadm quota recalc -A

Настройка плагина quota-status

В Dovecot имеется плагин quota-status, который реализует сервис для проверки превышения квоты получателем письма. Этот сервис можно использовать на SMTP-сервере для отклонения писем для получателей, почтовый ящик котороых превысил квоты.

В файл /etc/dovecot/conf.d/90-quota.conf вписываем:

plugin {
  quota_status_success = OK 
  quota_status_nouser = NOUSER
  quota_status_overquota = OVER
}

service quota-status {
  executable = quota-status -p postfix

  unix_listener exim-quota-status {
    mode = 0660
    user = Debian-exim
    group = Debian-exim
  }

  client_limit = 1
}

Настройка автоочистки

Для очистки почтовых ящиков от устаревших сообщений можно добавить в настройки ящика опцию autoexpunge с указанием времени сохранения писем. Все письма старше указанного времени будут автоматически удаляться при отключении пользователя. Например, наcтроим в файле конфигурации /etc/dovecot/conf.d/15-mailboxes.conf автоматическое удаление писем старше 2 недель из папок со спамом и корзины:

namespace inbox {
  mailbox Junk {
    special_use = \Junk
    autoexpunge = 2w
    autoexpunge_max_mails = 1000
  }
  mailbox Trash {
    special_use = \Trash
    autoexpunge = 2w
    autoexpunge_max_mails = 1000
  }
}

Кроме включения самого автоматического удаления рекомендуется включить индекс писем, включить хранение отметки времени в индексе и ограничить количество писем, удаляемых за один раз.

Для этого пропишем в файле конфигурации /etc/dovecot/conf.d/10-mail.conf следущие опции:

mailbox_list_index = yes
mail_always_cache_fields = date.save

Настройка SSL

Процедуру получения сертификатов SSL оставим за рамками этой статьи. Предположим сейчас, что сертификаты уже были получены и тогда можно включить поддержку SSL в файле /etc/dovecot/conf.d/10-ssl.conf и указать в нём пути к файлам сертификата:

ssl = yes
ssl_cert = </etc/ssl/mail_public.pem
ssl_key = </etc/ssl/mail_private.pem

Для настройки нестандартных параметров генерации сеансовых ключей по алгоритму Диффи-Хеллмана можно сгенерировать параметры такой командой:

# openssl dhparam -out /etc/ssl/mail_dh.pem 4096

Путь к сгенерированному файлу параметров указывается в том же файле /etc/dovecot/conf.d/10-ssl.conf:

ssl_dh = </etc/ssl/mail_dh.pem

Настройка плагина sieve

Sieve - это скрипты фильтрации почты, которые выполняются агентом локальной доставки (LDA) в момент получения письма от почтового сервера (MTA). Скрипты позволяют раскладывать письма в разные папки, ориентируясь на их содержимое - тему письма, получателей, отправителей и т.п. Можно удалить письмо, переслать его на другой ящик или отправить уведомление отправителю, причём использовать можно любое поле заголовка или содержимое тела письма.

Главное преимущество Sieve заключается в том, что пользователю не нужно настраивать правила фильтрации в каждом из используемых им почтовых клиентов - правила едины для всех почтовых клиентов сразу. Кроме того, фильтрация происходит вообще без участия клиента. Клиент, подключившись к почтовому ящику, имеет возможность работать уже с отсортированной почтой. Кроме того, отправка уведомлений о получении или пересылка писем на другой ящик вообще может происходить без участия почтового клиента.

Конечно, в наши времена больших почтовых сервисов типа Gmail или Яндекс-почты, этим никого не удивишь. Но тут плюс заключается в том, что перед нами не стоит дилемма "удобство" - "безопасность". Мы можем хранить почту у себя, не делясь ею с посторонними компаниями, имея над ней полный контроль, и в то же время можем пользоваться удобствами, характерными для больших почтовых сервисов.

Установим пакет с плагином Sieve:

# apt-get install dovecot-sieve

Включим использование плагина в файле /etc/dovecot/conf.d/15-lda.conf:

protocol lda {
  mail_plugins = $mail_plugins sieve
}

Укажем настройки плагина в файле /etc/dovecot/conf.d/90-sieve.conf:

plugin {
  sieve = /srv/vmail/%Ld/%n/active.sieve # Расположение активного скрипта
  sieve_max_script_size = 1M             # Максимальный размер одного скрипта
  sieve_quota_max_scripts = 50           # Максимальное количество скриптов
  sieve_quota_max_storage = 1M           # Максимальный общий объём скриптов
}

Подробнее о скриптах Sieve можно почитать на Википедии, в статье Sieve.

Настройка сервиса managesieve

Плагин sieve не был бы столь полезным, если бы Sieve-скриптами нельзя было бы управлять прямо из почтового клиента. Именно эту функцию и реализует сервис ManageSieve. Он ожидает подключений клиентов на отдельном TCP-порту 4190. Для управления скриптами клиент использует учётные данные своего почтового ящика.

Для включения сервиса достаточно лишь установить дополнительный пакет:

# apt-get install dovecot-managesieved

В следующих заметках фильтрация писем при помощи Sieve будет рассмотрена подробнее - я покажу, как им пользоваться в почтовых клиентах.

Результирующий файл конфигурации

Поскольку настроек очень много, проверить их можно при помощи следующей команды:

# doveconf -nP

Опция n предписывает показывать только те настройки, которые отличаются от настроек по умолчанию. У меня со всеми плагинами, настройка которых была тут описана, команда выдаёт следующий результат:

# 2.3.19.1 (9b53102964): /etc/dovecot/dovecot.conf
# Pigeonhole version 0.5.19 (4eae2f79)
# OS: Linux 6.1.0-26-amd64 x86_64 Debian 12.7 ext4
# Hostname: mail.domain.tld
auth_default_realm = domain.tld
auth_mechanisms = plain login
disable_plaintext_auth = no
first_valid_gid = 120
first_valid_uid = 120
last_valid_gid = 120
last_valid_uid = 120
log_timestamp = "%Y-%m-%d %H:%M:%S "
mail_always_cache_fields = date.save
mail_gid = vmail
mail_home = /srv/vmail/%Ld/%Ln
mail_location = maildir:/srv/vmail/%Ld/%Ln
mail_plugins = acl quota
mail_privileged_group = mail
mail_uid = vmail
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 index ihave duplicate mime foreverypart extracttext
namespace {
  list = children
  location = maildir:%%h/Maildir:INDEX=%h/shared/%%u
  prefix = shared/%%u/
  separator = /
  type = shared
}
namespace inbox {
  inbox = yes
  location = 
  mailbox Drafts {
    special_use = \Drafts
  }
  mailbox Junk {
    autoexpunge = 2 weeks
    autoexpunge_max_mails = 1000
    special_use = \Junk
  }
  mailbox Sent {
    special_use = \Sent
  }
  mailbox "Sent Messages" {
    special_use = \Sent
  }
  mailbox Trash {
    autoexpunge = 2 weeks
    autoexpunge_max_mails = 1000
    special_use = \Trash
  }
  prefix = 
}
passdb {
  args = scheme=CRYPT username_format=%u /etc/dovecot/users
  driver = passwd-file
}
plugin {
  acl = vfile
  acl_shared_dict = file:/srv/vmail/%Ld/shared-mailboxes.db
  quota = dict:user::file:%h/dovecot-quota
  quota_rule = *:
  quota_status_nouser = NOUSER
  quota_status_overquota = OVER
  quota_status_success = OK
  sieve = /srv/vmail/%Ld/%Ln/active.sieve
  sieve_max_script_size = 1M
  sieve_quota_max_scripts = 50
  sieve_quota_max_storage = 1M
}
postmaster_address = postmaster@domain.tld
protocols = " imap sieve pop3"
quota_full_tempfail = yes
service auth {
  unix_listener auth-userdb {
    mode = 0660
    user = Debian-exim
  }
}
service quota-status {
  client_limit = 1
  executable = quota-status -p postfix
  unix_listener exim-quota-status {
      group = Debian-exim
      mode = 0660
      user = Debian-exim
  }
}
ssl_cert = </etc/ssl/mail_public.pem
ssl_client_ca_dir = /etc/ssl/certs
ssl_dh = </etc/ssl/mail_dh.pem
ssl_key = </etc/ssl/mail_private.pem
userdb {
  args = username_format=%u /etc/dovecot/users
  default_fields = uid=vmail gid=vmail userdb_home=/srv/vmail/%Ld/%Ln userdb_location=maildir:/srv/vmail/%Ld/%Ln userdb_quota_rule=*:storage=1G
  driver = passwd-file
}
protocol lda {
  mail_plugins = acl quota sieve
}
protocol imap {
  mail_plugins = acl quota imap_acl imap_quota
}

Настройка Exim

Настройка Exim, как уже было сказано, зависит от настроенного Dovecot. Exim использует сервис Dovecot для SMTP-аутентификации и список почтовых ящиков из файла /etc/dovecot/users.

Установка

Установим SMTP-сервер Exim:

# apt-get install exim4-daemon-heavy

Создаём файл конфигурации /etc/exim4/exim4.conf со следующим начальным содержимым:

# Имя нашей почтовой системы
primary_hostname = mail.domain.tld

# Список доменов нашей почтовой системы
domainlist local_domains = /etc/exim4/local_domains

# Список доменов, для которых наша почтовая система является резервной
domainlist relay_domains = /etc/exim4/relay_domains

# Список узлов, почту от которых будем принимать без проверок
hostlist relay_from_hosts = 

# Правила для проверок
acl_not_smtp = acl_check_not_smtp
acl_smtp_rcpt = acl_check_rcpt
#acl_smtp_data = acl_check_data

# Сокет-файл антивируса ClamAV
# av_scanner = clamd:/run/clamav/clamd.ctl
# Сокет-файл SpamAssassin
# spamd_address =

# Отключаем IPv6, слушаем порты 25 и 587
disable_ipv6
daemon_smtp_ports = 25 : 587

# Дописываем домены отправителя и получателя, если они не указаны
qualify_domain = domain.tld
qualify_recipient = domain.tld

# Exim никогда не должен запускать процессы от имени пользователя root
never_users = root

# Проверять прямую и обратную записи узла отправителя по DNS
host_lookup = *

# Отключаем проверку пользователей узла отправителя по протоколу ident
rfc1413_hosts = *
rfc1413_query_timeout = 0s

# Только эти узлы могут не указывать домен отправителя или получателя
sender_unqualified_hosts = +relay_from_hosts
recipient_unqualified_hosts = +relay_from_hosts

# Лимит размера сообщения, 30 мегабайт
message_size_limit = 30M

# Запрещаем использовать знак % для явной маршрутизации почты
percent_hack_domains =

# Настройки обработки ошибок доставки, используются значения по умолчанию
ignore_bounce_errors_after = 2d
timeout_frozen_after = 7d

begin acl

  # Проверки для локальных отправителей
  acl_check_not_smtp:
     accept

  # Проверки на этапе RCPT
  acl_check_rcpt:
    accept hosts = :

    # Отклоняем неправильные адреса почтовых ящиков  
    deny message = Restricted characters in address
         domains = +local_domains
         local_parts = ^[.] : ^.*[@%!/|]

    # Отклоняем неправильные адреса почтовых ящиков  
    deny message = Restricted characters in address
         domains = !+local_domains
         local_parts = ^[./|] : ^.*[@%!] : ^.*/\\.\\./

    # В локальные ящики postmaster и abuse принимает почту всегда
    accept local_parts = postmaster : abuse
           domains = +local_domains

    # Проверяем существование домена отправителя
    require verify = sender

    # Принимаем почту от доверенных узлов, попутно исправляя заголовки письма
    accept hosts = +relay_from_hosts
           control = submission

    # Принимаем почту от аутентифицированных узлов, попутно исправляя заголовки письма
    accept authenticated = *
           control = submission/domain=

    # Для не доверенных и не аутентифицированных требуется, чтобы получатель был в домене,
    # ящик которого находится у нас или для которого мы являемся резервным почтовым сервером
    require message = Relay not permitted
            domains = +local_domains : +relay_domains

    # Если домен правильный, то проверяем получателя
    require verify = recipient

    accept

begin routers

  # Поиск транспорта для удалённых получателей
  dnslookup:
    driver = dnslookup
    domains = ! +local_domains
    transport = remote_smtp
    ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8
    no_more

  # Пересылки для локальных получателей из файла /etc/aliases
  system_aliases:
    driver = redirect
    allow_fail
    allow_defer
    domains = domain.tld
    data = ${lookup{$local_part}lsearch{/etc/aliases}}

  # Пересылки для получателей в разных доменах
  aliases:
    driver = redirect
    allow_fail
    allow_defer
    data = ${lookup{$local_part@$domain}lsearch{/etc/exim4/aliases}}  

  # Получение почты на локальный ящик
  mailbox:
    driver = accept
    condition = ${lookup{$local_part@$domain}lsearch{/etc/dovecot/users}{yes}{no}}
    user = dovecot
    transport = dovecot_virtual_delivery
    cannot_route_message = Unknown user

begin transports

  # Транспорт для удалённых получателей
  remote_smtp:
    driver = smtp

  # Транспорт для локальных получателей из Dovecot
  dovecot_virtual_delivery:
    driver = pipe
    command = /usr/lib/dovecot/dovecot-lda -d $local_part@$domain -f $sender_address
    message_prefix =
    message_suffix =
    delivery_date_add
    envelope_to_add
    return_path_add
    log_output
    user = vmail
    temp_errors = 64 : 69 : 70: 71 : 72 : 73 : 74 : 75 : 78

begin retry

  *   *   F,2h,15m; G,16h,1h,1.5; F,4d,6h

begin rewrite

begin authenticators

  # Использование LOGIN-аутентификации из Dovecot
  dovecot_login:
    driver = dovecot
    public_name = LOGIN
    server_socket = /var/run/dovecot/auth-userdb
    server_set_id = $auth1

  # Использование PLAIN-аутентификации из Dovecot  
  dovecot_plain:
    driver = dovecot
    public_name = PLAIN
    server_socket = /var/run/dovecot/auth-userdb
    server_set_id = $auth1

Сразу поменяем права доступа к файлу конфигурации:

# chmod u=rw,g=r,o= /ect/exim4/exim4.conf
# chown root:Debian-exim /etc/exim4/exim4.conf

Можно дать остальным пользователям доступ на чтение, т.к. никакой особо секретной информации в файле конфигурации нет. С другой стороны - нужды давать такой доступ тоже нет.

Чтобы Exim мог читать файл /etc/dovecot/users, включим пользователя Debian-exim в группу dovecot:

# usermod -aG dovecot Debian-exim

Создадим файл /etc/exim4/local_domains со списками локальных доменов, для которых Exim будет принимать почту к доставке:

domain.tld

Создадим файл /etc/exim4/relay_domains со списокм доменов, для которых Exim будет выступать в качестве резервного. Он будет принимать почту для дальнейшей пересылки на SMTP-сервер с более высоким приоритетом, соответствующим меньшему значению в записи MX соответствующего домена. В моём случае этот файл будет пустым.

Теперь можно перезапустить Exim, чтобы он начал работать в минимальной конфигурации:

# systemctl restart exim4

Также теперь можно перезапустить и Dovecot, т.к. после установки Exim в системе должен появиться пользователь Debian-exim, упоминаемый в конфигурации Dovecot:

# systemctl restart dovecot

Настройка антивируса

Устанавливаем демон ClamAV для проверки файлов на вирусы:

# apt-get install clamav-daemon

Поскольку по велению Cisco обновления антивирусных баз данных больше не раздаются IP-адресам из России, нам понадобится настроить использование зеркала. Для этого нужно закомментировать в файле /etc/clamav/freshclam.conf настроенные серверы с обновлениями и их зеркала, отключить опцию ScriptedUpdates и указать российское зеркало, которое отдаёт обновления антивирусных баз:

#DNSDatabaseInfo current.cvd.clamav.net
ScriptedUpdates no
#DatabaseMirror db.local.clamav.net
#DatabaseMirror database.clamav.net
DatabaseMirror http://clamav.ufanet.ru

Сразу же обновляем антивирусную базу:

# freshclam

Включим clamav в группу Debian-exim, чтобы он мог сканировать файлы, созданные Exim'ом:

# usermod -aG Debian-exim clamav

Добавим в главную секцию файла /etc/exim4/exim4.conf путь к сокет-файлу ClamAV и ACL для этапа DATA:

av_scanner = clamd:/run/clamav/clamd.ctl
acl_smtp_data = acl_check_data

В секцию acl файла /etc/exim4/exim4.conf добавим правило, запрещающее приём писем, содержащих вирусы:

acl_check_data:

  deny message = message contains a virus ($malware_name)
       malware = *

  accept

Перезагрузим Exim, чтобы настройки вступили в силу:

# systemctl reload exim4

Осталось проверить, что антивирусная система используется. Для этого создадим специально предназначенный для таких целей тестовый файл EICAR:

$ echo -n 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > eicar.txt

И попробуем его отправить во вложении с какого-нибудь почтового ящика почтовой системы на тот же ящик. Если письмо не пришло, значит антивирусная система работает. Для полной уверенности можно ещё заглянуть в журнал почтовой системы /var/log/exim4/mainlog, где должна появиться строчка вида:

2014-04-12 22:06:06 1WZ0RR-0007TO-RM H=localhost (domain.tld) [127.0.0.1] F= A=dovecot_plain:box@domain.tld rejected after DATA: message contains a virus (Eicar-Test-Signature)

Проверка квот

В файл конфигурации /etc/exim4/exim4.conf добавим в правило acl_check_rcpt дополнительную проверку перед окончательным действием accept:

defer message = 422 Mailbox $local_part@$domain is over quota
      domains = +local_domains
      condition = ${if eq{${extract{action}\
                                   {${readsocket{/var/run/dovecot/exim-quota-status}\
                                                {size=$message_size\nrecipient=$local_part@$domain\n\n}\
                                                {5s}\
                                                { }\
                                                {action=FAIL}}}}}\
                         {OVER}\
                         {yes}\
                         {no}}

Указанное выше условие отправляет в сокет /var/run/dovecot/exim-quota-status три строки. В первой строке указан размер письма, во второй - его получатель, третья строка - пустая. Пустая строка сигнализирует о завершении запроса. Дальше в течение 5 секунд ожидается ответ. Если ответ поступил, то все переводы строк в ответе заменяются на пробелы. Если ответ не поступил, то вместо ответа дальше будет использоваться строка "action=FAIL". В результате должна получиться строка, в которой содержится ассоциативный массив, в котором разделителями элементов являются пробелы, а разделителями ключей и значений - знаки "равно", вот такая:

key1=value1 key2=value2 key3=value3

Выражение extract извлекает из этого словаря значение ключа action. Если значение равно OVER, то считается, что ящик переполнен и отправителю сообщается, что он должен отложить письмо в очередь, т.к. в данный момент почтовый ящик одного из получателей переполнен.

Перезагрузим Exim, чтобы настройки вступили в силу:

# systemctl reload exim4

Письма на переполненный ящик отбиваются с таким сообщением в журнале:

2019-06-11 22:48:31 H=forward100p.mail.yandex.net [77.88.28.100] X=TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256 CV=no F= temporarily rejected RCPT : 422 Mailbox yyy@domain.tld is over quota

Настройка SSL/TLS

Изменим настройки прослушиваемых портов в главной секции файла /etc/exim4/exim4.conf, добавив в список порт 465:

daemon_smtp_ports = 25 : 465 : 587

Добавим настройки TLS в главную секцию файла /etc/exim4/exim4.conf:

tls_on_connect_ports = 465
tls_advertise_hosts = *
tls_certificate = /etc/ssl/mail_public.pem
tls_privatekey = /etc/ssl/mail_private.pem

Порт 465 используется для подключения сразу по защищённому каналу SSL, без явного согласования перехода на защищённый обмен данными. Порт 587 обычно используется для подключений со стороны почтовых клиентов, как правило, с обязательным использованием аутентификации. В рассматриваемой конфигурации порты 25 и 587 никак не различаются, поведение сервера одинаково на обоих портах.

Для задействования добавленных настроек TLS, перезапустим почтовый сервер:

# systemctl restart exim4

Настройка DKIM-подписей

Для удобного создания ключей DKIM-подписей можно установить пакет opendkim-tools:

# apt-get install opendkim-tools

На самом деле необходимые ключи можно генерировать и при помощи openssl, т.к. пакет opendkim-tools содержит набор shell-скриптов, являющихся обёрткой над утилитой openssl.

Теперь создадим каталог для ключей и сгенерируем пару ключей для домена domain.tld:

# mkdir /etc/exim4/dkim
# cd /etc/exim4/dkim
# opendkim-genkey -D /etc/exim4/dkim/ -d domain.tld -s mail
# mv mail.private mail.domain.tld.private
# mv mail.txt mail.domain.tld.txt

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

В файле /etc/exim4/dkim/domains пропишем соответствие доменов отправителей и используемых приватных ключей:

domain.tld: key=/etc/exim4/dkim/mail.domain.tld.private

Выставим права доступа к файлам приватных ключей:

# cd /etc/exim4/dkim
# chmod u=rw,g=r,o= *.private
# chown root:Debian-exim *.private

Добавляем в секцию транспортов файла /etc/exim4/exim4.conf, в транспорт remote_smtp, настройки для добавления DKIM-подписей к письмам:

  # Транспорт для удалённых получателей
  # Добавляем к исходящим письмам DKIM-подпись
  remote_smtp:
    driver = smtp
    dkim_domain = ${lookup{$sender_address_domain}lsearch*@{/etc/exim4/dkim/domains}{$sender_address_domain}{}}
    dkim_selector = mail
    dkim_private_key = ${extract{key}{${lookup{$sender_address_domain}lsearch*@{/etc/exim4/dkim/domains}}}{$value}{}}

Достаточно перезагрузить конфигурацию, чтобы письма во внешние домены начали подписываться DKIM-ключами:

# systemctl reload exim4

Проверка DKIM-подписей

Воспользуемся встроенной в Exim возможностью проверки DKIM-подписей входящих писем. Я буду проверять подписи у тех писем, в которых они есть. Плюс к тому, будем требовать наличия правильной DKIM-подписи для доменов публичных почтовых сервисов, о которых заведомо известно, что они добавляют DKIM-подписи к своим письмам. Это позволит защититься от поддельных писем, якобы исходящих из доменов этих почтовых сервисов.

Зададим в главной секции файла /etc/exim4/exim4.conf домены, для которых требуется правильная DKIM-подпись:

domainlist dkim_required_domains = gmail.com : yandex.ru : rambler.ru : \
                                   mail.ru : bk.ru : list.ru : inbox.ru

Практика показала, что неправильная настройка DKIM-подписей - не редкость даже в солидных компаниях. Тем не менее, от них бывает нужно принимать письма. Впишем в главную секцию файла /etc/exim4/exim4.conf домены, у которых не нужно проверять DKIM-подпись, даже при её наличии:

# Домены, до которых подпись DKIM проверяться не будет
domainlist dkim_accept_domains = bashgaz.ru : brsc.ru

В эту же главную секцию файла /etc/exim4/exim4.conf добавим имя списка управления доступом, который будет проверять DKIM-подпись:

acl_smtp_dkim = acl_check_dkim

В секцию acl файла /etc/exim4/exim4.conf добавим описание самого списка управления доступом:

acl_check_dkim:

  # Принимаем письма от указанных доменов без проверки подписи DKIM
  accept sender_domains = +dkim_accept_domains

  # Отклоняем письма с неправильной DKIM-подписью
  deny message = Wrong DKIM signature 
       dkim_status = fail

  # Для выбранных доменов требуем наличия DKIM-подписи
  deny message = Valid DKIM signature needed for mail from $sender_domain
       sender_domains = +dkim_required_domains
       dkim_status = none

  accept

Перезагрузим файл конфигурации Exim, чтобы настройки вступили в силу:

# systemctl reload exim4

Настройка грейлистинга

Для грейлистинга воспользуемся демоном greylistd, написанном на Python. Этот демон не настолько сложен, как milter-greylist, которым я воспользовался для настройки грейлистинга в Postfix, однако его простота с лихвой компенсируется возможностями Exim. Установим пакет greylistd:

# apt-get install greylistd

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

# Если отправитель попал в чёрный список, отправляем его в грейлистинг
defer message = Greylisting in action, try later
      !senders = :
      !hosts = ${if exists{/etc/greylistd/whitelist-hosts}\
                          {/etc/greylistd/whitelist-hosts}{}} : \
               ${if exists{/var/lib/greylistd/whitelist-hosts}\
                          {/var/lib/greylistd/whitelist-hosts}{}}
      dnslists = zen.spamhaus.org : exitnodes.tor.dnsbl.sectoor.de
      condition = ${readsocket{/var/run/greylistd/socket}\
                              {--grey $sender_host_address $sender_address $local_part@$domain}\
                              {5s}{}{false}}

В поле !senders можно прописать адреса тех отправителей, которые не должны подвергаться грейлистингу. Соответственно, чтобы узел с определённым IP-адресом не подвергался грейлистингу, его можно добавить в файл /etc/greylistd/whitelist-hosts.

Включим Debian-exim в группу greylist, чтобы Exim имел доступ к сокету и файлам greylistd:

# usermod -aG greylist Debian-exim

Осталось попросить Exim перезагрузить файл конфигурации, чтобы новые настройки вступили в силу:

# systemctl reload exim4

Настройка SPF-записи

SPF-запись - это TXT-запись следующего вида:

domain.tld. IN TXT "v=spf1 +mx ~all"

Если указанный домен обслуживает Sender Policy Framework, описывающему синтаксис SFP-записи - SPF Record Syntax. Стоит также прочесть о наиболее частых ошибках, допускаемых при создании SFP-записи - Common mistakes.

Проверка SPF-записей

К счастью, поддержка проверки SPF, встроенная в Exim, более не находится в статусе экспериментальной и для её включения больше не нужно пересобирать пакет, т.к. он уже собран с соответствующей опцией.

SPF-записи могут классифицировать IP-адрес одним из следующих образов:

  • pass (+) - рекомендуется принять почту,
  • fail (-) - рекомендуется отклонить почту,
  • softfail (~) - рекомендуется принять почту, но пометить её как подозрительную,
  • neutral (?) - рекомендуется обрабатывать почту таким образом, как будто SPF-записи не существует.

Дополнительно, есть ещё два статуса, которые сообщают о постоянной или временной ошибке проверки IP-адреса.

Когда SPF-записи были только придуманы, некоторые системные администраторы слишком буквально воспринимали их рекомендации. Случались ситуации, когда первичный почтовый сервер не принимал почту от своего резервного сервера лишь потому, что его IP-адресу соответствовала SPF-запись, предписывающая не принимать письмо. Поэтому сложилась практика не указывать политику fail, а использовать вместо неё политики softfail или neutral. На мой взгляд, если бы не было таких прямолинейных системных администраторов, не было бы никакого смысла в политиках, отличных от pass и fail.

Нормальная почта может исходить только от серверов отправителя и должна приходить на серверы получателя без каких-либо посторонних промежуточных серверов. Сервер получателя должен проверять соответствие отправителя SPF-политике на основных и резервных серверах, а при приёме писем с резервного сервера на основной уже не обращать внимания на то, что его резервный сервер не удовлетворяет политике SPF. Именно поэтому я воспринимаю политики softfail и neutral точно так же, как воспринимаю политику fail. Я в любом случае приму почту от резервного сервера, не смотря на рекомендации SPF-записи, но на резервном сервере я обязательно их проверю.

Перед правилами проверки грейлистинга в списке управления доступом acl_check_rcpt в секции acl файла /etc/exim4/exim4.conf добавим следующее правило, соответствующее описанным выше соображениям:

# Отклоняем письма, не соответствующие политике домена отправителя
deny message = Reject due SPF policy
     spf = fail : softfail : neutral

Осталось лишь перезапустить Exim, чтобы заработал демон из собранного нами пакета и вступили в силу новые настройки:

# systemctl reload exim4

Требование аутентификации

Чтобы запретить локальным пользователям отправлять почту без аутентификации, добавим в конфигурацию такое правило:

# Не даём локальным отправителям слать почту без аутентификации
deny message = Local sender must be authenticated
     sender_domains = +local_domains
     !authenticated = *

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

# Не даём локальным отправителям представляться чужим именем
deny message = Send your letters from your mailbox
     condition = ${if eq{$authenticated_id}{$sender_address}{no}{yes}}
     authenticated = *

Оба правила нужно добавить в секцию acl файла /etc/exim4/exim4.conf, в правило acl_check_rcpt перед принятием почты от аутентифицированных пользователей.

Осталось перезагрузить файл конфигурации, чтобы она вступила в силу:

# systemctl reload exim4

Итоговый файл конфигурации

В конечном итоге у меня получился такой файл конфигурации /etc/exim4/exim4.conf:

# Имя нашей почтовой системы
primary_hostname = mail.domain.tld

# Список доменов нашей почтовой системы
domainlist local_domains = /etc/exim4/local_domains

# Список доменов, для которых наша почтовая система является резервной
domainlist relay_domains = /etc/exim4/relay_domains

# Домены, для которых требуется наличие правильной DKIM-подписи
domainlist dkim_required_domains = gmail.com : yandex.ru : rambler.ru : \
                                   mail.ru : bk.ru : list.ru : inbox.ru

# Домены, до которых подпись DKIM проверяться не будет
domainlist dkim_accept_domains = bashgaz.ru : brsc.ru

# Список узлов, почту от которых будем принимать без проверок
hostlist relay_from_hosts = 

# Правила для проверок
acl_not_smtp = acl_check_not_smtp
acl_smtp_rcpt = acl_check_rcpt
acl_smtp_data = acl_check_data

# Сокет-файл антивируса ClamAV
av_scanner = clamd:/run/clamav/clamd.ctl
# Сокет-файл SpamAssassin
# spamd_address =

# Отключаем IPv6, слушаем порты 25 и 587
disable_ipv6
daemon_smtp_ports = 25 : 465: 587

tls_on_connect_ports = 465
tls_advertise_hosts = *
tls_certificate = /etc/ssl/mail_public.pem
tls_privatekey = /etc/ssl/mail_private.pem

# Дописываем домены отправителя и получателя, если они не указаны
qualify_domain = domain.tld
qualify_recipient = domain.tld

# Exim никогда не должен запускать процессы от имени пользователя root
never_users = root

# Проверять прямую и обратную записи узла отправителя по DNS
host_lookup = *

# Отключаем проверку пользователей узла отправителя по протоколу ident
rfc1413_hosts = *
rfc1413_query_timeout = 0s

# Только эти узлы могут не указывать домен отправителя или получателя
sender_unqualified_hosts = +relay_from_hosts
recipient_unqualified_hosts = +relay_from_hosts

# Лимит размера сообщения, 30 мегабайт
message_size_limit = 30M

# Запрещаем использовать знак % для явной маршрутизации почты
percent_hack_domains =

# Настройки обработки ошибок доставки, используются значения по умолчанию
ignore_bounce_errors_after = 2d
timeout_frozen_after = 7d

begin acl

  # Проверки для локальных отправителей
  acl_check_not_smtp:
     accept

  # Проверки на этапе RCPT
  acl_check_rcpt:
    accept hosts = :

    # Отклоняем неправильные адреса почтовых ящиков  
    deny message = Restricted characters in address
         domains = +local_domains
         local_parts = ^[.] : ^.*[@%!/|]

    # Отклоняем неправильные адреса почтовых ящиков  
    deny message = Restricted characters in address
         domains = !+local_domains
         local_parts = ^[./|] : ^.*[@%!] : ^.*/\\.\\./

    # В локальные ящики postmaster и abuse принимает почту всегда
    accept local_parts = postmaster : abuse
           domains = +local_domains

    # Проверяем существование домена отправителя
    require verify = sender

    # Принимаем почту от доверенных узлов, попутно исправляя заголовки письма
    accept hosts = +relay_from_hosts
           control = submission

    # Не даём локальным отправителям слать почту без аутентификации
    deny message = Local sender must be authenticated
         sender_domains = +local_domains
         !authenticated = *

    # Не даём локальным отправителям представляться чужим именем
    deny message = Send your letters from your mailbox
         condition = ${if eq{$authenticated_id}{$sender_address}{no}{yes}}
         authenticated = *

    # Принимаем почту от аутентифицированных узлов, попутно исправляя заголовки письма
    accept authenticated = *
           control = submission/domain=

    # Для не доверенных и не аутентифицированных требуется, чтобы получатель был в домене,
    # ящик которого находится у нас или для которого мы являемся резервным почтовым сервером
    require message = Relay not permitted
            domains = +local_domains : +relay_domains

    # Если домен правильный, то проверяем получателя
    require verify = recipient

    # Отклоняем письма, не соответствующие политике домена отправителя
    deny message = Reject due SPF policy
         spf = fail : softfail : neutral

    # Отклоняем письма для адресатов, превысивших квоту
    defer message = 422 Mailbox $local_part@$domain is over quota
          domains = +local_domains
          condition = ${if eq{${extract{action}\
                                       {${readsocket{/var/run/dovecot/exim-quota-status}\
                                                    {size=$message_size\nrecipient=$local_part@$domain\n\n}\
                                                    {5s}\
                                                    { }\
                                                    {action=FAIL}}}}}\
                             {OVER}\
                             {yes}\
                             {no}}

    # Если отправитель попал в чёрный список, отправляем его в грейлистинг
    defer message = Greylisting in action, try later
          !senders = :
          !hosts = ${if exists{/etc/greylistd/whitelist-hosts}\
                              {/etc/greylistd/whitelist-hosts}{}} : \
                   ${if exists{/var/lib/greylistd/whitelist-hosts}\
                              {/var/lib/greylistd/whitelist-hosts}{}}
          dnslists = zen.spamhaus.org : exitnodes.tor.dnsbl.sectoor.de
          condition = ${readsocket{/var/run/greylistd/socket}\
                                  {--grey $sender_host_address $sender_address $local_part@$domain}\
                                  {5s}{}{false}}

    accept

  acl_check_data:

    deny message = message contrains a virus ($malware_name)
         malware = *

    accept

  acl_check_dkim:

    # Принимаем письма от указанных доменов без проверки подписи DKIM
    accept sender_domains = +dkim_accept_domains

    # Отклоняем письма с неправильной DKIM-подписью
    deny message = Wrong DKIM signature 
         dkim_status = fail

    # Для выбранных доменов требуем наличия DKIM-подписи
    deny message = Valid DKIM signature needed for mail from $sender_domain
         sender_domains = +dkim_required_domains
         dkim_status = none

    accept

begin routers

  # Поиск транспорта для удалённых получателей
  dnslookup:
    driver = dnslookup
    domains = ! +local_domains
    transport = remote_smtp
    ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8
    no_more

  # Пересылки для локальных получателей из файла /etc/aliases
  system_aliases:
    driver = redirect
    allow_fail
    allow_defer
    domains = domain.tld
    data = ${lookup{$local_part}lsearch{/etc/aliases}}

  # Пересылки для получателей в разных доменах
  aliases:
    driver = redirect
    allow_fail
    allow_defer
    data = ${lookup{$local_part@$domain}lsearch{/etc/exim4/aliases}}  

  # Получение почты на локальный ящик
  mailbox:
    driver = accept
    condition = ${lookup{$local_part@$domain}lsearch{/etc/dovecot/users}{yes}{no}}
    user = dovecot
    transport = dovecot_virtual_delivery
    cannot_route_message = Unknown user

begin transports

  # Транспорт для удалённых получателей
  remote_smtp:
    driver = smtp
    dkim_domain = ${lookup{$sender_address_domain}lsearch*@{/etc/exim4/dkim/domains}{$sender_address_domain}{}}
    dkim_selector = mail
    dkim_private_key = ${extract{key}{${lookup{$sender_address_domain}lsearch*@{/etc/exim4/dkim/domains}}}{$value}{}}

  # Транспорт для локальных получателей из Dovecot
  dovecot_virtual_delivery:
    driver = pipe
    command = /usr/lib/dovecot/dovecot-lda -d $local_part@$domain -f $sender_address
    message_prefix =
    message_suffix =
    delivery_date_add
    envelope_to_add
    return_path_add
    log_output
    user = vmail
    temp_errors = 64 : 69 : 70: 71 : 72 : 73 : 74 : 75 : 78

  begin retry

  *   *   F,2h,15m; G,16h,1h,1.5; F,4d,6h

  begin rewrite

begin authenticators

  # Использование LOGIN-аутентификации из Dovecot
  dovecot_login:
    driver = dovecot
    public_name = LOGIN
    server_socket = /var/run/dovecot/auth-userdb
    server_set_id = $auth1

  # Использование PLAIN-аутентификации из Dovecot  
  dovecot_plain:
    driver = dovecot
    public_name = PLAIN
    server_socket = /var/run/dovecot/auth-userdb
    server_set_id = $auth1

Заключение

В описанном варианте настройки мне не нравится то, что файлы с настройками не удалось передать в собственность отдельной группы пользователей. Это позволило бы предоставлять определённым системным пользователям доступ на редактирование этих файлов. Кроме того, можно было бы написать даже веб-приложение для управления доменами, почтовыми ящиками и пересылками. Не удалось это сделать по той простой причине, что Dovecot наотрез отказывается использовать вторичные группы при доступе к файлам. Возможно это недоработка самого Dovecot, а может быть - предусмотренное поведение. Сейчас у меня нет острой необходимости разобраться в этом, поэтому я оставил всё как получилось: пользователь Debian-exim состоит в группах dovecot и greylist, а в группе Debian-exim состоит пользователь clamav.